| <script> | |
| (function () { | |
| 'use strict'; | |
| var ctx = null; | |
| var nextTime = 0; | |
| var segNum = 0; | |
| var lastSig = ''; | |
| function log(msg) { | |
| console.log('[StreamPlayer]', msg); | |
| var el = document.getElementById('dbg-log'); | |
| if (el) { | |
| el.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg + ' | |
| ' + el.textContent; | |
| } | |
| } | |
| function getCtx() { | |
| if (!ctx || ctx.state === 'closed') { | |
| ctx = new AudioContext({ sampleRate: 22050 }); | |
| nextTime = 0; | |
| segNum = 0; | |
| log('AudioContext created, state=' + ctx.state); | |
| } | |
| if (ctx.state === 'suspended') { | |
| ctx.resume().then(function() { log('AudioContext resumed'); }).catch(function(e) { log('resume error: ' + e); }); | |
| } | |
| return ctx; | |
| } | |
| function playChunk(rawPath) { | |
| segNum++; | |
| var seg = segNum; | |
| var url = '/gradio_api/file=' + rawPath.replace(/\\/g, '/').replace(/\/g, '/'); | |
| log('seg ' + seg + ' | fetching: ' + url); | |
| var c = getCtx(); | |
| if (c.state === 'suspended') c.resume().catch(function(){}); | |
| fetch(url) | |
| .then(function(r) { | |
| log('seg ' + seg + ' | HTTP ' + r.status); | |
| if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + url); | |
| return r.arrayBuffer(); | |
| }) | |
| .then(function(buf) { | |
| if (buf.byteLength === 0) throw new Error('Received 0 bytes'); | |
| log('seg ' + seg + ' | arrayBuffer ' + buf.byteLength + ' bytes, decoding...'); | |
| return getCtx().decodeAudioData(buf); | |
| }) | |
| .then(function(decoded) { | |
| var ac = getCtx(); | |
| var src = ac.createBufferSource(); | |
| src.buffer = decoded; | |
| src.connect(ac.destination); | |
| var now = ac.currentTime; | |
| var at = (nextTime > now + 0.05) ? nextTime : (now + 0.05); | |
| src.start(at); | |
| nextTime = at + decoded.duration; | |
| log('seg ' + seg + ' | ' + decoded.duration.toFixed(2) + 's | start@' + at.toFixed(3) + ' end@' + nextTime.toFixed(3)); | |
| }) | |
| .catch(function(e) { | |
| log('ERROR seg ' + seg + ': ' + e.message); | |
| console.error('[StreamPlayer]', e); | |
| }); | |
| } | |
| function checkSignal() { | |
| var sig = document.getElementById('sph-sig'); | |
| if (!sig) return; | |
| var val = sig.getAttribute('data-v') || ''; | |
| if (val === lastSig) return; | |
| lastSig = val; | |
| log('signal received: ' + val); | |
| if (!val || val === '__idle__') return; | |
| if (val === '__reset__') { | |
| if (ctx && ctx.state !== 'closed') ctx.close().catch(function(){}); | |
| ctx = null; nextTime = 0; segNum = 0; | |
| log('player reset'); | |
| } else if (val === '__done__') { | |
| log('all chunks sent!'); | |
| } else { | |
| playChunk(val); | |
| } | |
| } | |
| document.addEventListener('click', function() { | |
| if (ctx && ctx.state === 'suspended') ctx.resume().catch(function(){}); | |
| }, true); | |
| setInterval(checkSignal, 50); | |
| log('StreamPlayer JS loaded, polling every 50ms'); | |
| })(); | |
| </script> | |