// Radio Emulator // Kaotisk Hund 2024 // // A simple co-mechanism to pretend playing a live radio as it would happen for // a radio station that mostly plays prerecoded shows. // // Client side implementation // Let's remind here the structures we are waiting for: // 1. Hash // 2. 0 // 3. audio/ogg file // 4. application/json file // // 1. Hash // Can be an SHA512sum or SHA256sum, we don't do checks, we only ask the hash // accompanied by what do we think it is. // // 2. 0 // When nothing exists on the radio station we are visiting. // // 3. audio/ogg file // An audio file to play. // // 4. application/json file // Could be one of the following: // - list // - show_info // var debugMode = true; const apiURL = "http://z.kaotisk-hund.com:8010/"; const version = "v0"; const listRequest = `${apiURL}${version}/list` const jsonRequest = `${apiURL}${version}/application/json/` const audioRequest = `${apiURL}${version}/audio/ogg/` const documentTitle = "Radio Station Emulator"; const separator = " :: "; var audioElement = document.querySelector('audio'); var placeholders = { currentTime: document.querySelector('.current-time'), listStarted: document.querySelector('.started-on'), currentShowHash : document.querySelector('.current-show-hash'), listHash : document.querySelector('.list-hash'), artist: document.querySelector('.artist'), title: document.querySelector('.title'), radioPlayer: document.querySelector('.radio-player'), youAreHere : document.querySelector('.you-are-here'), playProgress: document.querySelector('.play-progress'), startsOn: document.querySelector('.starts-on'), showDuration: document.querySelectorAll('.show-duration'), relativeTime: document.querySelector('.relative-time'), listeningAt: document.querySelector('.listening-at'), dlProgress: document.querySelector('.dl-progress'), minTimesPlayed: document.querySelector('.min-played'), maxTimesPlayed: document.querySelector('.max-played'), listDuration: document.querySelector('.list-duration'), previouslyPlayed: document.querySelector('.previously-played'), showInfo: document.querySelector('.show-info'), listInfo: document.querySelector('.list-info'), dt: document.querySelector('.d-t'), deltaTime: document.querySelector('.delta-time'), timeOfVisit: document.querySelector('.time-of-visit') } var values = { delta_time: 0, min_times_played: 0, maxTimesPlayed: 0, Dt: 0, now_on_sequence: 0, seconds_here: 0, current_time: 0 } function debugLog(message) { if ( debugMode ) console.log(message); } function setTitleSyncing(){ document.title = "🔃Syncing" + separator + documentTitle; } function setTitleMessage(message){ document.title = message + separator + documentTitle; } function updateComponentsAfterIncreaseSeconds() { placeholders.youAreHere.innerText = values.seconds_here; placeholders.playProgress.value = values.current_time + values.seconds_here; placeholders.relativeTime.innerText = values.current_time + values.seconds_here; placeholders.listeningAt.innerText = Math.floor(audioElement.currentTime); } function increaseSeconds() { values.seconds_here = values.seconds_here + 1; updateComponentsAfterIncreaseSeconds(); return values.seconds_here; } function getSecondsHere() { return values.seconds_here; } setInterval(increaseSeconds, 1000); function FetchJSON( url, callback, params ) { setTitleSyncing(); const request = new XMLHttpRequest(); request.addEventListener("load", ()=>{ var json = JSON.parse(request.response); if(request.status !== 404){ callback(json, params); } else { debugLog(`ERROR ${request.status} while loading ${url}`); } }); request.addEventListener("error", ()=>{ debugLog("An error occured. While loading: "+url+" for "+callback+"."); }); request.addEventListener("abort", ()=>{ debugLog("Request aborted: "+url+" for "+callback+"."); }); request.open("GET", url); request.send(); } function FetchAudio(url, callback) { const request = new XMLHttpRequest(); request.responseType = 'blob'; request.addEventListener("load", ()=>{ if(request.status === 200){ debugLog("Got it... trying!") placeholders.dlProgress.value = 100; audioElement.src = URL.createObjectURL(request.response); callback(); // audioElement.play(); debugLog("Tried... did it work?"); } else { debugLog(`ERROR ${request.status} while loading ${url}`); } }); request.addEventListener("progress", (event)=>{ if (event.lengthComputable) { debugLog(`Fetching: ${event.total}`); placeholders.dlProgress.value = (event.loaded/event.total)*100; } }); request.addEventListener("error", ()=>{ debugLog("An error occured. While loading: "+url); }); request.addEventListener("abort", ()=>{ debugLog("Request aborted: "+url); }); request.open("GET", url); request.send(); } function genericCallback(json, params) { debugLog('genericCallback'); debugLog(json); debugLog(params); } var calledLoadShowCallback = 0; function stopHereAndReflect() { return 0; } function updateComponentsAfterLoadShowCallback(json, listItem) { setTitleMessage ( "▶️ " + json.artist + " - " + json.title ); placeholders.currentShowHash.innerText = listItem.hash; placeholders.artist.innerText = json.artist; placeholders.title.innerText = json.title; placeholders.showDuration.forEach((element)=>{element.innerText = Math.floor(listItem.duration/1000)}); placeholders.startsOn.innerText = listItem.starts_on; placeholders.playProgress.max = Math.floor(listItem.duration/1000); placeholders.showInfo.innerText = JSON.stringify(json, null, 2); placeholders.currentTime.innerText = values.current_time; } function loadShowCallback(json, params) { const [ list, now_on_sequence, listItem, hash_of_list ] = params; debugLog('loadShowCallback'); debugLog(json); debugLog(listItem); //debugLog(params); audioElement.load(); FetchAudio(`${audioRequest}${json.hash}`, sync_radio); audioElement.type = json.mimetype; values.current_time = Math.floor((values.now_on_sequence/1000)); // - listItem.starts_on)/1000); updateComponentsAfterLoadShowCallback(json, listItem); audioElement.addEventListener('ended', function(){ values.current_time = 0; values.seconds_here = 0; placeholders.currentTime.value = 0; FetchJSON(`${listRequest}`, hashCallback, [ new Date().getTime() ]); }); } function slowIncreaseVolume() { if (audioElement.volume < 0.91) { audioElement.volume += 0.05; } else { audioElement.volume = 1; } } function preciseIncreaseVolume(value) { if ( value <= 1 ) audioElement.volume = value; } function fadeInAudio() { var timeSpan = 1000; var timeStep = 100; for ( var i = 0; i <= 90; i++ ) { timeSpan = timeSpan + timeStep; var newVolume = Math.sin(i*(Math.PI/180)); setTimeout( preciseIncreaseVolume(newVolume), timeSpan); } } function sync_radio() { var new_now = values.current_time + getSecondsHere(); debugLog("Trying to sync @ "+ values.current_time + " + " + getSecondsHere() + " = " + new_now); audioElement.currentTime = new_now; audioElement.play(); audioElement.muted = false; audioElement.volume = 0; fadeInAudio(); return new_now; } function syncOnDOMfromListCallback(now, hash_of_list, json) { placeholders.timeOfVisit.innerText = now; placeholders.listStarted.innerText = json.started_on; placeholders.listDuration.innerText = json.duration; placeholders.listHash.innerText = hash_of_list; placeholders.deltaTime.innerText = values.delta_time; placeholders.minTimesPlayed.innerText = values.min_times_played; placeholders.maxTimesPlayed.innerText = values.max_times_to_be_played; placeholders.dt.innerText = values.Dt; placeholders.currentTime.innerText = values.now_on_sequence; placeholders.listInfo.innerText = JSON.stringify(json, null, 2); } function appendPreviouslyPlayedShows(listItem){ var tmp = document.createElement('pre'); tmp.innerText = JSON.stringify(listItem, null, 2); placeholders.previouslyPlayed.append(tmp); } function listCallback(json, params) { debugLog('listCallback'); debugLog(json); debugLog(params); var [ now, hash_of_list ] = params; values.delta_time = now - json.started_on; values.min_times_played = Math.floor( values.delta_time / json.duration ); values.max_times_to_be_played = values.delta_time / json.duration; values.Dt = values.max_times_to_be_played - values.min_times_played; values.now_on_sequence = values.Dt * json.duration; syncOnDOMfromListCallback(now, hash_of_list, json); debugLog(`now_on_sequence: ${values.now_on_sequence}, Dt: ${values.Dt}`) debugLog(json.list.map((item, index)=>({index, value: item})).sort((a,b)=>{return b.value.index - a.value.index})); if ( json.list.length === 1 ) { FetchJSON(`${jsonRequest}${json.list[0].hash}`, loadShowCallback, [json, values.now_on_sequence, json.list[0], hash_of_list]); } else { for ( var i = 0; i < json.list.length - 1; i++) { debugLog("getting there " + i) debugLog(`${json.list[i].starts_on} < ${values.now_on_sequence} < ${json.list[i+1].starts_on}`); if ( json.list[i].starts_on < values.now_on_sequence && values.now_on_sequence < json.list[i+1].starts_on ) { if( i !== 0) { appendPreviouslyPlayedShows(json.list[i-1]); } values.now_on_sequence = values.now_on_sequence - json.list[i].starts_on; debugLog(`now_on_sequence (1updated): ${values.now_on_sequence}`); FetchJSON(`${jsonRequest}${json.list[i].hash}`, loadShowCallback, [json, values.now_on_sequence, json.list[i], hash_of_list]); debugLog('First!'); break; } else if ( values.now_on_sequence > json.list[i].starts_on && values.now_on_sequence > json.list[i+1].starts_on ) { appendPreviouslyPlayedShows(json.list[i]); values.now_on_sequence = values.now_on_sequence - json.list[i+1].starts_on; FetchJSON(`${jsonRequest}${json.list[i+1].hash}`, loadShowCallback, [json, values.now_on_sequence, json.list[i+1], hash_of_list]); debugLog('Second!'); break; } else { // values.now_on_sequence = values.now_on_sequence - json.list[i].starts_on; debugLog(`We are here: ${i}`); debugLog(values.now_on_sequence); debugLog(json.list[i].starts_on); if (i > 0) debugLog(json.list[i-1].starts_on); debugLog('Nothing!'); // continue; } } } } function hashCallback(json, params) { var [ now ] = params; debugLog('hashCallback'); FetchJSON(`${jsonRequest}${json.latest_list}`, listCallback, [now, json.latest_list]); } FetchJSON(`${listRequest}`, hashCallback, [ new Date().getTime() ]);