mirror of
https://github.com/YusufB5/ASCILINE.git
synced 2026-06-14 22:25:13 +02:00
Revert "Fix #7: startup freeze (audio master clock jumps backward on cold start)"
This commit is contained in:
parent
bb0c096ffd
commit
667b412994
2 changed files with 3 additions and 78 deletions
22
app.js
22
app.js
|
|
@ -44,7 +44,6 @@ let selectionBuffer = null;
|
|||
let lastRenderTime = 0;
|
||||
let frameCount = 0, currentFps = 0, lastFpsUpdate = 0;
|
||||
let streamStartTime = 0;
|
||||
let audioClockOffset = null; // anchors the audio clock so it never jumps backward (issue #7)
|
||||
|
||||
const CHAR_LUT = new Array(128);
|
||||
for (let i = 0; i < 128; i++) CHAR_LUT[i] = String.fromCharCode(i);
|
||||
|
|
@ -187,7 +186,6 @@ function connectWebSocket() {
|
|||
streamStartTime = performance.now();
|
||||
lastRenderTime = performance.now();
|
||||
lastFpsUpdate = lastRenderTime;
|
||||
audioClockOffset = null; // re-anchor for this stream
|
||||
requestAnimationFrame(renderFrame);
|
||||
};
|
||||
|
||||
|
|
@ -271,25 +269,11 @@ function renderFrame(now) {
|
|||
requestAnimationFrame(renderFrame);
|
||||
|
||||
// ── MASTER CLOCK LOGIC ──
|
||||
// The audio track is the master clock, BUT it must never jump backward
|
||||
// (issue #7, the cold-start freeze). On a cold load the <audio> element can
|
||||
// start late: the wall-clock fallback has already advanced playback a second
|
||||
// or two, and when audio finally begins, audioEl.currentTime is back near 0.
|
||||
// Snapping the master clock back to ~0 makes every buffered frame read as
|
||||
// "in the future", so renderFrame() returns forever and the stream freezes
|
||||
// until audio catches up. The fix: the first time audio is genuinely playing,
|
||||
// capture the offset between where the wall clock already is and the audio
|
||||
// clock, and add it back — so the clock follows audio's *rate* without ever
|
||||
// moving backward. (When audio starts promptly the offset is ~0.)
|
||||
const wallClock = (now - streamStartTime) / 1000.0;
|
||||
let masterClock;
|
||||
if (audioEl && audioEl.readyState >= 1 && !audioEl.paused && audioEl.currentTime > 0) {
|
||||
if (audioClockOffset === null) {
|
||||
audioClockOffset = Math.max(0, wallClock - audioEl.currentTime);
|
||||
}
|
||||
masterClock = audioEl.currentTime + audioClockOffset;
|
||||
if (audioEl && audioEl.readyState >= 1 && !audioEl.paused) {
|
||||
masterClock = audioEl.currentTime;
|
||||
} else {
|
||||
masterClock = wallClock;
|
||||
masterClock = (now - streamStartTime) / 1000.0;
|
||||
}
|
||||
|
||||
if (frameBuffer.length === 0) return;
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* freeze_repro2.js — Model the COLD-START freeze (issue #7): audio starts late,
|
||||
* so the wall-clock fallback renders ahead, then the master clock snaps back to
|
||||
* audioEl.currentTime (~0) and playback freezes until audio catches up.
|
||||
*
|
||||
* Faithfully simulates: a server feeding frames at fps, a capped jitter buffer,
|
||||
* a 60fps render loop, and the app.js master-clock + frame-gate logic — under
|
||||
* three clock policies.
|
||||
*/
|
||||
function sim({ policy, audioDelayMs }) {
|
||||
const fps = 24, rAF = 1000 / 60, DURATION = 6000;
|
||||
const buf = [];
|
||||
let lastServerFrame = -1, audioStart = null;
|
||||
const renderTimes = [];
|
||||
|
||||
for (let t = 0; t <= DURATION; t += rAF) {
|
||||
// server feeds frames in real time; jitter buffer caps at 20 (drops oldest)
|
||||
const want = Math.floor(t / 1000 * fps);
|
||||
while (lastServerFrame < want) buf.push({ time: ++lastServerFrame / fps });
|
||||
while (buf.length > 20) buf.shift();
|
||||
|
||||
const audioPlaying = t >= audioDelayMs;
|
||||
if (audioPlaying && audioStart === null) audioStart = t;
|
||||
const audioCurrent = audioPlaying ? (t - audioStart) / 1000 : 0;
|
||||
const wall = t / 1000;
|
||||
|
||||
// ── master clock policy ──
|
||||
let master;
|
||||
if (policy === 'broken') master = audioPlaying ? audioCurrent : wall;
|
||||
else if (policy === 'guard') master = (audioPlaying && audioCurrent > 0) ? audioCurrent : wall;
|
||||
else if (policy === 'monotonic') {
|
||||
// fix: anchor audio so the clock never jumps backward
|
||||
if (audioPlaying) {
|
||||
if (sim._off === undefined) sim._off = wall - audioCurrent; // capture once
|
||||
master = audioCurrent + sim._off;
|
||||
} else master = wall;
|
||||
}
|
||||
|
||||
// ── frame gate (verbatim from app.js) ──
|
||||
if (buf.length) {
|
||||
while (buf.length > 1 && buf[0].time < master - 0.1) buf.shift();
|
||||
if (buf[0].time <= master + 0.05) { buf.shift(); renderTimes.push(t); }
|
||||
}
|
||||
}
|
||||
delete sim._off;
|
||||
const last = renderTimes[renderTimes.length - 1] ?? 0;
|
||||
const afterAudio = renderTimes.filter(t => t > audioDelayMs).length;
|
||||
const trailingStallMs = Math.round(DURATION - last); // froze until the end?
|
||||
return { rendered: renderTimes.length, afterAudio, trailingStallMs };
|
||||
}
|
||||
|
||||
const EXPECTED_AFTER = Math.round((6000 - 2000) / 1000 * 24); // ~96 frames in the 2-6s window
|
||||
console.log(`audio starts 2s late (cold start). Expect ~${EXPECTED_AFTER} frames AFTER 2s if smooth:\n`);
|
||||
for (const policy of ['broken', 'guard', 'monotonic']) {
|
||||
const r = sim({ policy, audioDelayMs: 2000 });
|
||||
const frozen = r.afterAudio < 5;
|
||||
console.log(` ${policy.padEnd(10)} total ${String(r.rendered).padStart(3)} | after-audio ${String(r.afterAudio).padStart(3)}/${EXPECTED_AFTER} | `
|
||||
+ (frozen ? `FROZE (stuck ${r.trailingStallMs}ms to end)` : `smooth`));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue