Fix emit resume timing drift across playback and delay phases

This commit is contained in:
Jage9
2026-02-28 03:10:50 -05:00
parent 50ff9bf927
commit f0b97c028c
2 changed files with 57 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
// Maintainer-controlled web client version. // Maintainer-controlled web client version.
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
window.CHGRID_WEB_VERSION = "2026.02.28 R308"; window.CHGRID_WEB_VERSION = "2026.02.28 R309";
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
window.CHGRID_TIME_ZONE = "America/Detroit"; window.CHGRID_TIME_ZONE = "America/Detroit";

View File

@@ -229,13 +229,14 @@ export class ItemEmitRuntime {
const effectiveRate = resumeState.playbackRate > 0 ? resumeState.playbackRate : 1; const effectiveRate = resumeState.playbackRate > 0 ? resumeState.playbackRate : 1;
const durationSeconds = resumeState.durationSeconds; const durationSeconds = resumeState.durationSeconds;
if (durationSeconds && durationSeconds > 0) { if (durationSeconds && durationSeconds > 0) {
const cycleSeconds = durationSeconds + Math.max(0, resumeState.loopDelaySeconds); const loopDelaySeconds = Math.max(0, resumeState.loopDelaySeconds);
const progressed = (resumeState.currentTimeSeconds + elapsedSeconds * effectiveRate) % cycleSeconds; const playWallSeconds = durationSeconds / effectiveRate;
if (progressed < durationSeconds) { const cycleWallSeconds = playWallSeconds + loopDelaySeconds;
const targetTime = Math.min(Math.max(0, progressed), Math.max(0, durationSeconds - 0.01)); const seekAndPlayNow = (targetTimeSeconds: number) => {
const clampedTarget = Math.min(Math.max(0, targetTimeSeconds), Math.max(0, durationSeconds - 0.01));
const applySeek = () => { const applySeek = () => {
try { try {
element.currentTime = targetTime; element.currentTime = clampedTarget;
} catch { } catch {
// Ignore seek failures before metadata is fully available. // Ignore seek failures before metadata is fully available.
} }
@@ -243,9 +244,57 @@ export class ItemEmitRuntime {
applySeek(); applySeek();
element.addEventListener('loadedmetadata', applySeek, { once: true }); element.addEventListener('loadedmetadata', applySeek, { once: true });
this.nextEmitStartAtMs.delete(item.id); this.nextEmitStartAtMs.delete(item.id);
};
const scheduleAfterSeconds = (seconds: number) => {
this.nextEmitStartAtMs.set(item.id, nowMs + Math.max(0, seconds) * 1000);
};
const scheduledStartMs = this.nextEmitStartAtMs.get(item.id);
if (scheduledStartMs !== undefined) {
if (nowMs < scheduledStartMs) {
// Still in delay window tracked while runtime was out of range.
} else if (cycleWallSeconds > 0) {
const sinceStartSeconds = Math.max(0, (nowMs - scheduledStartMs) / 1000);
const inCycleSeconds = sinceStartSeconds % cycleWallSeconds;
if (inCycleSeconds < playWallSeconds) {
seekAndPlayNow(inCycleSeconds * effectiveRate);
} else {
scheduleAfterSeconds(cycleWallSeconds - inCycleSeconds);
}
} else {
seekAndPlayNow(0);
}
} else if (resumeState.wasPlaying) {
const playRemainingWallSeconds = Math.max(0, (durationSeconds - Math.max(0, resumeState.currentTimeSeconds)) / effectiveRate);
if (elapsedSeconds < playRemainingWallSeconds) {
seekAndPlayNow(Math.max(0, resumeState.currentTimeSeconds) + elapsedSeconds * effectiveRate);
} else {
const afterTrackSeconds = elapsedSeconds - playRemainingWallSeconds;
if (cycleWallSeconds > 0) {
const inCycleSeconds = afterTrackSeconds % cycleWallSeconds;
if (inCycleSeconds < loopDelaySeconds) {
scheduleAfterSeconds(loopDelaySeconds - inCycleSeconds);
} else {
seekAndPlayNow((inCycleSeconds - loopDelaySeconds) * effectiveRate);
}
} else {
seekAndPlayNow(0);
}
}
} else { } else {
const delayRemainingSeconds = cycleSeconds - progressed; // Saved while paused/ended with no known schedule: treat as delay-first state.
this.nextEmitStartAtMs.set(item.id, nowMs + delayRemainingSeconds * 1000); if (elapsedSeconds < loopDelaySeconds) {
scheduleAfterSeconds(loopDelaySeconds - elapsedSeconds);
} else if (cycleWallSeconds > 0) {
const afterDelaySeconds = elapsedSeconds - loopDelaySeconds;
const inCycleSeconds = afterDelaySeconds % cycleWallSeconds;
if (inCycleSeconds < playWallSeconds) {
seekAndPlayNow(inCycleSeconds * effectiveRate);
} else {
scheduleAfterSeconds(cycleWallSeconds - inCycleSeconds);
}
} else {
seekAndPlayNow(0);
}
} }
} }
} }