From a9a97d9495b67258c2eeab196ce860a010cff466 Mon Sep 17 00:00:00 2001 From: kaotisk Date: Tue, 3 Dec 2024 15:07:36 +0200 Subject: Integrate radio into the webui --- src/css/akn.css | 174 ++++++++++++++ src/js/image-generator.js | 146 ++++++++++++ src/js/radio-emulator.js | 458 +++++++++++++++++++++++++++++++++++++ src/js/ui/sections/radioSection.js | 194 ++++++++++++++-- 4 files changed, 959 insertions(+), 13 deletions(-) create mode 100644 src/js/image-generator.js create mode 100644 src/js/radio-emulator.js (limited to 'src') diff --git a/src/css/akn.css b/src/css/akn.css index 04b4917..e034628 100644 --- a/src/css/akn.css +++ b/src/css/akn.css @@ -553,3 +553,177 @@ details { display: flex; flex-direction: row; } + +/* Radio CSS */ +* { + padding: 0px; + margin: 0; +} + +#radio-main div { + display: flex; + flex-direction: column; + gap: 0px; +} + +#radio-main { + order: 2; + gap: 10px; +} + +#radio-main .div-groups-row { + display: flex; + flex-direction: row; + align-items: center; +} + +#radio-main progress { + width: auto; +} + +#radio-main a { + color: orange; +} + +#radio-main a:hover { + color: yellow; +} + +#radio-main button { + background:black; + color:red; + font-family: 'Doto'; + border: dotted 2px; + width: 100%; +} + +#radio-main .no-break { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + word-break: none; +} + +#radio-main button { + background:black; + color:red; + font-size: 24px; + border: dotted 2px; +} + +#radio-main button:hover { + background:#ffff96; + color:red; + border: dotted 2px aliceblue; +} + +#radio-main pre { + border: 1px dotted; + overflow-x: auto; + padding: 0.5vh 1vw; +} + +#radio-main details { + border: 1px dotted; + padding: 1vh 2vw; + width: 88vh; +} + +#radio-main summary { + border-bottom: 1px dotted; + padding: 0.5vh 1vw; +} + +#radio-main summary:hover { + background-color: lightgreen; + color: black; +} + +#radio-main .start-top { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-evenly; + align-items: flex-start; + gap: 10px; +} + +#radio-main .previously-played { + flex-direction: column-reverse; + overflow-y: auto; + height: 40vh; + gap: 5px; +} + +#radio-main .div-row { + flex-direction: row; + align-items: center; + justify-content: center; + flex-wrap: wrap; +} + +#radio-main .previously-played img { + width: 48px; +} + +#radio-main .previously-played div { + gap: 5px; +} + + +#radio-main .previously-played div:hover { + background-color: lightgreen; + color: black; +} + +#radio-main .div-inline-reverse { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + align-items: center; + justify-content: space-evenly; +} + +#radio-main .div-inline { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: space-evenly; +} + +#radio-main table { + width: 100%; +} + +#radio-main th, #radio-main td { + background-color: black; + padding: 2px; +} + +#radio-main tr { + background-color: black; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + border: 1px dotted; + align-items: center; +} + +#radio-main audio { + width: auto; +} + +#radio-main .generated { + width: 128px; + height: 128px; + max-width: 128px; + max-height: 128px; +} + +#radio-main .more-details { + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; +} diff --git a/src/js/image-generator.js b/src/js/image-generator.js new file mode 100644 index 0000000..99f6ed9 --- /dev/null +++ b/src/js/image-generator.js @@ -0,0 +1,146 @@ +// Image generator from SHA512SUM to 256px*256px canvas +// +// Kaotisk Hund - 2024 +// +// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0 +// + +export function generateImage(hash, what) +{ + var pixels = []; + + var yi = 0; + var xi = 0; + for ( var y = 0; y < 256; ++y ){ + pixels[y] = []; + for ( var x = 0; x < 256; ++x ) + { + pixels[y][x] = '#000'; + } + } + var z = 1; + for ( var y = 0; y < 256; ++y ) + { + yi = y - 128; + for ( var x = 0; x < 256; ++x ) + { + var pixel; + xi = x - 128; + z=Math.abs(Math.floor((yi*yi+xi*xi)/3)); + //z=Math.abs(Math.floor((xi*yi)/1)); + var r1 = hash[z] === undefined ?'0':hash[z]; + var r2 = hash[z+1] === undefined ?'0':hash[z+1]; + var g1 = hash[z+2] === undefined ?'0':hash[z+2]; + var g2 = hash[z+3] === undefined ?'0':hash[z+3]; + var b1 = hash[z+4] === undefined ?'0':hash[z+4]; + var b2 = hash[z+5] === undefined ?'0':hash[z+5]; + var t1 = hash[z+6] === undefined ?'0':hash[z+6]; + var t2 = hash[z+7] === undefined ?'0':hash[z+7]; + if (t2 === undefined) + { + pixel = "#000"; + } + else + { + pixel = `#${r1}${r2}${g1}${g2}${b1}${b2}`; + //pixel = `#${r1}${r2}${g1}${g2}${b1}${b2}${t1}${t2}`; + //pixel = `#${r1}${r1}${r1}${r1}${r1}${r1}${r1}${r1}`; + //pixel = `#${r1}${r1}${r1}${r1}${r1}${r1}`; + //pixel = `#${r1}${g1}${b1}`; + } + if ( 0 >= xi && 0 >= yi ) + { + // //z=Math.abs(Math.floor((yi*xi)/4)); + // //if ( z > 120 ) z = z-120; + // //pixel = `#${hash[z-113]}${hash[z-114]}${hash[z-115]}${hash[z-116]}${hash[z-117]}${hash[z-118]}${hash[z-119]}${hash[z-120]}`; + // pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}${hash[z+6]}${hash[z+7]}`; + // //pixel = '#2a2' + } + else if ( 0 <= xi && 0 >= yi ) + { + //pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}`; //${hash[z+6]}${hash[z+7]}`; + // //z=Math.abs(Math.floor((yi*xi)/128)); + // pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}${hash[z+6]}${hash[z+7]}`; + // // pixel = '#000'; + // //pixel = '#22a'; + } + else if ( 0 >= xi && 0 <= yi ) + { + //pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}`; //${hash[z+6]}${hash[z+7]}`; + // z=Math.abs(Math.floor((yi*xi)/128)); + // // pixel = '#000'; + // pixel = '#a22'; + } + else if ( 0 <= xi && 0 <= yi ) + { + //pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}`; //${hash[z+6]}${hash[z+7]}`; + // z=Math.abs(Math.floor((yi*xi)/128)); + // pixel = '#000'; + } + pixels[y][x] = pixel; + } + } + createImage( pixels, hash, what ); +} + +function createImage(pixels, hash, what) +{ + //debugLog(pixels); + var canvas = document.createElement('canvas'); + //canvas.width = pixels[0].length; + //canvas.height = pixels.length; + var output_x_y = 256; + + canvas.width = output_x_y; + canvas.height = output_x_y; + var context = canvas.getContext('2d'); + var zoom_factor = 6; + var zoom_size = Math.floor(output_x_y/zoom_factor); + var radius = Math.floor(output_x_y/zoom_size); + // debugLog(`Radius: ${radius}`); + for ( var y = 0; y < zoom_size; ++y ) + { + for ( var x = 0; x < zoom_size; ++x ) + { + var fy = (canvas.height-zoom_size)/2; + var fx = (canvas.width-zoom_size)/2; + var ax = Math.floor(x+fx+1); + var ay = Math.floor(y+fy+1); + //debugLog(`${ax} + ${ay}`); + var pixel_to_be_enhanced = pixels[ay][ax]; + var c = 0; + var r = 0; + for (var i = 0; i < radius; ++i) + { + for (var k = 0; k < radius; ++k) + { + var subpixel = pixel_to_be_enhanced; + if ( i >= radius - Math.floor(radius/3) ) + { + if ( k >= radius - Math.floor(radius/3) ) + { + // debugLog(i); + c = (x*radius)+k; + r = (y*radius)+i; + context.fillStyle = subpixel; + context.fillRect(c, r, radius/3, radius/3); + context.stroke(); + } + } + //debugLog(`${x} -> ${c}, ${y} -> ${r}`); + } + } + } + } + if ( what !== 'new' ) + { + document.querySelector('.generated').src = canvas.toDataURL('image/png'); + } + else + { + document.querySelector(`#i-${hash}`).src = canvas.toDataURL('image/png'); + document.querySelector(`#i-${hash}`).width = 128; + } +} + +// @license-end diff --git a/src/js/radio-emulator.js b/src/js/radio-emulator.js new file mode 100644 index 0000000..0926d2a --- /dev/null +++ b/src/js/radio-emulator.js @@ -0,0 +1,458 @@ +// Radio Station Emulator +// +// Kaotisk Hund - 2024 +// +// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0 +// +// 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 +// +import { makeElement } from "./arching-kaos-generator.js"; +import { generateImage } from "./image-generator.js"; + +var debugMode = false; +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 = " :: "; + +export function getAudioElement() +{ + return document.querySelector('#radio-player'); +} + +export function getCurrentTime() +{ + return document.querySelector('.current-time'); +} + +export function getListStarted() +{ + return document.querySelector('.started-on'); +} + +export function getCurrentShowHash () +{ + return document.querySelector('.current-show-hash'); +} + +export function getListHash () +{ + return document.querySelector('.list-hash'); +} + +export function getArtist() +{ + return document.querySelector('.artist'); +} + +export function getTitle() +{ + return document.querySelector('.title'); +} + +export function getRadioPlayer() +{ + return document.querySelector('.radio-player'); +} + +export function getYouAreHere () +{ + return document.querySelector('.you-are-here'); +} + +export function getPlayProgress() +{ + return document.querySelector('.play-progress'); +} + +export function getStartsOn() +{ + return document.querySelector('.starts-on'); +} + +export function getShowDuration() +{ + return document.querySelectorAll('.show-duration'); +} + +export function getRelativeTime() +{ + return document.querySelector('.relative-time'); +} + +export function getListeningAt() +{ + return document.querySelector('.listening-at'); +} + +export function getDlProgress() +{ + return document.querySelector('.dl-progress'); +} + +export function getMinTimesPlayed() +{ + return document.querySelector('.min-played'); +} + +export function getMaxTimesPlayed() +{ + return document.querySelector('.max-played'); +} + +export function getListDuration() +{ + return document.querySelector('.list-duration'); +} + +export function getPreviouslyPlayed() +{ + return document.querySelector('.previously-played'); +} + +export function getShowInfo() +{ + return document.querySelector('.show-info'); +} + +export function getListInfo() +{ + return document.querySelector('.list-info'); +} + +export function getDt() +{ + return document.querySelector('.d-t'); +} + +export function getDeltaTime() +{ + return document.querySelector('.delta-time'); +} + +export function getTimeOfVisit() +{ + return 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() +{ + getYouAreHere().innerText = values.seconds_here; + getPlayProgress().value = values.current_time + values.seconds_here; + getRelativeTime().innerText = values.current_time + values.seconds_here; + getListeningAt().innerText = Math.floor(getAudioElement().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!") + getDlProgress().value = 100; + getAudioElement().src = URL.createObjectURL(request.response); + callback(); + //getAudioElement().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}`); + getDlProgress().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 ); + getCurrentShowHash().innerText = listItem.hash; + getArtist().innerText = json.artist; + getTitle().innerText = json.title; + getShowDuration().forEach((element)=>{element.innerText = Math.floor(listItem.duration/1000)}); + getStartsOn().innerText = listItem.starts_on; + getPlayProgress().max = Math.floor(listItem.duration/1000); + getShowInfo().innerText = JSON.stringify(json, null, 2); + getCurrentTime().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); + getAudioElement().load(); + FetchAudio(`${audioRequest}${json.hash}`, sync_radio); + getAudioElement().type = json.mimetype; + values.current_time = Math.floor((values.now_on_sequence/1000)); // - listItem.starts_on)/1000); + updateComponentsAfterLoadShowCallback(json, listItem); + getAudioElement().addEventListener('ended', function(){ + values.current_time = 0; + values.seconds_here = 0; + getCurrentTime().value = 0; + FetchJSON(`${listRequest}`, hashCallback, [ new Date().getTime() ]); + }); +} + +function preciseIncreaseVolume(value) +{ + if ( value <= 1 ) getAudioElement().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); + } +} + +export function sync_radio() +{ + var new_now = values.current_time + getSecondsHere(); + debugLog("Trying to sync @ "+ values.current_time + " + " + getSecondsHere() + " = " + new_now); + getAudioElement().currentTime = new_now; + getAudioElement().play(); + getAudioElement().muted = false; + getAudioElement().volume = 0; + fadeInAudio(); + return new_now; +} + +function syncOnDOMfromListCallback(now, hash_of_list, json) +{ + getTimeOfVisit().innerText = now; + getListStarted().innerText = json.started_on; + getListDuration().innerText = json.duration; + getListHash().innerText = hash_of_list; + getDeltaTime().innerText = values.delta_time; + getMinTimesPlayed().innerText = values.min_times_played; + getMaxTimesPlayed().innerText = values.max_times_to_be_played; + getDt().innerText = values.Dt; + getCurrentTime().innerText = values.now_on_sequence; + getListInfo().innerText = JSON.stringify(json, null, 2); +} + +function infoShowCallback(json, params) +{ + var [ hash ] = params; + if ( document.querySelector(`#d-${hash}`) !== null ) + { + var parent = document.querySelector(`#d-${hash}`); + var tableElement = { + element:'div', + id: `t-${hash}`, + innerHTML:[ + {element:"div",innerText:`${json.artist}`}, + {element:"div",innerText:`${json.title}`} + ] + }; + makeElement(tableElement, parent); + } +} + +function appendPreviouslyPlayedShows(listItem){ + if ( document.querySelector(`#d-${listItem.hash}`) === null ) + { + var tmp = { + element:'div', + style: 'flex-direction:row;align-items:center;', + id: `d-${listItem.hash}`, + innerHTML:[ + { element:"img", id: `i-${listItem.hash}` } + ] + } + makeElement(tmp, getPreviouslyPlayed()); + document.querySelector(`#d-${listItem.hash}`).scrollIntoView(); + generateImage(listItem.hash, 'new'); + FetchJSON(`${jsonRequest}${listItem.hash}`, infoShowCallback, [listItem.hash]); + } +} + +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++) + { + if( i !== 0 ) appendPreviouslyPlayedShows(json.list[i-1]); + 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 ) + { + 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]); + generateImage(json.list[i].hash); + debugLog('First!'); + break; + } + else if ( values.now_on_sequence > json.list[i+1].starts_on && i === json.list.length - 2 ) + { + if( i !== 0 ) 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]); + generateImage(json.list[i].hash); + debugLog('Second!'); + break; + } + else + { + 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!'); + } + } + } +} + +function hashCallback(json, params) +{ + var [ now ] = params; + debugLog('hashCallback'); + FetchJSON(`${jsonRequest}${json.latest_list}`, listCallback, [now, json.latest_list]); +} + +export function start_radio() +{ + FetchJSON(`${listRequest}`, hashCallback, [ new Date().getTime() ]); +} + +// @license-end diff --git a/src/js/ui/sections/radioSection.js b/src/js/ui/sections/radioSection.js index cddd3f6..0cc131f 100644 --- a/src/js/ui/sections/radioSection.js +++ b/src/js/ui/sections/radioSection.js @@ -6,30 +6,198 @@ // import { makeElement } from "../../arching-kaos-generator.js"; +import { start_radio, sync_radio } from "../../radio-emulator.js"; + +window.start_radio = start_radio; +window.sync_radio = sync_radio; export function radioSection() { + var whereAmI = { + element: "div", + className: "where-am-i", + innerHTML: [ + { element: "img", src:"./img/logo.png", onclick:"menusel({id:'#/welcome-section'})"}, + { element: "span", innerText:">"}, + { element: "h2", innerText:"Radio"} + ] + }; + var radioContent = { + element:"div", + id: "radio-main", + innerHTML:[ + {element:"div",className:"start-top",innerHTML:[ + {element:"div",className:"div-groups",style:"flex-grow:1;",innerHTML:[ + {element:"div",className:"now-playing-details",innerHTML:[ + {element:"div",className:"div-groups-row",innerHTML:[ + {element:"img", className:"generated"}, + {element:"table", innerHTML:[ + {element:"tr",innerHTML:[ + {element:"th", innerText:"Artist"}, + {element:"td", className:"artist"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"Title"}, + {element:"td", className:"title"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"Starts On (ms)"}, + {element:"td", className:"starts-on"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"Duration"}, + {element:"td",className:"show-duration"} + ]} + ]} + ]} + ]}, + {element:"div", className:"enhanced-player",innerHTML:[ + {element:"div", className:"radio-player",innerHTML:[ + {element:"audio", id: "radio-player", preload:"auto", controls:true, muted:true} + ]}, + {element:"div", className:"div-row",innerHTML:[ + {element:"button", id:"start-button",innerText:"Start!"}, + {element:"button", id:"sync-button",innerText:"Sync"} + ]} + ]} + ]}, + {element:"div",className:"previously-played"}, + ]}, + {element:"div", className:"more-details", innerHTML:[ + {element:"details",id:"progress-details", innerHTML:[ + {element:"summary", innerText:"Progress details"}, + {element:"div",innerHTML:[ + {element:"p", innerText:"Download progress:"}, + {element:"progress",className:"dl-progress", max:"100"} + ]}, + {element:"div",innerHTML:[ + {element:"p", innerText:"Live progress:"}, + {element:"progress",className:"play-progress"} + ]}, + {element:"div", className:"div-inline",innerHTML:[ + {element:"div", className:"div-inline",innerHTML:[ + {element:"p", innerText:"Listening at:"}, + {element:"p", className:"listening-at"}, + ]}, + {element:"div", className:"div-inline",innerHTML:[ + {element:"p", innerText:"Show playback:"}, + {element:"div", className:"no-break",innerHTML:[ + {element:"p", className:"relative-time"}, + {element:"p", innerText:"/"}, + {element:"p", className:"show-duration"} + ]} + ]} + ]} + ]}, + {element:"details",id:"sync-info", innerHTML:[ + {element:"summary", innerText:"Sync info"}, + {element:"div",className:"groups",innerHTML:[ + {element:"h3", innerText:"Timings"}, + {element:"table", innerHTML:[ + {element:"tr",innerHTML:[ + {element:"th", innerText:"Time of visit (ms)"}, + {element:"td", className:"time-of-visit"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"Time elapsed since visited (s)"}, + {element:"td", className:"you-are-here"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"List started on (ms)"}, + {element:"td", className:"started-on"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"List duration (s)"}, + {element:"td",className:"list-duration"} + ]} + ]}, + {element:"h3", innerText:"Calculations"}, + {element:"table", innerHTML:[ + {element:"tr",innerHTML:[ + {element:"th", innerText:"Times Fully Played"}, + {element:"td", className:"min-played"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"Times Played"}, + {element:"td", className:"max-played"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"Dt = TP - TFP"}, + {element:"td", className:"d-t"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"Delta time"}, + {element:"td",className:"delta-time"} + ]}, + {element:"tr",innerHTML:[ + {element:"th", innerText:"Initial \"tune in\" time (s)"}, + {element:"td",className:"current-time"} + ]} + + ]}, + ]} + ]}, + {element:"details",id:"about", innerHTML:[ + {element:"summary",innerText:"Info to get you started"}, + {element:"h2",innerText:"Notice"}, + {element:"p",innerText:"If you are visiting for the first time, you might need to \"Allow Audio\" first. Please do and refresh the page."}, + {element:"h2",innerText:"About"}, + {element:"p",innerText:"This is a \"Radio Station Emulator\". We create lists with shows to be played, but since we are not doing streaming of the playlists, we offer another way of \"tuning in\"."}, + {element:"p",innerText:"Providing the time a list started playing and having pre-calculated the starting timestamps of each show relevant to the list start timestamp, we can calculate which show is on and what its current time of playing is."}, + {element:"p",innerText:"Ultimately, you are hearing what we would be streaming, as you would do for a regular radio station."}, + {element:"h2",innerText:"Steps"}, + {element:"ol",innerHTML:[ + {element:"li",innerText:"First, your browser is going to ask our server here, what is the current list that plays right now. It will get a response and will fetch that list."}, + {element:"li",innerText:"Based on properties of the list overall and the time of visit, your browser will start comparing each show's properties found in the list, to figure the relative time of yours on the list. A progress bar labeled \"Live progress\" will be indicating the correct time on the show."}, + {element:"li",innerText:"Having figured out the show that is playing, it will go and download the whole show. Progress of that would be observable via the \"Download progress\" bar below."}, + {element:"li",innerText:"Upon completion of the download, the \"Sync\" button is auto pressed and the player start playing the show from the calculated second it calculated previously."}, + ]}, + {element:"h2",innerText:"Notes"}, + {element:"ul",innerHTML:[ + {element:"li",innerText:"Sometimes, you might need to press the \"Sync\" button more than once. That's mostly due to bandwidth capabilities of both the server and the client. The \"tune\" would be right if you press it multiple times and land near the same timespace over and over."}, + {element:"li",innerText:"Ideally, if \"Listening at:\" and \"Show playback:\" havea the same value then you are in sync!"} + ]} + ]}, + {element:"details",id:"data", innerHTML:[ + {element:"summary", innerText:"Data segments"}, + {element:"h3",innerText:"Hash of list"}, + {element:"pre", className:"list-hash"}, + {element:"h3",innerText:"List info (JSON)"}, + {element:"pre", className:"list-info"}, + {element:"h3",innerText:"Hash of current show"}, + {element:"pre", className:"current-show-hash"}, + {element:"h3",innerText:"Show info (JSON)"}, + {element:"pre", className:"show-info"}, + {element:"div", innerHTML:[ + {element:"a", href:"./data.html", target:"_blank",innerText:"Data"} + ]} + ]} + ]} + ] + }; + var content = { + element: "div", + className: "content", + innerHTML: [ + radioContent + ] + }; var radioSection = { element: "div", id: 'radio-section', hidden: true, style: 'height: 100%;', innerHTML: [ - { - element: 'button', - style:"position: fixed;", - onclick:"refreshRadio()", - innerText:'Refresh' - }, - { - element: 'iframe', - id:"radio-iframe", - src:"https://radio.arching-kaos.com", - style:"width: 100%; height: 100%;" - } + whereAmI, + content ] }; - makeElement(radioSection, document.querySelector('.main')); + document.querySelector("#start-button").addEventListener("click",()=>{ + start_radio(); + document.querySelector("#start-button").remove(); + }); + document.querySelector("#sync-button").addEventListener("click",()=>{sync_radio();}); } + // @license-end -- cgit v1.2.3