Fix emit resume timing drift across playback and delay phases
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
} else {
|
||||||
const delayRemainingSeconds = cycleSeconds - progressed;
|
scheduleAfterSeconds(cycleWallSeconds - inCycleSeconds);
|
||||||
this.nextEmitStartAtMs.set(item.id, nowMs + delayRemainingSeconds * 1000);
|
}
|
||||||
|
} 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 {
|
||||||
|
// Saved while paused/ended with no known schedule: treat as delay-first state.
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user