From 9d2687f8a1e46f7139dc4b47dd713413d83c460a Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Sat, 2 Aug 2025 10:11:32 +0200 Subject: [PATCH] add fancy frontend player --- Cargo.lock | 43 ++++ Cargo.toml | 3 +- src/main.rs | 96 ++++++++- static/howler.core.min.js | 2 + static/player.js | 405 ++++++++++++++++++++++++++++++++++++++ static/siriwave.js | 148 ++++++++++++++ static/styles.css | 331 +++++++++++++++++++++++++++++++ 7 files changed, 1025 insertions(+), 3 deletions(-) create mode 100644 static/howler.core.min.js create mode 100644 static/player.js create mode 100644 static/siriwave.js create mode 100644 static/styles.css diff --git a/Cargo.lock b/Cargo.lock index 7609589..a470ce2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,6 +689,30 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maud" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" +dependencies = [ + "axum-core", + "http", + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "mediatype" version = "0.20.0" @@ -800,6 +824,7 @@ dependencies = [ "axum", "bytes", "chrono", + "maud", "reqwest", "serde_json", "stream-download", @@ -835,6 +860,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", +] + [[package]] name = "quinn" version = "0.11.8" @@ -1482,6 +1519,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index f374949..1eb7470 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,5 @@ async-stream = "0.3" serde_json = "1" tracing = "0.1" stream-download = "0.22" -chrono = "0.4.41" +chrono = "0.4" +maud = { version = "0.27", features = ["axum"] } diff --git a/src/main.rs b/src/main.rs index 5b31760..76b1f1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,108 @@ mod state; mod streamer; -use axum::{routing::get, Router}; +use axum::{response::IntoResponse, routing::get, Router}; +use maud::{html, Markup, DOCTYPE}; +use reqwest::header; use state::AppState; use std::sync::Arc; +async fn index() -> Markup { + html! { + (DOCTYPE) + html lang="en" { + head { + meta charset="utf-8"; + meta name="viewport" content="user-scalable=no"; + title { "Ö1 Morgenjournal" } + link rel="stylesheet" href="/styles.css"; + } + body { + // Top Info + div #title { + span #track {} + div #timer { "0:00" } + div #duration { "0:00" } + } + + // Controls + div .controlsOuter { + div .controlsInner { + div #loading {} + div .btn #playBtn {} + div .btn #pauseBtn {} + div .btn #prevBtn {} + div .btn #nextBtn {} + } + div .btn #playlistBtn {} + div .btn #volumeBtn {} + } + + // Progress + div #waveform {} + div #bar {} + div #progress {} + + // Playlist + div #playlist { + div #list {} + } + + // Volume + div #volume .fadeout { + div #barFull .bar {} + div #barEmpty .bar {} + div #sliderBtn {} + } + + // Scripts + script src="/howler.core.min.js" {} + script src="/siriwave.js" {} + script src="/player.js" {} + } + } + } +} + +async fn styles() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/css")], + include_str!("../static/styles.css"), + ) +} + +async fn howler() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/javascript")], + include_str!("../static/howler.core.min.js").to_string(), + ) +} + +async fn player() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/javascript")], + include_str!("../static/player.js").to_string(), + ) +} + +async fn siriwave() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/javascript")], + include_str!("../static/siriwave.js").to_string(), + ) +} + #[tokio::main] async fn main() -> Result<(), Box> { let state = Arc::new(AppState::new()); let app = Router::new() - .route("/", get(streamer::stream_handler)) + .route("/", get(index)) + .route("/stream", get(streamer::stream_handler)) + .route("/howler.core.min.js", get(howler)) + .route("/styles.css", get(styles)) + .route("/player.js", get(player)) + .route("/siriwave.js", get(siriwave)) .with_state(state); println!("Streaming server running on http://localhost:3029"); diff --git a/static/howler.core.min.js b/static/howler.core.min.js new file mode 100644 index 0000000..3901ac4 --- /dev/null +++ b/static/howler.core.min.js @@ -0,0 +1,2 @@ +/*! howler.js v2.2.4 | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */ +!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=1e3,e._html5AudioPool=[],e.html5PoolSize=10,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.autoUnlock=!0,e._setup(),e},volume:function(e){var o=this||n;if(e=parseFloat(e),o.ctx||_(),void 0!==e&&e>=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator?e._navigator.userAgent:"",a=r.match(/OPR\/(\d+)/g),u=a&&parseInt(a[0].split("/")[1],10)<33,d=-1!==r.indexOf("Safari")&&-1===r.indexOf("Chrome"),i=r.match(/Version\/(.*?) /),_=d&&i&&parseInt(i[1],10)<15;return e._codecs={mp3:!(u||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!(o.canPlayType('audio/wav; codecs="1"')||o.canPlayType("audio/wav")).replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),m4b:!!(o.canPlayType("audio/x-m4b;")||o.canPlayType("audio/m4b;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),webm:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n;if(!e._audioUnlocked&&e.ctx){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var o=function(n){for(;e._html5AudioPool.length0?d._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(d._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3;d._sprite=e,d._ended=!1;var p=function(){d._paused=!1,d._seek=_,d._start=c,d._stop=f,d._loop=!(!d._loop&&!t._sprite[e][2])};if(_>=f)return void t._ended(d);var m=d._node;if(t._webAudio){var v=function(){t._playLock=!1,p(),t._refreshBuffer(d);var e=d._muted||t._muted?0:d._volume;m.gain.setValueAtTime(e,n.ctx.currentTime),d._playStart=n.ctx.currentTime,void 0===m.bufferSource.start?d._loop?m.bufferSource.noteGrainOn(0,_,86400):m.bufferSource.noteGrainOn(0,_,s):d._loop?m.bufferSource.start(0,_,86400):m.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l)),o||setTimeout(function(){t._emit("play",d._id),t._loadQueue()},0)};"running"===n.state&&"interrupted"!==n.ctx.state?v():(t._playLock=!0,t.once("resume",v),t._clearTimer(d._id))}else{var h=function(){m.currentTime=_,m.muted=d._muted||t._muted||n._muted||m.muted,m.volume=d._volume*n.volume(),m.playbackRate=d._rate;try{var r=m.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,p(),r.then(function(){t._playLock=!1,m._unlocked=!0,o?t._loadQueue():t._emit("play",d._id)}).catch(function(){t._playLock=!1,t._emit("playerror",d._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),d._ended=!0,d._paused=!0})):o||(t._playLock=!1,p(),t._emit("play",d._id)),m.playbackRate=d._rate,m.paused)return void t._emit("playerror",d._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==e||d._loop?t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l):(t._endTimers[d._id]=function(){t._ended(d),m.removeEventListener("ended",t._endTimers[d._id],!1)},m.addEventListener("ended",t._endTimers[d._id],!1))}catch(e){t._emit("playerror",d._id,e)}};"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"===m.src&&(m.src=t._src,m.load());var y=window&&window.ejecta||!m.readyState&&n._navigator.isCocoonJS;if(m.readyState>=3||y)h();else{t._playLock=!0,t._state="loading";var g=function(){t._state="loaded",h(),m.removeEventListener(n._canPlayEvent,g,!1)};m.addEventListener(n._canPlayEvent,g,!1),t._clearTimer(d._id)}}return d._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),d+=i*r,d=Math.round(100*d)/100,d=i<0?Math.max(o,d):Math.min(o,d),u._webAudio?e._volume=d:u.volume(d,e._id,!0),a&&(u._volume=d),(on&&d>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var d;if("number"!=typeof e)return d=t._soundById(o),d?d._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var i=0;i=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return 0;if("number"==typeof e&&("loaded"!==t._state||t._playLock))return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var d=t._soundById(o);if(d){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var i=t.playing(o)?n.ctx.currentTime-d._playStart:0,_=d._rateSeek?d._rateSeek-d._seek:0;return d._seek+(_+i*Math.abs(d._rate))}return d._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),d._seek=e,d._ended=!1,t._clearTimer(o),t._webAudio||!d._node||isNaN(d._node.duration)||(d._node.currentTime=e);var l=function(){s&&t.play(o,!0),t._emit("seek",o)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1);var u=!0;for(t=0;t=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t=0;if(!e.bufferSource)return o;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o},_clearSound:function(e){/MSIE |Trident\//.test(n._navigator&&n._navigator.userAgent)||(e.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):n.noAudio||(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._endFn=e._endListener.bind(e),e._node.addEventListener("ended",e._endFn,!1),e._node.src=o._src,e._node.preload=!0===o._preload?"auto":o._preload,e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)},_endListener:function(){var e=this,n=e._parent;n._duration===1/0&&(n._duration=Math.ceil(10*e._node.duration)/10,n._sprite.__default[1]===1/0&&(n._sprite.__default[1]=1e3*n._duration),n._ended(e)),e._node.removeEventListener("ended",e._endFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void i(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a0?(r[o._src]=e,i(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},i=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());n._navigator&&!r&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:n._volume,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof global?(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t):"undefined"!=typeof window&&(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t)}(); \ No newline at end of file diff --git a/static/player.js b/static/player.js new file mode 100644 index 0000000..5fe7d03 --- /dev/null +++ b/static/player.js @@ -0,0 +1,405 @@ +/*! + * Howler.js Audio Player Demo + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +// Cache references to DOM elements. +var elms = ['track', 'timer', 'duration', 'playBtn', 'pauseBtn', 'prevBtn', 'nextBtn', 'playlistBtn', 'volumeBtn', 'progress', 'bar', 'wave', 'loading', 'playlist', 'list', 'volume', 'barEmpty', 'barFull', 'sliderBtn']; +elms.forEach(function(elm) { + window[elm] = document.getElementById(elm); +}); + +/** + * Player class containing the state of our playlist and where we are in it. + * Includes all methods for playing, skipping, updating the display, etc. + * @param {Array} playlist Array of objects with playlist song details ({title, file, howl}). + */ +var Player = function(playlist) { + this.playlist = playlist; + this.index = 0; + + // Display the title of the first track. + track.innerHTML = '1. ' + playlist[0].title; + + // Setup the playlist display. + playlist.forEach(function(song) { + var div = document.createElement('div'); + div.className = 'list-song'; + div.innerHTML = song.title; + div.onclick = function() { + player.skipTo(playlist.indexOf(song)); + }; + list.appendChild(div); + }); +}; +Player.prototype = { + /** + * Play a song in the playlist. + * @param {Number} index Index of the song in the playlist (leave empty to play the first or current). + */ + play: function(index) { + var self = this; + var sound; + + index = typeof index === 'number' ? index : self.index; + var data = self.playlist[index]; + + // If we already loaded this track, use the current one. + // Otherwise, setup and load a new Howl. + if (data.howl) { + sound = data.howl; + } else { + sound = data.howl = new Howl({ + src: [ './' + data.file ], + html5: true, // Force to HTML5 so that the audio can stream in (best for large files). + onplay: function() { + // Display the duration. + duration.innerHTML = self.formatTime(Math.round(sound.duration())); + + // Start updating the progress of the track. + requestAnimationFrame(self.step.bind(self)); + + // Start the wave animation if we have already loaded + wave.container.style.display = 'block'; + bar.style.display = 'none'; + pauseBtn.style.display = 'block'; + }, + onload: function() { + // Start the wave animation. + wave.container.style.display = 'block'; + bar.style.display = 'none'; + loading.style.display = 'none'; + }, + onend: function() { + // Stop the wave animation. + wave.container.style.display = 'none'; + bar.style.display = 'block'; + self.skip('next'); + }, + onpause: function() { + // Stop the wave animation. + wave.container.style.display = 'none'; + bar.style.display = 'block'; + }, + onstop: function() { + // Stop the wave animation. + wave.container.style.display = 'none'; + bar.style.display = 'block'; + }, + onseek: function() { + // Start updating the progress of the track. + requestAnimationFrame(self.step.bind(self)); + } + }); + } + + // Begin playing the sound. + sound.play(); + + // Update the track display. + track.innerHTML = (index + 1) + '. ' + data.title; + + // Show the pause button. + if (sound.state() === 'loaded') { + playBtn.style.display = 'none'; + pauseBtn.style.display = 'block'; + } else { + loading.style.display = 'block'; + playBtn.style.display = 'none'; + pauseBtn.style.display = 'none'; + } + + // Keep track of the index we are currently playing. + self.index = index; + }, + + /** + * Pause the currently playing track. + */ + pause: function() { + var self = this; + + // Get the Howl we want to manipulate. + var sound = self.playlist[self.index].howl; + + // Puase the sound. + sound.pause(); + + // Show the play button. + playBtn.style.display = 'block'; + pauseBtn.style.display = 'none'; + }, + + /** + * Skip to the next or previous track. + * @param {String} direction 'next' or 'prev'. + */ + skip: function(direction) { + var self = this; + + // Get the next track based on the direction of the track. + var index = 0; + if (direction === 'prev') { + index = self.index - 1; + if (index < 0) { + index = self.playlist.length - 1; + } + } else { + index = self.index + 1; + if (index >= self.playlist.length) { + index = 0; + } + } + + self.skipTo(index); + }, + + /** + * Skip to a specific track based on its playlist index. + * @param {Number} index Index in the playlist. + */ + skipTo: function(index) { + var self = this; + + // Stop the current track. + if (self.playlist[self.index].howl) { + self.playlist[self.index].howl.stop(); + } + + // Reset progress. + progress.style.width = '0%'; + + // Play the new track. + self.play(index); + }, + + /** + * Set the volume and update the volume slider display. + * @param {Number} val Volume between 0 and 1. + */ + volume: function(val) { + var self = this; + + // Update the global volume (affecting all Howls). + Howler.volume(val); + + // Update the display on the slider. + var barWidth = (val * 90) / 100; + barFull.style.width = (barWidth * 100) + '%'; + sliderBtn.style.left = (window.innerWidth * barWidth + window.innerWidth * 0.05 - 25) + 'px'; + }, + + /** + * Seek to a new position in the currently playing track. + * @param {Number} per Percentage through the song to skip. + */ + seek: function(per) { + var self = this; + + // Get the Howl we want to manipulate. + var sound = self.playlist[self.index].howl; + + // Convert the percent into a seek position. + if (sound.playing()) { + sound.seek(sound.duration() * per); + } + }, + + /** + * The step called within requestAnimationFrame to update the playback position. + */ + step: function() { + var self = this; + + // Get the Howl we want to manipulate. + var sound = self.playlist[self.index].howl; + + // Determine our current seek position. + var seek = sound.seek() || 0; + timer.innerHTML = self.formatTime(Math.round(seek)); + progress.style.width = (((seek / sound.duration()) * 100) || 0) + '%'; + + // If the sound is still playing, continue stepping. + if (sound.playing()) { + requestAnimationFrame(self.step.bind(self)); + } + }, + + /** + * Toggle the playlist display on/off. + */ + togglePlaylist: function() { + var self = this; + var display = (playlist.style.display === 'block') ? 'none' : 'block'; + + setTimeout(function() { + playlist.style.display = display; + }, (display === 'block') ? 0 : 500); + playlist.className = (display === 'block') ? 'fadein' : 'fadeout'; + }, + + /** + * Toggle the volume display on/off. + */ + toggleVolume: function() { + var self = this; + var display = (volume.style.display === 'block') ? 'none' : 'block'; + + setTimeout(function() { + volume.style.display = display; + }, (display === 'block') ? 0 : 500); + volume.className = (display === 'block') ? 'fadein' : 'fadeout'; + }, + + /** + * Format the time from seconds to M:SS. + * @param {Number} secs Seconds to format. + * @return {String} Formatted time. + */ + formatTime: function(secs) { + var minutes = Math.floor(secs / 60) || 0; + var seconds = (secs - minutes * 60) || 0; + + return minutes + ':' + (seconds < 10 ? '0' : '') + seconds; + } +}; + +// Setup our new audio player class and pass it the playlist. +var player = new Player([ + { + title: 'Ö1 Morgenjournal', + file: 'stream', + howl: null + } +]); + +// Bind our player controls. +playBtn.addEventListener('click', function() { + player.play(); +}); +pauseBtn.addEventListener('click', function() { + player.pause(); +}); +prevBtn.addEventListener('click', function() { + var sound = player.playlist[player.index].howl; + + if (sound && sound.playing()) { + var currentSeek = sound.seek() || 0; + var newSeek = currentSeek - 10; // Subtract 10 seconds + var duration = sound.duration(); + + // Make sure we don't seek beyond the end of the track + if (newSeek > 0) { + sound.seek(newSeek); + } else { + // If seeking would go past the beginning, go to the beginning + sound.seek(0); + } + } +}); +nextBtn.addEventListener('click', function() { + var sound = player.playlist[player.index].howl; + + if (sound && sound.playing()) { + var currentSeek = sound.seek() || 0; + var newSeek = currentSeek + 10; // Add 10 seconds + var duration = sound.duration(); + + // Make sure we don't seek beyond the end of the track + if (newSeek < duration) { + sound.seek(newSeek); + } else { + // If seeking would go past the end, go to the end + sound.seek(duration - 0.1); // Leave a small buffer to avoid issues + } + } +}); +waveform.addEventListener('click', function(event) { + player.seek(event.clientX / window.innerWidth); +}); +playlistBtn.addEventListener('click', function() { + player.togglePlaylist(); +}); +playlist.addEventListener('click', function() { + player.togglePlaylist(); +}); +volumeBtn.addEventListener('click', function() { + player.toggleVolume(); +}); +volume.addEventListener('click', function() { + player.toggleVolume(); +}); + +// Setup the event listeners to enable dragging of volume slider. +barEmpty.addEventListener('click', function(event) { + var per = event.layerX / parseFloat(barEmpty.scrollWidth); + player.volume(per); +}); +sliderBtn.addEventListener('mousedown', function() { + window.sliderDown = true; +}); +sliderBtn.addEventListener('touchstart', function() { + window.sliderDown = true; +}); +volume.addEventListener('mouseup', function() { + window.sliderDown = false; +}); +volume.addEventListener('touchend', function() { + window.sliderDown = false; +}); + +var move = function(event) { + if (window.sliderDown) { + var x = event.clientX || event.touches[0].clientX; + var startX = window.innerWidth * 0.05; + var layerX = x - startX; + var per = Math.min(1, Math.max(0, layerX / parseFloat(barEmpty.scrollWidth))); + player.volume(per); + } +}; + +volume.addEventListener('mousemove', move); +volume.addEventListener('touchmove', move); + +// Setup the "waveform" animation. +var wave = new SiriWave({ + container: waveform, + width: window.innerWidth, + height: window.innerHeight * 0.3, + cover: true, + speed: 0.03, + amplitude: 0.7, + frequency: 2 +}); +wave.start(); + +// Update the height of the wave animation. +// These are basically some hacks to get SiriWave.js to do what we want. +var resize = function() { + var height = window.innerHeight * 0.3; + var width = window.innerWidth; + wave.height = height; + wave.height_2 = height / 2; + wave.MAX = wave.height_2 - 4; + wave.width = width; + wave.width_2 = width / 2; + wave.width_4 = width / 4; + wave.canvas.height = height; + wave.canvas.width = width; + wave.container.style.margin = -(height / 2) + 'px auto'; + + // Update the position of the slider. + var sound = player.playlist[player.index].howl; + if (sound) { + var vol = sound.volume(); + var barWidth = (vol * 0.9); + sliderBtn.style.left = (window.innerWidth * barWidth + window.innerWidth * 0.05 - 25) + 'px'; + } +}; +window.addEventListener('resize', resize); +resize(); diff --git a/static/siriwave.js b/static/siriwave.js new file mode 100644 index 0000000..f93e375 --- /dev/null +++ b/static/siriwave.js @@ -0,0 +1,148 @@ +/* Modified from https://github.com/CaffeinaLab/SiriWaveJS */ + +(function() { + +function SiriWave(opt) { + opt = opt || {}; + + this.phase = 0; + this.run = false; + + // UI vars + + this.ratio = opt.ratio || window.devicePixelRatio || 1; + + this.width = this.ratio * (opt.width || 320); + this.width_2 = this.width / 2; + this.width_4 = this.width / 4; + + this.height = this.ratio * (opt.height || 100); + this.height_2 = this.height / 2; + + this.MAX = (this.height_2) - 4; + + // Constructor opt + + this.amplitude = opt.amplitude || 1; + this.speed = opt.speed || 0.2; + this.frequency = opt.frequency || 6; + this.color = (function hex2rgb(hex){ + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function(m,r,g,b) { return r + r + g + g + b + b; }); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? + parseInt(result[1],16).toString()+','+parseInt(result[2], 16).toString()+','+parseInt(result[3], 16).toString() + : null; + })(opt.color || '#fff') || '255,255,255'; + + // Canvas + + this.canvas = document.createElement('canvas'); + this.canvas.width = this.width; + this.canvas.height = this.height; + if (opt.cover) { + this.canvas.style.width = this.canvas.style.height = '100%'; + } else { + this.canvas.style.width = (this.width / this.ratio) + 'px'; + this.canvas.style.height = (this.height / this.ratio) + 'px'; + }; + + this.container = opt.container || document.body; + this.container.appendChild(this.canvas); + + this.ctx = this.canvas.getContext('2d'); + + // Start + + if (opt.autostart) { + this.start(); + } +} + +SiriWave.prototype._GATF_cache = {}; +SiriWave.prototype._globAttFunc = function(x) { + if (SiriWave.prototype._GATF_cache[x] == null) { + SiriWave.prototype._GATF_cache[x] = Math.pow(4/(4+Math.pow(x,4)), 4); + } + return SiriWave.prototype._GATF_cache[x]; +}; + +SiriWave.prototype._xpos = function(i) { + return this.width_2 + i * this.width_4; +}; + +SiriWave.prototype._ypos = function(i, attenuation) { + var att = (this.MAX * this.amplitude) / attenuation; + return this.height_2 + this._globAttFunc(i) * att * Math.sin(this.frequency * i - this.phase); +}; + +SiriWave.prototype._drawLine = function(attenuation, color, width){ + this.ctx.moveTo(0,0); + this.ctx.beginPath(); + this.ctx.strokeStyle = color; + this.ctx.lineWidth = width || 1; + + var i = -2; + while ((i += 0.01) <= 2) { + var y = this._ypos(i, attenuation); + if (Math.abs(i) >= 1.90) y = this.height_2; + this.ctx.lineTo(this._xpos(i), y); + } + + this.ctx.stroke(); +}; + +SiriWave.prototype._clear = function() { + this.ctx.globalCompositeOperation = 'destination-out'; + this.ctx.fillRect(0, 0, this.width, this.height); + this.ctx.globalCompositeOperation = 'source-over'; +}; + +SiriWave.prototype._draw = function() { + if (this.run === false) return; + + this.phase = (this.phase + Math.PI*this.speed) % (2*Math.PI); + + this._clear(); + this._drawLine(-2, 'rgba(' + this.color + ',0.1)'); + this._drawLine(-6, 'rgba(' + this.color + ',0.2)'); + this._drawLine(4, 'rgba(' + this.color + ',0.4)'); + this._drawLine(2, 'rgba(' + this.color + ',0.6)'); + this._drawLine(1, 'rgba(' + this.color + ',1)', 1.5); + + if (window.requestAnimationFrame) { + requestAnimationFrame(this._draw.bind(this)); + return; + }; + setTimeout(this._draw.bind(this), 20); +}; + +/* API */ + +SiriWave.prototype.start = function() { + this.phase = 0; + this.run = true; + this._draw(); +}; + +SiriWave.prototype.stop = function() { + this.phase = 0; + this.run = false; +}; + +SiriWave.prototype.setSpeed = function(v) { + this.speed = v; +}; + +SiriWave.prototype.setNoise = SiriWave.prototype.setAmplitude = function(v) { + this.amplitude = Math.max(Math.min(v, 1), 0); +}; + + +if (typeof define === 'function' && define.amd) { + define(function(){ return SiriWave; }); + return; +}; +window.SiriWave = SiriWave; + +})(); diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..6383747 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,331 @@ +/* https://howlerjs.com/assets/howler.js/examples/player/styles.css */ +html { + width: 100%; + height: 100%; + overflow: hidden; + padding: 0; + margin: 0; + outline: 0; +} + +body { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + overflow: hidden; + background: #bb71f3; + background: -moz-linear-gradient(-45deg, #bb71f3 0%, #3d4d91 100%); + background: -webkit-linear-gradient(-45deg, #bb71f3 0%, #3d4d91 100%); + background: linear-gradient(135deg, #bb71f3 0%, #3d4d91 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#bb71f3', endColorstr='#3d4d91', GradientType=1); + font-family: "Helvetica Neue", "Futura", "Trebuchet MS", Arial; + -webkit-user-select: none; + user-select: none; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); +} + +/* Top Info */ +#title { + position: absolute; + width: 100%; + top: 3%; + line-height: 34px; + height: 34px; + text-align: center; + font-size: 34px; + opacity: 0.9; + font-weight: 300; + color: #fff; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33); +} +#timer { + position: absolute; + top: 0; + left: 3%; + text-align: left; + font-size: 26px; + opacity: 0.9; + font-weight: 300; + color: #fff; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33); +} +#duration { + position: absolute; + top: 0; + right: 3%; + text-align: right; + font-size: 26px; + opacity: 0.5; + font-weight: 300; + color: #fff; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33); +} + +/* Controls */ +.controlsOuter { + position: absolute; + width: 100%; + height: 70px; + bottom: 3%; +} +.controlsInner { + position: absolute; + width: 340px; + height: 70px; + left: 50%; + margin: 0 -170px; +} +.btn { + position: absolute; + cursor: pointer; + opacity: 0.9; + -webkit-filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.33)); + filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.33)); + -webkit-user-select: none; + user-select: none; +} +.btn:hover { + opacity: 1; +} +#playBtn { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEUAAABGCAYAAACaGVmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QThERThENUFDNkJEMTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QThERThENUJDNkJEMTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBOERFOEQ1OEM2QkQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBOERFOEQ1OUM2QkQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnFM7yIAAAFWSURBVHja7NRJDoMwDEDRcPL2ZoVeLEUtCyoCZPBsf8mS10+Wp5zzK6X0XGdJ0bdpRcnbPgfOESUFzjmKe5wrFLc4NSjucFpQ3OD0oJjHGUExiwOBYg4HEsUMDgaKehxMFLU4FCjqcChR1OBwoIjH4UQRiyMBRRyOJBQxOBJR2HEko7DhaEAhx9GEQoajEQUdRzMKGo4FFHAcSyhgOBZRhnEso3TjeEBpxvGEUo3jEeUWxzPKKU6gFHACpYATKMfegbLDWOcRl/Jr2X7JHI+2gOEZ5RTDI8othieUagwPKM0YllG6MSyiDGNYQgHDsIACjqEZBQ1DIwo6hiYUMgwNKOQYklHYMCSisGNIQhGDIQFFHAYnilgMDhTxGJQoajAoUNRhYKKoxcBAUY8BiWIGAwLFHMYIilmMHhTzGC0objBqUNxhXKG4xSihuMfYo8yB8d9HgAEATyefBJWpw70AAAAASUVORK5CYII='); + width: 69px; + height: 70px; + left: 50%; + margin: auto -34.5px; +} +#pauseBtn { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEUAAABGCAYAAACaGVmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MjkzRjNDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MjkzRjRDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBOERFOEQ1Q0M2QkQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowMTgyOTNGMkM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pne5d1AAAAB8SURBVHja7NCxCQAwAALBuP/QZoEUlinua0G4tD1j83Asv/4GChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFCpRXV4ABAOgpXYTDYwEwAAAAAElFTkSuQmCC'); + width: 69px; + height: 70px; + left: 50%; + margin: auto -34.5px; + display: none; +} +#prevBtn { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MjkzRkJDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MjkzRkNDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMTgyOTNGOUM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowMTgyOTNGQUM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PndeAH8AAACtSURBVHja7NXJCcAgEAVQEevK0lk6M1aW6CGQQBBn+ephBv7F0wNn8Vd7OWCWnOjd2FpzYk7K2cJAxFEA78cwA6I3porohWlCoDEkBArDQmhjRAgtjApCilFFcDEQBAcTUYinKLcJCqFinGEMYxjDgDFpJkzZwDsSRf2mE4ni9gwEJW1gVZTWNKmgtEdbhELtGRYKvfRIqF4buAnV+xxUUaNu0y9q9KH8oG4BBgDjdpk0jMoz1gAAAABJRU5ErkJggg=='); + width: 35px; + height: 35px; + left: 0; + top: 50%; + margin: -17.5px auto; +} +#nextBtn { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MjkzRjdDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MjkzRjhDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMTgyOTNGNUM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowMTgyOTNGNkM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PiFbkXsAAACvSURBVHja7NZBDoAgDARA9GPiz/yZ8DKsxgMHY1rYBQ5sshdCwiQcWpdSOqWb1BGrSn6RiTJjmKhiDANVjUGiYBgECo6pQdEwJSg6xoJqhtGgVFkeETZBekhjdqZ6g4H5QnXH5Cg/CkadiZmYiZkYdtYGb8QRMDdi144C1jfFd0AG69RG7jNB6nsvV3+IZhgNgo6xIGiYEgQcU4OAYRCIagwSUYxhIMwYJsKEuQQYABTs+qTMuF1rAAAAAElFTkSuQmCC'); + width: 35px; + height: 35px; + right: 0; + top: 50%; + margin: -17.5px auto; +} +#playlistBtn { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MUVBQzMwNzRDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MUVBQzMwNzVDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxRUFDMzA3MkM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxRUFDMzA3M0M3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnWBPa0AAADGSURBVHjaYvz//z/DYAFMDIMIjDpm1DHDwjH/0fDugXAMCxK7AojPAnEHmpo0IFaisTsqIEECLPT+Q4ALlL0bihmQ+LQGYLsYoSXwf2iovAdiYyjbdaCiaQ+S2FkopjtgHK2bCESTC5r4+4GIKuQEjAz2DGQCHhTlDAtaLtoDjSJkoATN7iMzmjrRxO+NljODsZzpwBJNs0bLGShwhToCvWHVQYes7YruGJiFgljKmpEbTa5YKsrRcmbUMaOOGXUMNQBAgAEAm2XFJrn+jrsAAAAASUVORK5CYII='); + width: 35px; + height: 35px; + top: 50%; + left: 3%; + margin: -17.5px auto; +} +#volumeBtn { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MUVBQzMwNzBDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MUVBQzMwNzFDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxRUFDMzA2RUM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxRUFDMzA2RkM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtAZ+FgAAAHRSURBVHja7FjRbYMwEIWqC7CCO4IzgjMCHYEV6AjuCHSEMAKMkIwAI5ARXFt9rox1phDA5KMnPSGwOZ4fx/mOVCmVPIu9JE9ke5GRGpejyeQanUapkXlj5lzEIMM1GqjRAyG1eNCLCeAVyDQq9WOdRoHrDeDP7YCM8reGiNQYQER6D6DIGHDc02xFJsfqjF00GDEnRCaBegp+RmMp8oyYERcmAAvMvWl8aLSBuQ2O58D4Ff7eqJiZa4MTF1OYUsZAwF9BKaOw0tsf6pjx+wwVXWU41Pwk1DF2opQRK7+skDIyECMlrv8GfoztwMaW9K7XTqKMujeZV8SQma31eO0iNpkW8L/a3t02Yu7aITL8CDL2S2VE/jqsnnHJ3I8urnpKldhkOEFmdB6TTO7kFveVRScjoExLqBWVTIbsax765RFh7n74GoFMiQefCLXcbWFEhs/MFfeFZGon9fsx1I78PVDPyA3qmXyqnhEzV1lgRT124/rBSq+Dj/Pa7sDUPVesrEGRvUSZMlQ/rSmgCqc7qBZ0B7aQ37RVsb2QJOIp1DcNUDXbg4wFw2qV06j5ZCqQYSE/yYZ1LxVPvjJ86v50p/8ztr96X3JT+v+zKGDfAgwAZkMcn0x+AsMAAAAASUVORK5CYII='); + width: 35px; + height: 35px; + top: 50%; + right: 3%; + margin: -17.5px auto; +} + +/* Progress */ +#waveform { + width: 100%; + height: 30%; + position: absolute; + left: 0; + top: 50%; + margin: -15% auto; + display: none; + cursor: pointer; + opacity: 0.8; + -webkit-user-select: none; + user-select: none; +} +#waveform:hover { + opacity: 1; +} +#bar { + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: 2px; + background-color: rgba(255, 255, 255, 0.9); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33); + opacity: 0.9; +} +#progress { + position: absolute; + top: 0; + left: 0; + width: 0%; + height: 100%; + background-color: rgba(0, 0, 0, 0.1); + z-index: -1; +} + +/* Loading */ +#loading { + position: absolute; + left: 50%; + top: 50%; + margin: -35px; + width: 70px; + height: 70px; + background-color: #fff; + border-radius: 100%; + -webkit-animation: sk-scaleout 1.0s infinite ease-in-out; + animation: sk-scaleout 1.0s infinite ease-in-out; + display: none; +} +@-webkit-keyframes sk-scaleout { + 0% { -webkit-transform: scale(0) } + 100% { + -webkit-transform: scale(1.0); + opacity: 0; + } +} +@keyframes sk-scaleout { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } 100% { + -webkit-transform: scale(1.0); + transform: scale(1.0); + opacity: 0; + } +} + +/* Plylist */ +#playlist { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.5); + display: none; +} +#list { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} +.list-song { + width: 100%; + height: 120px; + font-size: 50px; + line-height: 120px; + text-align: center; + font-weight: bold; + color: #fff; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33); +} +.list-song:hover { + background-color: rgba(255, 255, 255, 0.1); + cursor: pointer; +} + +/* Volume */ +#volume { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.5); + touch-action: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + display: none; +} +.bar { + position: absolute; + top: 50%; + left: 5%; + margin: -5px auto; + height: 10px; + background-color: rgba(255, 255, 255, 0.9); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33); +} +#barEmpty { + width: 90%; + opacity: 0.5; + box-shadow: none; + cursor: pointer; +} +#barFull { + width: 90%; +} +#sliderBtn { + width: 50px; + height: 50px; + position: absolute; + top: 50%; + left: 93.25%; + margin: -25px auto; + background-color: rgba(255, 255, 255, 0.8); + box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.33); + border-radius: 25px; + cursor: pointer; +} + +/* Fade-In */ +.fadeout { + webkit-animation: fadeout 0.5s; + -ms-animation: fadeout 0.5s; + animation: fadeout 0.5s; +} +.fadein { + webkit-animation: fadein 0.5s; + -ms-animation: fadein 0.5s; + animation: fadein 0.5s; +} +@keyframes fadein { + from { opacity: 0; } + to { opacity: 1; } +} +@-webkit-keyframes fadein { + from { opacity: 0; } + to { opacity: 1; } +} +@-ms-keyframes fadein { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes fadeout { + from { opacity: 1; } + to { opacity: 0; } +} +@-webkit-keyframes fadeout { + from { opacity: 1; } + to { opacity: 0; } +} +@-ms-keyframes fadeout { + from { opacity: 1; } + to { opacity: 0; } +}