<!DOCTYPE html>
<!-----------------------------------------------------------------------------
     Copyright 2014-2025 Jesper Larsson

     This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>

     Klipspringer is free software: you can redistribute it and/or modify it
     under the terms of the GNU General Public License as published by the Free
     Software Foundation, either version 3 of the License, or (at your option)
     any later version.

     Klipspringer is distributed in the hope that it will be useful, but
     WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
     for more details.

     You should have received a copy of the GNU General Public License along
     with Klipspringer. If not, see <https://www.gnu.org/licenses/>.
   --------------------------------------------------------------------------->

<html>
    <head>
        <meta charset="UTF-8">
        <style>$(klipstyle.css:file)</style>
        <title>Play</title>
        <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
        $(klipicons.html:file)
    </head>

    <body>
        $(controls_common.html:file)

        <script>
         'use strict';
         $(sendcmd.js:file)

         const source = $(source);              // the source element from sources.json
         let progressTimer;                     // the timer used for progress loop

         // Sends command to server.
         const streamCmd = (cmd, args) => sendCmd(cmd, '&source=$(source_ix)&player_id=$(player_id)'+(args || ''));
         const streamCmdRestarting = (cmd, args) => {
             const playing = !audioElement.paused;
             streamCmd(cmd, args).then(ans => {
                 if (ans.restart) {
                     audioElement.src = streamUrl(ans.track, ans.time);
                     audioElement.load();
                     if (playing) { play() }
                 }
             });
         }

         // Find the first audio type that works.
         const audioTypeList = $(audiotype_list);
         let audioTypeIx = -1;
         const nextStreamType = () => {
             while (++audioTypeIx < audioTypeList.length) {
                 if (new Audio().canPlayType(audioTypeList[audioTypeIx][0]).length) { return audioTypeList[audioTypeIx][1] }
             }
             throw Error("No available format supported");
         }
         let streamType = nextStreamType();
         let subloadId=0;                       // included in stream URL to force the browser to refetch

         // Composes the stream URL.
         const streamUrl = (track, time, monomix) =>
             ('/audiostream?source=$(source_ix)&player_id=$(player_id)&timeout=$(timeout)&load_id=$(load_id).'+(subloadId++)+
              '&type='+streamType+'&track='+track+'&time='+time+
              (monomix === undefined ? "" : "&monomix="+(monomix ? "on" : "off")));

         // Initialize the audio element.
         const audioElement = new Audio(streamUrl($(stream_track), $(stream_time)));

         // On load metadata: get player status, and pause audio.
         audioElement.addEventListener('loadedmetadata', () => {
             updateStatus(0).then(() => updateDisplay(0)).catch(err => {
                 playStatus.err = playStatus.err || "Error updating status on loaded metadata: "+err;
                 console.error("Get status failed on metadata loaded: "+err);
                 audioElement.pause();
                 updateNotifs({ ...playStatus, paused: true });
             });
         });

         // On going into paused mode: update notifications.
         audioElement.addEventListener('pause', () => {
             clearTimeout(progressTimer);
             if (!playStatus.seeking) { updateNotifs({ ...playStatus, paused: audioElement.paused }) }
         });

         // On playing: reset, get new status, and launch progress loop.
         audioElement.addEventListener('playing', () => {
             playStatus.err = false;
             playStatus.seeking = false;
             updateStatus(playedTime()).then(() => {
                 updateNotifs({ ...playStatus, paused: audioElement.paused });
                 progressLoop();
             }).catch(err => {
                 audioElement.pause();
                 console.error("Get status failed on playing: "+JSON.stringify(err));
                 if (err.defunct) {
                     // Server seems to have timed out, reload page
                     window.location.reload();
                 }
                 playStatus.err = playStatus.err || err;
                 updateNotifs({ ...playStatus, paused: true });
             });
         });

         // On error: stop progress loop, display error, and set up to reload stream.
         audioElement.addEventListener('error', () => {
             clearTimeout(progressTimer);
             console.error("Error caught: "+audioElement.error.code+" ("+audioElement.error.message+")");
             if ([3, 4].includes(+audioElement.error.code) && audioTypeIx < audioTypeList.length-1) {
                 streamType = nextStreamType();
                 audioElement.src = streamUrl($(stream_track), $(stream_time));
                 audioElement.play();
             } else {
                 playStatus.err = playStatus.err || "Error code: "+audioElement.error.code;
                 updateNotifs({ ...playStatus, paused: audioElement.paused });
             }
         });

         // On end of data: if there are more tracks, load the next one, otherwise quit.
         audioElement.addEventListener('ended', () => {
             streamCmd("set_played_get_status", "&time="+playedTime());
             if (playStatus.has_following_track) {
                 audioElement.src = streamUrl(playStatus.track_ix+1, 0);
                 audioElement.load();
                 play();
             }
         });

         // Starts audio element play, updates notifications on accept, and handles errors on reject.
         const play = () => audioElement.play().then(() => {
             updateNotifs({ ...playStatus, paused: audioElement.paused });
         }).catch(err => {
             clearTimeout(progressTimer);
             if (err instanceof DOMException && err.name === 'NotAllowedError' && playStatus.initializing) {
                 // Fine, user just needs to click play.
             } else {
                 const message = "Play failed: " + err + (audioElement.error ? audioElement.error.code+" ("+audioElement.error.message+")" : "");
                 console.error(message);
                 playStatus.err = playStatus.err || message;
                 updateNotifs({ ...playStatus, paused: audioElement.paused });
             }
         });

         const setMonomix = (on) => { seek(0, on) }

         // Gets player status and updates stuff.
         const updateStatus = (played) => streamCmd("set_played_get_status", "&time="+played+"&info_id="+(playStatus.info_id || 0)).then(ans => {
             if (ans.err) {
                 console.error("Error in status update: "+JSON.stringify(ans.err));
                 return Promise.reject(ans);
             } else {
                 playStatus = ans;
                 playStatus.set_time = Date.now();

                 if (source && source.rate_menu) {
                     rateselectElt.value = playStatus.rec_rate;
                     rateselectElt.classList.remove("hidden");
                 }
                 if (source && source.bits_menu) {
                     bitsselectElt.value = playStatus.rec_bits;
                     bitsselectElt.classList.remove("hidden");
                 }
                 if (source && source.channels_menu) {
                     channelsselectElt.classList.remove("hidden");
                     channelsselectElt.value = playStatus.rec_channels;
                 }
                 if (source && (source.rate_menu || source.bits_menu || source.channels_menu)) {
                     sampleMenusElt.classList.remove("hidden");
                 }
                 if (source && source.player === "devicestream") {
                     timeLimitElt.value = playStatus.rec_limit;
                     timeLimitsElt.classList.remove("hidden");
                 }
                 updatePlayerInfo(ans);
                 if (!audioElement.paused) { updateDisplay(playedTime()) }
             }
         });

         // Updates the track display given the amount of played time.
         const updateDisplay = (played) => {
             updateTrackInfo({ ...playStatus, paused: audioElement.paused });
             if (playStatus.rec_time === undefined) {
                 bitsselectElt.disabled = false;
                 rateselectElt.disabled = false;
                 channelsselectElt.disabled = false;
                 showTime(played - (playStatus.current_track_start || 0));
             } else {
                 bitsselectElt.disabled = true;
                 rateselectElt.disabled = true;
                 channelsselectElt.disabled = true;
                 showTime(playStatus.rec_time+(Date.now()-playStatus.set_time)/1000);
             }
         }

         // Gets the played time reported by the audio element.
         const playedTime = () => {
             const last = audioElement.played.length-1;
             return last < 0 ? 0 : audioElement.played.end(last);
         }

         // Starts or stops recording.
         const start_record = () => { streamCmd('start_record') }
         const stop_record = () => { streamCmd('stop_record') }

         // Called when playing is finished.
         const quit = () => {
             audioElement.pause();
             streamCmd('quit').catch(() => { }).then(() => {
                 window.location.href = "/";
             });
         }

         const seek = (off, monomix) => {
             const destTime = playedTime()-playStatus.current_track_start+off;
             if (destTime >= playStatus.current_track_length && !playStatus.has_following_track) { return }
             const playing = !audioElement.paused;
             playStatus.seeking = true;
             audioElement.pause();
             audioElement.src = streamUrl(playStatus.track_ix, destTime, monomix);
             audioElement.load();
             if (playing) { play() }
         }

         const seeka = (cmd, args) => {
             streamCmdRestarting(cmd, args);
         }

         const skip = (cmd) => seeka(cmd, '&from_track='+playStatus.track_ix+'&from_time='+(playedTime()-playStatus.current_track_start));

         const jumpTrack = (i) => seeka('seeka', '&track='+i);

         const formatString = (r, b, c) => {
             let f = "", d = "";
             if (r.length) { f += d+"-r%20"+r; d = "%20" }
             if (b.length) { f += d+"-b%20"+encodeURIComponent(b); d = "%20" }
             if (c.length) { f += d+"-c%20"+c; d = "%20" }
             return f;
         }

         // Sets the audio quality for input device according to the current menu settings.
         const setAudioFormat = () => {
             playStatus.seeking = true;
             audioElement.pause();
             streamCmd("set_format", "&format="+formatString(rateselectElt.value, bitsselectElt.value, channelsselectElt.value))
                 .then(() => {
                     audioElement.src = streamUrl(0, 0);
                     audioElement.load();
                     play();
                 });
         }

         defineButtons({
             prev: () => { skip('prev') },
             next: () => { skip('next') },
             pause: () => { audioElement.pause() },
             play: () => { play() },
             xrew: () => { seek(-30) },
             rew: () => { seek(-5) },
             ffd: () => { seek(5) },
             xffd: () => { seek(30) },
             rec: () => { start_record() },
             stop: () => { stop_record() },
             home: () => { quit() },
             speakers: source.speakers_mirror === "" || isNaN(source.speakers_mirror)
                   ? null
                   : () => {
                       window.location.href = "/choice?source="+source.speakers_mirror
                                             +"&path="+encodeURIComponent(sourcePath)
                                             +"&track_number="+playStatus.track_number
                                             +"&track_time="+(playedTime() - playStatus.current_track_start);
                   }
         });

         // Initialize the bits menu.
         if (source && source.bits_menu) {
             for (const v of source.bits_menu) {
                 const opt = document.createElement("option");
                 const plusp = v.indexOf ? v.indexOf("+") : -1;
                 opt.value = v.toString();
                 opt.innerHTML = (plusp < 0 ? v : v.slice(0, plusp))+"-bit";
                 bitsselectElt.appendChild(opt);
             }
             bitsselectElt.addEventListener("change", setAudioFormat);
         }

         // Initialize the rate menu.
         if (source && source.rate_menu) {
             for (const v of source.rate_menu) {
                 const opt = document.createElement("option");
                 opt.value = v.toString();
                 opt.innerHTML = v >= 1000 ? v/1000 + " kHz" : v+" Hz";
                 rateselectElt.appendChild(opt);
             }
             rateselectElt.addEventListener("change", setAudioFormat);
         }

         // Initialize the channels menu.
         if (source && source.channels_menu) {
             for (const v of source.channels_menu) {
                 const opt = document.createElement("option");
                 opt.value = v.toString();
                 opt.innerHTML = v === 2 ? "Stereo" : (v === 1 ? "Mono" : v+" channels");
                 channelsselectElt.appendChild(opt);
             }
             channelsselectElt.addEventListener("change", setAudioFormat);
         }

         // Initialize time limit element.
         if (source && source.player === "devicestream") {
             timeLimitElt.addEventListener("change", () => {
                 streamCmd("set_reclimit", "&minutes="+timeLimitElt.value);
             });
         }

         const statusError = (err) => {
             console.error("Scheduled set played failed: "+err);
             playStatus.err = err ||  "Error updating status: "+err;
             audioElement.pause();
             updateNotifs({ ...playStatus, paused: true });
         }

         // Called in progress loop to update display.
         const reportPlayed = () => {
             const last = audioElement.played.length-1;
             if (last >= 0) {
                 const played = audioElement.played.end(last) || 0;
                 streamCmd("set_played_get_status", "&time="+(played)).then(ans => {
                     if (ans.err) {
                         if (ans.err === "currently not streaming") {
                             console.error("Attempting reload stream");
                             seek(0);
                         } else {
                             statusError(ans.err);
                         }
                     } else {
                         playStatus = ans;
                         playStatus.set_time = Date.now();
                         if (document.visibilityState === 'visible') {
                             updateDisplay(played);
                         }
                     }
                 }).catch(err => {
                     statusError(err);
                 });
             }
         }

         // Recursive timer loop.
         const progressLoop = () => {
             clearTimeout(progressTimer);
             progressTimer = setTimeout(() => {
                 reportPlayed();
                 progressLoop();
             }, 500);
         }

         // Headphone button actions, may not work perfectly.
         navigator.mediaSession.setActionHandler('previoustrack', () => { skip('prev') });
         navigator.mediaSession.setActionHandler('nexttrack', () => { skip('next') });
         navigator.mediaSession.setActionHandler('pause', () => {
             if (audioElement.paused) { play() }
             else { audioElement.pause() }
         });
         navigator.mediaSession.setActionHandler('play', () => { play() });

         // Start immediately, works on Chrome.
         play();
        </script>
    </body>
</html>

Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home