From 27ab02eccc1a772b6817a65536bc514089130c43 Mon Sep 17 00:00:00 2001 From: Matthew Potter Date: Tue, 23 Jul 2019 17:23:12 -0400 Subject: [PATCH 1/6] Private vars and event emitting I've added several event emitters throughout the class in hopes to eventually take all DOM manipulation out of the player class. It should all be within the app level instead and simply use events and variables from the class. --- index.html | 53 ++++---------- player.js | 210 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 139 insertions(+), 124 deletions(-) diff --git a/index.html b/index.html index ca0d1d2..794397c 100644 --- a/index.html +++ b/index.html @@ -131,8 +131,8 @@

24/7 music designed for coding.

if (window.mobilecheck() === false) { //set up the footer const footer = document.getElementById("footer"); - footer.className = "footer"; - var footerBlock = ` + footer.classList.add("footer"); + footer.innerHTML = `
@@ -154,54 +154,33 @@

24/7 music designed for coding.

`; - footer.innerHTML = footerBlock; //setup the info box for keyboard shortcuts const details = document.getElementById("details"); - details.className = "details"; - var detailsBlock = ` + details.classList.add('details'); + details.innerHTML = ` Keyboard Controls
Play/Pause:
Spacebar or "k"
Volume:
Up Arrow / Down Arrow
`; - details.innerHTML = detailsBlock; //start the show - var fCC_Player = new CodeRadio(); - var playButton = document.getElementById("playButton"); - var slider = document.getElementById("slider"); - - // set button initial state - if (fCC_Player._player.paused) { - playButton.src = - "https://cdn-media-1.freecodecamp.org/code-radio/play.svg"; - } + var fCC_Player = new CodeRadio(), + playButton = document.getElementById("playButton"), + slider = document.getElementById("slider"), + playContainer = document.getElementById("playContainer"); // set the vol slider - slider.oninput = function() { - fCC_Player.setTargetVolume(this.value / 10); - }; - - fCC_Player._player.onplay = function() { - playButton.src = - "https://cdn-media-1.freecodecamp.org/code-radio/pause.svg"; - }; - - fCC_Player._player.onpause = function() { - playButton.src = - "https://cdn-media-1.freecodecamp.org/code-radio/play.svg"; - }; - - fCC_Player._player.onvolumechange = function() { - slider.value = Math.round(fCC_Player._player.volume * 10); - }; - + slider.addEventListener('input', () => fCC_Player.setTargetVolume(slider.value / 10)); // play pause button condition - function handleToggleButton() { - fCC_Player.togglePlay(); - } - document.getElementById("playContainer").onclick = handleToggleButton; + playContainer.addEventListener('click', () => fCC_Player.togglePlay()); + + fCC_Player.on('play', () => playButton.src = "https://cdn-media-1.freecodecamp.org/code-radio/pause.svg"); + fCC_Player.on('pause', () => playButton.src = "https://cdn-media-1.freecodecamp.org/code-radio/play.svg"); + fCC_Player.on('volumeChange', (volume) => slider.value = Math.round(volume * 10)); + fCC_Player.on('newSong', (songData) => fCC_Player.renderMetadata()); + fCC_Player.on('listeners', (count) => document.querySelector('[data-meta="listeners"]').textContent = `coders listening right now: ${np.listeners.current}`); //bring in the animation container.className = "animation"; diff --git a/player.js b/player.js index 4ee9d66..8acc859 100644 --- a/player.js +++ b/player.js @@ -1,3 +1,13 @@ +const _events = Symbol('events'), + _url = Symbol('url'), + _player = Symbol('player'), + _alternateMounts = Symbol('alternateMounts'), + _currentSong = Symbol('currentSong'), + _songStartedAt = Symbol('songStartedAt') + _songDuration = Symbol('songDuration'), + _progressInterval = Symbol('progressInterval'), + _listeners = Symbol('listeners'); + class CodeRadio { constructor() { /*** @@ -39,19 +49,21 @@ class CodeRadio { * to it being a single audio element, there should be * no memory leaks of extra floating audio elements. */ - this._url = ""; - this._player = new Audio(); - this._player.volume = this.audioConfig.maxVolume; - this._player.crossOrigin = "anonymous"; + this[_url] = ""; + this[_alternateMounts] = []; + this[_player] = new Audio(); + this[_player].volume = this.audioConfig.maxVolume; + this[_player].crossOrigin = "anonymous"; // Note: the crossOrigin is needed to fix a CORS JavaScript requirement /*** * There are a few *private* variables used */ - this._currentSong = {}; - this._songStartedAt = 0; - this._songDuration = 0; - this._progressInterval = false; + this[_currentSong] = {}; + this[_songStartedAt] = 0; + this[_songDuration] = 0; + this[_progressInterval] = false; + this[_listeners] = 0; this.getNowPlaying(); @@ -74,15 +86,23 @@ class CodeRadio { * resets the URL. */ set url(url = false) { - if (url && this._url === "") { - this._url = url; - this._player.src = url; - this._player.play(); + if (url && this[_url] === "") { + this[_url] = url; + this[_player].src = url; + this[_player].play(); } } get url() { - return this._url; + return this[_url]; + } + + set mounts(mounts = []) { + this[_alternateMounts] = mounts; + } + + get mounts() { + return this[_alternateMounts]; } /*** @@ -91,12 +111,11 @@ class CodeRadio { * the metadata is updated. */ set currentSong(songData = {}) { - this._currentSong = songData; - this.renderMetadata(); + throw new Error('You cannot set the value of a readonly attribute'); } get currentSong() { - return this._currentSong; + return this[_currentSong]; } /*** @@ -104,21 +123,28 @@ class CodeRadio { * duration for the max of the meter and set the played at to 0 */ set played_at(t = 0) { - this._songStartedAt = t * 1000; // Time comes in a seconds so we multiply by 1000 to set millis + this[_songStartedAt] = t * 1000; // Time comes in a seconds so we multiply by 1000 to set millis this.meta.duration.value = 0; } get played_at() { - return this._songStartedAt; + return this[_songStartedAt]; } set duration(d = 0) { - this._songDuration = d; - this.meta.duration.max = this._songDuration; + throw new Error('You cannot set the value of a readonly attribute'); } get duration() { - return this._songDuration; + return this[_songDuration]; + } + + set listeners(v = 0) { + throw new Error('You cannot set the value of a readonly attribute'); + } + + get listeners() { + return this[_listeners]; } getNowPlaying() { @@ -129,26 +155,25 @@ class CodeRadio { np = np[0]; // There is only ever 1 song "Now Playing" so let's simplify the response // We look through the available mounts to find the default mount (or just the listen_url) - if (this.url === "") + if (this.url === "") { this.url = np.station.mounts.find(mount => !!mount.is_default).url; + this.mounts = np.station.mounts; + } // We only need to update th metadata if the song has been changed - if ( - !this.currentSong.id || - np.now_playing.song.id !== this.currentSong.id - ) { - this.currentSong = np.now_playing.song; - this.played_at = np.now_playing.played_at; - this.duration = np.now_playing.duration; - this.meta.listeners.textContent = `coders listening right now: ${ - np.listeners.current - }`; - if (!this._progressInterval) { - this._progressInterval = setInterval( - () => this.updateProgress(), - 100 - ); + if (np.now_playing.song.id !== this.currentSong.id) { + this[_currentSong] = np.now_playing.song; + this[_songStartedAt] = t * 1000; = np.now_playing.played_at; + this[_songDuration] = np.now_playing.duration; + if (this[_listeners] !== np.listeners.current) { + this[_listeners] = np.listeners.current; + this.emit('listeners', this[_listeners]); } + + this.meta.duration.max = this[_songDuration]; + this.meta.duration.value = 0; + this.emit('newSong', this[_currentSong]); + if (!this[_progressInterval]) this[_progressInterval] = setInterval(() => this.updateProgress(), 100); } // Since the server doesn't have a socket connection (yet), we need to long poll it for the current song @@ -169,7 +194,7 @@ class CodeRadio { // In order to get around some mobile browser limitations, we can only generate a lot // of the audio context stuff AFTER the audio has been triggered. We can't see it until // then anyway so it makes no difference to desktop. - this._player.addEventListener("play", () => { + this[_player].addEventListener("play", () => { if (!this.eq.context) { this.initiateEQ(); this.createVisualizer(); @@ -186,14 +211,10 @@ class CodeRadio { this.togglePlay(); break; case "ArrowUp": - this.setTargetVolume( - Math.min(this.audioConfig.maxVolume + this.audioConfig.volumeSteps, 1) - ); + this.setTargetVolume(Math.min(this.audioConfig.maxVolume + this.audioConfig.volumeSteps, 1)); break; case "ArrowDown": - this.setTargetVolume( - Math.max(this.audioConfig.maxVolume - this.audioConfig.volumeSteps, 0) - ); + this.setTargetVolume(Math.max(this.audioConfig.maxVolume - this.audioConfig.volumeSteps, 0)); break; } } @@ -203,7 +224,7 @@ class CodeRadio { this.eq.context = new AudioContext(); // Apply the audio element as the source where to pull all the data from - this.eq.src = this.eq.context.createMediaElementSource(this._player); + this.eq.src = this.eq.context.createMediaElementSource(this[_player]); // Use some amazing trickery that allows javascript to analyse the current state this.eq.analyser = this.eq.context.createAnalyser(); @@ -261,12 +282,7 @@ class CodeRadio { let y, x = 0; // Intial bar x coordinate - this.visualizer.ctx.clearRect( - 0, - 0, - this.visualizer.width, - this.visualizer.height - ); // Clear the complete canvas + this.visualizer.ctx.clearRect(0, 0, this.visualizer.width, this.visualizer.height); // Clear the complete canvas this.visualizer.ctx.fillStyle = this.config.translucent; // Set the primary colour of the brand (probably moving to a higher object level variable soon) this.visualizer.ctx.beginPath(); // Start creating a canvas polygon this.visualizer.ctx.moveTo(x, 0); // Start at the bottom left @@ -281,16 +297,18 @@ class CodeRadio { } play() { - if (this._player.paused) { - this._player.volume = 0; - this._player.play(); + if (this[_player].paused) { + this[_player].volume = 0; + this[_player].play(); + this.emit('play'); this.fadeUp(); return this; } } pause() { - this._player.pause(); + this[_player].pause(); + this.emit('pause'); return this; } @@ -301,31 +319,24 @@ class CodeRadio { */ togglePlay() { // If there already is a source, confirm it’s playing or not - if (!!this._player.src) { + if (!!this[_player].src) { // If the player is paused, set the volume to 0 and fade up - if (this._player.paused) { - this._player.volume = 0; - this._player.play(); - this.fadeUp(); - + if (this[_player].paused) this.play(); // if it is already playing, fade the music out (resulting in a pause) - } else this.fade(); + else this.fade(); } return this; } setTargetVolume(v) { - this.audioConfig.maxVolume = parseFloat( - Math.max(0, Math.min(1, v).toFixed(1)) - ); - this._player.volume = this.audioConfig.maxVolume; + this.audioConfig.maxVolume = parseFloat(Math.max(0, Math.min(1, v).toFixed(1))); + this[_player].volume = this.audioConfig.maxVolume; } // Simple fade command to initiate the playing and pausing in a more fluid method fade(direction = "down") { - this.audioConfig.targetVolume = - direction.toLowerCase() === "up" ? this.audioConfig.maxVolume : 0; + this.audioConfig.targetVolume = direction.toLowerCase() === "up" ? this.audioConfig.maxVolume : 0; this.updateVolume(); return this; } @@ -341,50 +352,75 @@ class CodeRadio { // In order to have nice fading, this method adjusts the volume dynamically over time. updateVolume() { // In order to fix floating math issues, we set the toFixed in order to avoid 0.999999999999 increments - let currentVolume = parseFloat(this._player.volume.toFixed(1)); + let currentVolume = parseFloat(this[_player].volume.toFixed(1)); // If the volume is correctly set to the target, no need to change it if (currentVolume === this.audioConfig.targetVolume) { // If the audio is set to 0 and it’s been met, pause the audio - if (this.audioConfig.targetVolume === 0) this._player.pause(); + if (this.audioConfig.targetVolume === 0) this[_player].pause(); // Unmet audio volume settings require it to be changed } else { // We capture the value of the next increment by either the configuration or the difference between the current and target if it's smaller than the increment - let volumeNextIncrement = Math.min( - this.audioConfig.volumeSteps, - Math.abs(this.audioConfig.targetVolume - this._player.volume) - ); + let volumeNextIncrement = Math.min(this.audioConfig.volumeSteps, Math.abs(this.audioConfig.targetVolume - this[_player].volume)); // Adjust the audio based on if the target is higher or lower than the current - this._player.volume += - this.audioConfig.targetVolume > this._player.volume + this[_player].volume += + this.audioConfig.targetVolume > this[_player].volume ? volumeNextIncrement : -volumeNextIncrement; + + this.emit('volumeChange', this[_player].volume); // The speed at which the audio lowers is also controlled. - setTimeout( - () => this.updateVolume(), - this.audioConfig.volumeTransitionSpeed - ); + setTimeout(() => this.updateVolume(), this.audioConfig.volumeTransitionSpeed); } } renderMetadata() { - if (!!this._currentSong.art) { - this.meta.picture.style.backgroundImage = `url(${this._currentSong.art})`; + if (!!this[_currentSong].art) { + this.meta.picture.style.backgroundImage = `url(${this[_currentSong].art})`; this.meta.container.classList.add("thumb"); } else { this.meta.container.classList.remove("thumb"); this.meta.picture.style.backgroundImage = ""; } - this.meta.title.textContent = this._currentSong.title; - this.meta.artist.textContent = this._currentSong.artist; - this.meta.album.textContent = this._currentSong.album; + this.meta.title.textContent = this[_currentSong].title; + this.meta.artist.textContent = this[_currentSong].artist; + this.meta.album.textContent = this[_currentSong].album; } updateProgress() { - this.meta.duration.value = - (new Date().valueOf() - this._songStartedAt) / 1000; + this.meta.duration.value = (new Date().valueOf() - this[_songStartedAt]) / 1000; + } + + on(trigger, fn, once = false) { + if (typeof fn != 'function') throw new Error(`Invalid Listener: ${trigger}. Must be a function`); + if (!this[_events]) this[_events] = {}; + if (!this[_events][trigger]) this[_events][trigger] = new Array(); + this[_events][trigger].push({ + listener: fn, + once: !!once + }); + } + + once(trigger, fn) { + this.on(trigger, fn, true); + } + + off(trigger, fn) { + if (!this[_events] || !this[_events][trigger]) return; + this[_events][trigger] = this[_events][trigger].map(evt => (evt !== fn)); + } + + emit(trigger, data) { + return new Promise((resolve, reject) => { + if (!this[_events] || !this[_events][trigger]) return; + this[_events][trigger].forEach((evt, i) => { + evt.listener(data); + if (evt.once) this[_events][trigger].splice(i, 1); + }); + resolve(); + }); } } From 3e7933cafe6cfaaa412e16e7986813010e9f27e3 Mon Sep 17 00:00:00 2001 From: Matthew Potter Date: Wed, 24 Jul 2019 13:00:21 -0400 Subject: [PATCH 2/6] Major overhaul of extracting visualizer and adding bitrate options The bitrate should now automatically select the 64kbps if the connection speed is 3G or slower. Users can also choose the speed manually via a select box that will now be generated on desktop. --- codeRadio-desktop.js | 103 +++++++++++++++++++++ codeRadio-mobile.js | 37 ++++++++ codeRadio.js | 32 +++++++ index.html | 149 +----------------------------- interface.css | 21 ++--- player.js | 209 ++++++++++++------------------------------- visualizer.js | 102 +++++++++++++++++++++ 7 files changed, 343 insertions(+), 310 deletions(-) create mode 100644 codeRadio-desktop.js create mode 100644 codeRadio-mobile.js create mode 100644 codeRadio.js create mode 100644 visualizer.js diff --git a/codeRadio-desktop.js b/codeRadio-desktop.js new file mode 100644 index 0000000..c99e527 --- /dev/null +++ b/codeRadio-desktop.js @@ -0,0 +1,103 @@ +import { CodeRadio } from './player.js'; +import { Visualizer } from './visualizer.js'; + +if (!window.mobilecheck()) { + //set up the footer + const footer = document.createElement('footer'), + visContainer = document.createElement('div'), + details = document.createElement('details'), + connectionSpeed = document.createElement('select'), + speedOptions = []; + connectionSpeed.addEventListener('change', evt => { + fCC_Player.mount = connectionSpeed.value; + }); + visContainer.id = 'visualizer'; + footer.innerHTML = ` +
+
+
+ +
+
+
+
+
+
+
+ Play Pause Button +
+
+ +
+ `; + //setup the info box for keyboard shortcuts + + details.innerHTML = ` + Keyboard Controls +
+
Play/Pause:
Spacebar or "k"
+
Volume:
Up Arrow / Down Arrow
+
`; + + document.body.appendChild(footer); + document.querySelector('main').appendChild(visContainer); + document.querySelector('main').appendChild(details); + //start the show + window.fCC_Player = new CodeRadio(); + window.fCC_Visualizer = new Visualizer(fCC_Player, visContainer); + var playButton = document.getElementById("playButton"), + slider = document.getElementById("slider"), + playContainer = document.getElementById("playContainer"), + progressInterval = false, + meta = { + container: document.getElementById("nowPlaying"), + picture: document.querySelector('[data-meta="picture"]'), + title: document.querySelector('[data-meta="title"]'), + artist: document.querySelector('[data-meta="artist"]'), + album: document.querySelector('[data-meta="album"]'), + duration: document.querySelector('[data-meta="duration"]'), + listeners: document.querySelector('[data-meta="listeners"]') + }; + + slider.addEventListener('input', () => fCC_Player.setTargetVolume(slider.value / 10)); + playContainer.addEventListener('click', () => fCC_Player.togglePlay()); + + fCC_Player.on('play', () => playButton.src = "https://cdn-media-1.freecodecamp.org/code-radio/pause.svg"); + fCC_Player.on('pause', () => playButton.src = "https://cdn-media-1.freecodecamp.org/code-radio/play.svg"); + fCC_Player.on('volumeChange', (volume) => slider.value = Math.round(volume * 10)); + fCC_Player.on('listeners', (count) => document.querySelector('[data-meta="listeners"]').textContent = `Listeners: ${count}`); + fCC_Player.on('newSong', (songData) => { + if (speedOptions.length === 0) { + fCC_Player.mounts.forEach(mount => { + let option = document.createElement('option'); + option.value = mount.url; + option.textContent = mount.name; + if (fCC_Player.url === mount.url) option.selected = true; + connectionSpeed.appendChild(option); + speedOptions.push(option); + }); + document.querySelector('.site-nav-left').appendChild(connectionSpeed); + } + meta.duration.value = 0; + meta.duration.max = fCC_Player.duration; + if (!!songData.art) { + meta.picture.style.backgroundImage = `url(${songData.art})`; + meta.container.classList.add("thumb"); + } else { + meta.container.classList.remove("thumb"); + meta.picture.style.backgroundImage = ""; + } + meta.title.textContent = songData.title; + meta.artist.textContent = songData.artist; + meta.album.textContent = songData.album; + if (!progressInterval) progressInterval = setInterval(() => { + meta.duration.value = (new Date().valueOf() - fCC_Player.playedAt) / 1000; + }, 100); + }); + + container.classList.add("animation"); +} \ No newline at end of file diff --git a/codeRadio-mobile.js b/codeRadio-mobile.js new file mode 100644 index 0000000..64908f1 --- /dev/null +++ b/codeRadio-mobile.js @@ -0,0 +1,37 @@ +if (!!window.mobilecheck()) { + const container = document.getElementById("container"); + container.innerHTML = ` +
+ +
+

+
`; + + //call api and update the listener numbber + var xhr = new XMLHttpRequest(); + + function reqListener() { + var listenerNum = JSON.parse(this.response)[0].listeners.total; + listenerMounter(listenerNum); + setTimeout(callMaker, 20000); + } + + function listenerMounter(number) { + var d = document.getElementById("listeners-num"); + d.textContent = number + " coders listening right now"; + } + + function callMaker() { + xhr.addEventListener("load", reqListener); + xhr.open("GET", "/app/api/nowplaying"); + + xhr.send(); + } + + callMaker(); +} \ No newline at end of file diff --git a/codeRadio.js b/codeRadio.js new file mode 100644 index 0000000..ff6666d --- /dev/null +++ b/codeRadio.js @@ -0,0 +1,32 @@ +window.mobilecheck = function () { + var check = false; + (function (a) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( + a + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4) + ) + ) + check = true; + })(navigator.userAgent || navigator.vendor || window.opera); + return check; +}; + +function showMenu() { + toggleClass("toggle-button-nav", "reverse-toggle-color"); + toggleClass("nav", "show-main-nav-items"); + toggleClass("site-nav", "expand-nav"); +} + +function toggleClass(id, className) { + var d = document.getElementById(id); + d.classList.toggle(className); +} + +if (window && window.addEventListener) { + window.addEventListener("load", function () { + document.getElementById("toggle-button-nav").onclick = showMenu; + }); +} \ No newline at end of file diff --git a/index.html b/index.html index 794397c..c94de45 100644 --- a/index.html +++ b/index.html @@ -57,152 +57,9 @@

Welcome to Code Radio.

24/7 music designed for coding.

-
-
- - - - - - - - - - - + + + diff --git a/interface.css b/interface.css index 939e353..95460df 100644 --- a/interface.css +++ b/interface.css @@ -14,7 +14,7 @@ body { overflow: hidden; } -footer.footer { +footer { background-color: #0a0a23; height: 70px; width: 100%; @@ -50,6 +50,7 @@ footer.footer { top: 18px; font-size: 20px; } + #metaDisplay { width: calc(100% - 210px); } @@ -165,10 +166,9 @@ footer.footer { position: absolute; } -.details { - display: none; +details { position: absolute; - bottom: 50px; + bottom: 100px; right: 50px; background-color: rgba(10, 10, 35, 0.6); color: #fff; @@ -177,8 +177,8 @@ footer.footer { transition: opacity 0.25s ease-out; } -.details:hover, -.details[open] { +details:hover, +details[open] { opacity: 1; } @@ -310,6 +310,11 @@ dd { height: auto; } +nav select { + position: relative; + top: 15px; +} + @media (max-width: 500px) { .site-header { padding-right: 0; @@ -456,10 +461,6 @@ body { flex-direction: column; height: 200px; } - #container.animation, - #visualizer { - display: none; - } footer.footer { display: none; diff --git a/player.js b/player.js index 8acc859..753972c 100644 --- a/player.js +++ b/player.js @@ -3,23 +3,25 @@ const _events = Symbol('events'), _player = Symbol('player'), _alternateMounts = Symbol('alternateMounts'), _currentSong = Symbol('currentSong'), - _songStartedAt = Symbol('songStartedAt') + _songStartedAt = Symbol('songStartedAt'), _songDuration = Symbol('songDuration'), _progressInterval = Symbol('progressInterval'), - _listeners = Symbol('listeners'); + _listeners = Symbol('listeners'), + _audioConfig = Symbol('audioConfig'), + _fastConnection = Symbol('fastConnection'); + +export class CodeRadio { -class CodeRadio { constructor() { /*** * General configuration options */ this.config = { - baseColour: "rgb(10, 10, 35)", - translucent: "rgba(10, 10, 35, 0.6)", - multiplier: 0.7529, metadataTimer: 2000 }; + this[_fastConnection] = (!!navigator.connection) ? (navigator.connection.downlink > 1.5) : false; + /*** * The equalizer data is held as a seperate data set * to allow for easy implementation of visualizers. @@ -37,7 +39,7 @@ class CodeRadio { // Some basic configuration for nicer audio transitions // (Used in earlier projects and just maintained) - this.audioConfig = { + this[_audioConfig] = { targetVolume: 0, maxVolume: 0.5, volumeSteps: 0.1, @@ -52,7 +54,7 @@ class CodeRadio { this[_url] = ""; this[_alternateMounts] = []; this[_player] = new Audio(); - this[_player].volume = this.audioConfig.maxVolume; + this[_player].volume = this[_audioConfig].maxVolume; this[_player].crossOrigin = "anonymous"; // Note: the crossOrigin is needed to fix a CORS JavaScript requirement @@ -67,17 +69,7 @@ class CodeRadio { this.getNowPlaying(); - this.meta = { - container: document.getElementById("nowPlaying"), - picture: document.querySelector('[data-meta="picture"]'), - title: document.querySelector('[data-meta="title"]'), - artist: document.querySelector('[data-meta="artist"]'), - album: document.querySelector('[data-meta="album"]'), - duration: document.querySelector('[data-meta="duration"]'), - listeners: document.querySelector('[data-meta="listeners"]') - }; - - this.setupEventListeners(); + document.addEventListener("keydown", evt => this.keyboardControl(evt)); } /*** @@ -86,7 +78,7 @@ class CodeRadio { * resets the URL. */ set url(url = false) { - if (url && this[_url] === "") { + if (url) { this[_url] = url; this[_player].src = url; this[_player].play(); @@ -97,8 +89,20 @@ class CodeRadio { return this[_url]; } - set mounts(mounts = []) { - this[_alternateMounts] = mounts; + get player() { + return this[_player]; + } + + set player(v) { + throw new Error('You cannot set the value of a readonly attribute'); + } + + set mounts(mounts) { + throw new Error('You cannot set the value of a readonly attribute'); + } + + set mount(mount) { + this.url = mount; } get mounts() { @@ -122,12 +126,11 @@ class CodeRadio { * In order to get the constant durations, we simply take the * duration for the max of the meter and set the played at to 0 */ - set played_at(t = 0) { - this[_songStartedAt] = t * 1000; // Time comes in a seconds so we multiply by 1000 to set millis - this.meta.duration.value = 0; + set playedAt(t = 0) { + throw new Error('You cannot set the value of a readonly attribute'); } - get played_at() { + get playedAt() { return this[_songStartedAt]; } @@ -147,6 +150,19 @@ class CodeRadio { return this[_listeners]; } + get playing() { + return this[_player].playing; + } + + setMountToConnection() { + this[_fastConnection] = (!!navigator.connection) ? (navigator.connection.downlink > 1.5) : false; + if (this[_fastConnection]) { + this.url = this.mounts.find(mount => !!mount.is_default).url; + } else { + this.url = this.mounts.find(mount => mount.bitrate < this.mounts.find(m => !!m.is_default).bitrate).url || this.mounts.find(mount => !!mount.is_default).url; + } + } + getNowPlaying() { // To prevent browser based caching, we add the date to the request, it won't impact the response fetch(`/app/api/nowplaying?t=${new Date().valueOf()}`) @@ -156,24 +172,20 @@ class CodeRadio { // We look through the available mounts to find the default mount (or just the listen_url) if (this.url === "") { - this.url = np.station.mounts.find(mount => !!mount.is_default).url; - this.mounts = np.station.mounts; + this[_alternateMounts] = np.station.mounts; + this.setMountToConnection(); } // We only need to update th metadata if the song has been changed if (np.now_playing.song.id !== this.currentSong.id) { this[_currentSong] = np.now_playing.song; - this[_songStartedAt] = t * 1000; = np.now_playing.played_at; + this[_songStartedAt] = np.now_playing.played_at * 1000; this[_songDuration] = np.now_playing.duration; if (this[_listeners] !== np.listeners.current) { this[_listeners] = np.listeners.current; this.emit('listeners', this[_listeners]); } - - this.meta.duration.max = this[_songDuration]; - this.meta.duration.value = 0; this.emit('newSong', this[_currentSong]); - if (!this[_progressInterval]) this[_progressInterval] = setInterval(() => this.updateProgress(), 100); } // Since the server doesn't have a socket connection (yet), we need to long poll it for the current song @@ -185,23 +197,6 @@ class CodeRadio { }); } - /*** - * Yay, let's get some keyboard shortcuts in this tool - */ - setupEventListeners() { - document.addEventListener("keydown", evt => this.keyboardControl(evt)); - - // In order to get around some mobile browser limitations, we can only generate a lot - // of the audio context stuff AFTER the audio has been triggered. We can't see it until - // then anyway so it makes no difference to desktop. - this[_player].addEventListener("play", () => { - if (!this.eq.context) { - this.initiateEQ(); - this.createVisualizer(); - } - }); - } - keyboardControl(evt = {}) { // Quick note: if you're wanting to do similar in your projects, keyCode use to be the // standard however it is being depricated for the key attribute @@ -211,91 +206,14 @@ class CodeRadio { this.togglePlay(); break; case "ArrowUp": - this.setTargetVolume(Math.min(this.audioConfig.maxVolume + this.audioConfig.volumeSteps, 1)); + this.setTargetVolume(Math.min(this[_audioConfig].maxVolume + this[_audioConfig].volumeSteps, 1)); break; case "ArrowDown": - this.setTargetVolume(Math.max(this.audioConfig.maxVolume - this.audioConfig.volumeSteps, 0)); + this.setTargetVolume(Math.max(this[_audioConfig].maxVolume - this[_audioConfig].volumeSteps, 0)); break; } } - initiateEQ() { - // Create a new Audio Context element to read the samples from - this.eq.context = new AudioContext(); - - // Apply the audio element as the source where to pull all the data from - this.eq.src = this.eq.context.createMediaElementSource(this[_player]); - - // Use some amazing trickery that allows javascript to analyse the current state - this.eq.analyser = this.eq.context.createAnalyser(); - this.eq.src.connect(this.eq.analyser); - this.eq.analyser.connect(this.eq.context.destination); - this.eq.analyser.fftSize = 256; - - // Create a buffer array for the number of frequencies available (minus the high pitch useless ones that never really do anything anyway) - this.eq.bands = new Uint8Array(this.eq.analyser.frequencyBinCount - 32); - this.updateEQBands(); - } - - /*** - * The equalizer bands available need to be updated - * constantly in order to ensure that the value for any - * visualizer is up to date. - */ - updateEQBands() { - // Populate the buffer with the audio source’s current data - this.eq.analyser.getByteFrequencyData(this.eq.bands); - - // Can’t stop, won’t stop - requestAnimationFrame(() => this.updateEQBands()); - } - - /*** - * When starting the page, the visualizer dom is needed to be - * created. - */ - createVisualizer() { - let container = document.createElement("canvas"); - document.getElementById("visualizer").appendChild(container); - container.width = container.parentNode.offsetWidth; - container.height = container.parentNode.offsetHeight; - - this.visualizer = { - ctx: container.getContext("2d"), - height: container.height, - width: container.width, - barWidth: container.width / this.eq.bands.length - }; - - this.drawVisualizer(); - } - - /*** - * As a base visualizer, the equalizer bands are drawn using - * canvas in the window directly above the song into. - */ - drawVisualizer() { - if (this.eq.bands.reduce((a, b) => a + b, 0) !== 0) - requestAnimationFrame(() => this.drawVisualizer()); - // Because timeupdate events are not triggered at browser speed, we use requestanimationframe for higher framerates - else setTimeout(() => this.drawVisualizer(), 250); // If there is no music or audio in the song, then reduce the FPS - - let y, - x = 0; // Intial bar x coordinate - this.visualizer.ctx.clearRect(0, 0, this.visualizer.width, this.visualizer.height); // Clear the complete canvas - this.visualizer.ctx.fillStyle = this.config.translucent; // Set the primary colour of the brand (probably moving to a higher object level variable soon) - this.visualizer.ctx.beginPath(); // Start creating a canvas polygon - this.visualizer.ctx.moveTo(x, 0); // Start at the bottom left - this.eq.bands.forEach(band => { - y = this.config.multiplier * band; // Get the overall hight associated to the current band and convert that into a Y position on the canvas - this.visualizer.ctx.lineTo(x, y); // Draw a line from the current position to the wherever the Y position is - this.visualizer.ctx.lineTo(x + this.visualizer.barWidth, y); // Continue that line to meet the width of the bars (canvas width ÷ bar count) - x += this.visualizer.barWidth; // Add pixels to the x for the next bar - }); - this.visualizer.ctx.lineTo(x, 0); // Bring the line back down to the bottom of the canvas - this.visualizer.ctx.fill(); // Fill it - } - play() { if (this[_player].paused) { this[_player].volume = 0; @@ -330,13 +248,13 @@ class CodeRadio { } setTargetVolume(v) { - this.audioConfig.maxVolume = parseFloat(Math.max(0, Math.min(1, v).toFixed(1))); - this[_player].volume = this.audioConfig.maxVolume; + this[_audioConfig].maxVolume = parseFloat(Math.max(0, Math.min(1, v).toFixed(1))); + this[_player].volume = this[_audioConfig].maxVolume; } // Simple fade command to initiate the playing and pausing in a more fluid method fade(direction = "down") { - this.audioConfig.targetVolume = direction.toLowerCase() === "up" ? this.audioConfig.maxVolume : 0; + this[_audioConfig].targetVolume = direction.toLowerCase() === "up" ? this[_audioConfig].maxVolume : 0; this.updateVolume(); return this; } @@ -355,44 +273,27 @@ class CodeRadio { let currentVolume = parseFloat(this[_player].volume.toFixed(1)); // If the volume is correctly set to the target, no need to change it - if (currentVolume === this.audioConfig.targetVolume) { + if (currentVolume === this[_audioConfig].targetVolume) { // If the audio is set to 0 and it’s been met, pause the audio - if (this.audioConfig.targetVolume === 0) this[_player].pause(); + if (this[_audioConfig].targetVolume === 0) this.pause(); // Unmet audio volume settings require it to be changed } else { // We capture the value of the next increment by either the configuration or the difference between the current and target if it's smaller than the increment - let volumeNextIncrement = Math.min(this.audioConfig.volumeSteps, Math.abs(this.audioConfig.targetVolume - this[_player].volume)); + let volumeNextIncrement = Math.min(this[_audioConfig].volumeSteps, Math.abs(this[_audioConfig].targetVolume - this[_player].volume)); // Adjust the audio based on if the target is higher or lower than the current this[_player].volume += - this.audioConfig.targetVolume > this[_player].volume + this[_audioConfig].targetVolume > this[_player].volume ? volumeNextIncrement : -volumeNextIncrement; this.emit('volumeChange', this[_player].volume); // The speed at which the audio lowers is also controlled. - setTimeout(() => this.updateVolume(), this.audioConfig.volumeTransitionSpeed); + setTimeout(() => this.updateVolume(), this[_audioConfig].volumeTransitionSpeed); } } - - renderMetadata() { - if (!!this[_currentSong].art) { - this.meta.picture.style.backgroundImage = `url(${this[_currentSong].art})`; - this.meta.container.classList.add("thumb"); - } else { - this.meta.container.classList.remove("thumb"); - this.meta.picture.style.backgroundImage = ""; - } - this.meta.title.textContent = this[_currentSong].title; - this.meta.artist.textContent = this[_currentSong].artist; - this.meta.album.textContent = this[_currentSong].album; - } - - updateProgress() { - this.meta.duration.value = (new Date().valueOf() - this[_songStartedAt]) / 1000; - } on(trigger, fn, once = false) { if (typeof fn != 'function') throw new Error(`Invalid Listener: ${trigger}. Must be a function`); diff --git a/visualizer.js b/visualizer.js new file mode 100644 index 0000000..8f16dd0 --- /dev/null +++ b/visualizer.js @@ -0,0 +1,102 @@ +const _player = Symbol('player') + +export class Visualizer { + + constructor(fCC_Player, container = false) { + this[_player] = fCC_Player.player; + this.eq = {}; + this.config = { + baseColour: "rgb(10, 10, 35)", + translucent: "rgba(10, 10, 35, 0.6)", + multiplier: 0.7529, + }; + this.container = container || document.getElementById("visualizer"); + + // In order to get around some mobile browser limitations, we can only generate a lot + // of the audio context stuff AFTER the audio has been triggered. We can't see it until + // then anyway so it makes no difference to desktop. + fCC_Player.player.addEventListener('play', () => { + if (!this.eq.context) { + this.initiateEQ(); + this.createVisualizer(); + } + }) + + } + + initiateEQ() { + // Create a new Audio Context element to read the samples from + this.eq.context = new AudioContext(); + // Apply the audio element as the source where to pull all the data from + this.eq.src = this.eq.context.createMediaElementSource(this[_player]); + + // Use some amazing trickery that allows javascript to analyse the current state + this.eq.analyser = this.eq.context.createAnalyser(); + this.eq.src.connect(this.eq.analyser); + this.eq.analyser.connect(this.eq.context.destination); + this.eq.analyser.fftSize = 256; + + // Create a buffer array for the number of frequencies available (minus the high pitch useless ones that never really do anything anyway) + this.eq.bands = new Uint8Array(this.eq.analyser.frequencyBinCount - 32); + this.updateEQBands(); + } + + /*** + * The equalizer bands available need to be updated + * constantly in order to ensure that the value for any + * visualizer is up to date. + */ + updateEQBands() { + // Populate the buffer with the audio source’s current data + this.eq.analyser.getByteFrequencyData(this.eq.bands); + + // Can’t stop, won’t stop + requestAnimationFrame(() => this.updateEQBands()); + } + + /*** + * When starting the page, the visualizer dom is needed to be + * created. + */ + createVisualizer() { + let container = document.createElement("canvas"); + this.container.appendChild(container); + container.width = container.parentNode.offsetWidth; + container.height = container.parentNode.offsetHeight; + + this.visualizer = { + ctx: container.getContext("2d"), + height: container.height, + width: container.width, + barWidth: container.width / this.eq.bands.length + }; + + this.drawVisualizer(); + } + + /*** + * As a base visualizer, the equalizer bands are drawn using + * canvas in the window directly above the song into. + */ + drawVisualizer() { + if (this.eq.bands.reduce((a, b) => a + b, 0) !== 0) requestAnimationFrame(() => this.drawVisualizer()); + // Because timeupdate events are not triggered at browser speed, we use requestanimationframe for higher framerates + else setTimeout(() => this.drawVisualizer(), 250); // If there is no music or audio in the song, then reduce the FPS + + let y, + x = 0; // Intial bar x coordinate + this.visualizer.ctx.clearRect(0, 0, this.visualizer.width, this.visualizer.height); // Clear the complete canvas + this.visualizer.ctx.fillStyle = this.config.translucent; // Set the primary colour of the brand (probably moving to a higher object level variable soon) + this.visualizer.ctx.beginPath(); // Start creating a canvas polygon + this.visualizer.ctx.moveTo(x, 0); // Start at the bottom left + this.eq.bands.forEach(band => { + y = this.config.multiplier * band; // Get the overall hight associated to the current band and convert that into a Y position on the canvas + this.visualizer.ctx.lineTo(x, y); // Draw a line from the current position to the wherever the Y position is + this.visualizer.ctx.lineTo(x + this.visualizer.barWidth, y); // Continue that line to meet the width of the bars (canvas width ÷ bar count) + x += this.visualizer.barWidth; // Add pixels to the x for the next bar + }); + this.visualizer.ctx.lineTo(x, 0); // Bring the line back down to the bottom of the canvas + this.visualizer.ctx.fill(); // Fill it + } + +} \ No newline at end of file From 62962c6891d817648cd8f339cd6f578fae70d172 Mon Sep 17 00:00:00 2001 From: Matthew Potter Date: Wed, 24 Jul 2019 13:57:00 -0400 Subject: [PATCH 3/6] Updated to save some bandwidth for users on slower connections Unless the browser supports the navigator.connection and the connection speed is above 3G, the gif animation and the cover art is not shown. --- codeRadio-desktop.js | 8 +++-- img/cover_placeholder.gif | Bin 0 -> 1296 bytes index.html | 66 ++++++++++++++++++++++++++++++++++---- interface.css | 2 +- 4 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 img/cover_placeholder.gif diff --git a/codeRadio-desktop.js b/codeRadio-desktop.js index c99e527..bbcd482 100644 --- a/codeRadio-desktop.js +++ b/codeRadio-desktop.js @@ -8,6 +8,7 @@ if (!window.mobilecheck()) { details = document.createElement('details'), connectionSpeed = document.createElement('select'), speedOptions = []; + connectionSpeed.addEventListener('change', evt => { fCC_Player.mount = connectionSpeed.value; }); @@ -84,12 +85,12 @@ if (!window.mobilecheck()) { } meta.duration.value = 0; meta.duration.max = fCC_Player.duration; - if (!!songData.art) { + if (!!songData.art && (!!navigator.connection && navigator.connection.downlink > 1.5)) { meta.picture.style.backgroundImage = `url(${songData.art})`; meta.container.classList.add("thumb"); } else { meta.container.classList.remove("thumb"); - meta.picture.style.backgroundImage = ""; + meta.picture.style.backgroundImage = "url(./img/cover_placeholder.gif)"; } meta.title.textContent = songData.title; meta.artist.textContent = songData.artist; @@ -100,4 +101,7 @@ if (!window.mobilecheck()) { }); container.classList.add("animation"); + if (!!navigator.connection && navigator.connection.downlink > 1.5) { + container.classList.add('saron'); + } } \ No newline at end of file diff --git a/img/cover_placeholder.gif b/img/cover_placeholder.gif new file mode 100644 index 0000000000000000000000000000000000000000..e22ad7ba51b071dbcc475fa799897fdab085cb79 GIT binary patch literal 1296 zcmV+r1@HPtNk%w1VMYK(0K@2zI;^N}T$;rID zy!rY0<>lqFva-IuzW4X{|NsBz=jZI~?CtIC?(XjB=;-O`>E`C<>gww2>+ALP_3`oX z@bK{O@9(?2ySlo%A^8LV00000EC2ui07d{t06+!*AS8}tX`X1RuBUcIa4gSNU0b+? z?R<~rIa_5$lwBw$7xAXUWHOSdR640zuh{AIxx>|n$K)$=+d-$;31!V*cQ%2*1R*ed z000JOo!w@B7GGf{E@e+>S8Ws>i~$D<6dN3qlpO&C02dpI6jxe*UV|is6*x{>iHaN> z8jlhj9k31!3bhI!dWwBfe?K*%BT6u3PG?kA91H^k2&@h;`@I&qM*$^5G7y0`8{Qds_00Z)4FhNW^8>Y;B6Bq7DH80nS z)Z2#8%(Q4#p0&X^?P0`;7rHH!fQ6H#0|h+)sPyomfJ+An3>3)4W*4t=6+jv?Mlux} zMkf_akVI*cB}|$+=(6&u97r;ybnxtHB|rjND+?T;0E@$f1T7ULh!9{Y(=A;Dr5m+w zSX71JFv>mq$)mg&Pv=2plF*yOzrA<_F{ig4i^GT$D_+dFvEprdX~df>B(lV{!P=e^ zlEW|H&kh9-zRNBzx6Z{Fs_tl)G+xZwG>e=XWv=ZitkXtBXAo+_snIl=+^{KHU7RqU z5(Dn@XN5X<02KI8YbCBdd_4K_HAH7Ai-&sZ@Xh5n#v0DoR;+;Z58nS0H4wT_hJLbLAG~ zYrRSK8*Sln)KF_+45=A6Zz(jAYN2%jk!v%UX4yMHlxgOdyA@X80>riH=9_TFDd(JY z%Bg{4c;YIJdT7m5%?FAqK0s~B-RIz9+sa&;D ziq}TDb3lj3bP9Mt6T90Y_b#|mn&e0W_~vWfbk}A3BNs1PDN?v<$QGf)so;Y0GdRB6BEienIiq-%;=N81u*dW~k0i z@hSq8BSJk`)PEcJ?D3D(IOQ;c_mzC~eoB|C@q0+N7`4!B#TYQoNSA2I1P0(C9WF3G zy)Y=hOl^v_@>(JR0~NReO8^33h2jMr+Y~i@u8Mt(6&*;RKm*jVMB?6kXQcoGZ3{kg zcizGqUMC-XfMNjzz{1FlBo`on0m~L1=PQhwAI`x5DY|691!axvHq@8PW7@K`Ax>P! z!tJ^(D98#4?03S(>#X3Zdb!)JyhBM9u~Rw^=B<0>Dl4kThH8Yjq{3Q^^sA}*Woh8Q zD*pHmvDT@V*07o^q+^!amk&LZS(#lF{B@rac12!RiQ81R4y1PDR&#J~nR@PUj8 G0RTI66lj6~ literal 0 HcmV?d00001 diff --git a/index.html b/index.html index c94de45..a70c34f 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,65 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - Code Radio - + freeCodeCamp.org Code Radio