fix(core): the picture keeps playing while a just-started take's audio catches up

This commit is contained in:
Miguel Angel Simon Sierra
2026-10-29 05:57:52 -04:00
parent 5d437d5eec
commit f614d52e94
3 changed files with 31 additions and 4 deletions
+27 -1
View File
@@ -353,6 +353,32 @@ describe("TransportClock", () => {
expect(clock.now()).toBeCloseTo(6.37, 5);
});
it("runs the playhead past a just-started element frozen 300 ms behind it, as iOS leaves it", () => {
const { clock, advance } = createClock({ duration: 30 });
clock.play();
advancePolled(clock, advance, 6300);
const take = createMockAudioEl(0, false); // playing, its time frozen at its start
clock.attachAudioSource({ el: take, compositionStart: 6, mediaStart: 0 });
let previous = clock.now();
for (let i = 0; i < 20; i++) {
advance(16);
const now = clock.now();
expect(now).toBeGreaterThan(previous); // never held, though the element has not moved
previous = now;
}
expect(previous).toBeCloseTo(6.3 + 0.32, 1);
});
it("holds for an element no more than the steer limit behind, so a small lag is not seeked", () => {
const { clock, advance } = createClock({ duration: 30 });
clock.play();
advancePolled(clock, advance, 6300);
const take = createMockAudioEl(0, false); // 50 ms behind the playhead
clock.attachAudioSource({ el: take, compositionStart: 6.25, mediaStart: 0 });
advance(100);
expect(clock.now()).toBeCloseTo(6.3, 5);
});
it("holds the playhead while its element seeks, even to a time ahead", () => {
const { clock, advance } = createClock({ duration: 30 });
clock.play();
@@ -400,7 +426,7 @@ describe("TransportClock", () => {
clock.play();
advancePolled(clock, advance, 2000);
const bed = Object.assign(createMockAudioEl(0, false), { loop: true, duration: 8 });
clock.attachAudioSource({ el: bed, compositionStart: 1.9, mediaStart: 0 });
clock.attachAudioSource({ el: bed, compositionStart: 1.95, mediaStart: 0 });
advance(16);
expect(clock.now()).toBeCloseTo(2, 5);
});
+3 -2
View File
@@ -1,5 +1,5 @@
import { timeAtSourceTime, type RateSpec } from "../speedRamp.js";
import { MEDIA_HARD_SYNC_SECONDS } from "./mediaSteer.js";
import { AUDIO_STEER_LIMIT_SECONDS } from "./mediaSteer.js";
import { playRangeHoldTime } from "./protocol.js";
export type TransportClockSnapshot = {
@@ -99,7 +99,8 @@ export class TransportClock {
private _heldTimeWhenBehind(el: HTMLMediaElement, behind: number, spec: RateSpec): number | null {
if (el.loop && behind > timeAtSourceTime(spec, el.duration) / 2) return null;
const buffering = el.readyState < HAVE_FUTURE_DATA;
return buffering || behind <= MEDIA_HARD_SYNC_SECONDS ? this._lastNow : null;
// Past the steer limit the sync seeks the audio onto the playhead, so the picture does not wait for it.
return buffering || behind <= AUDIO_STEER_LIMIT_SECONDS ? this._lastNow : null;
}
/** Folds a >500ms gap since the last read into `_playStartMs` so it's never reported as played time. */
+1 -1
View File
@@ -11,7 +11,7 @@ const MEDIA_STEER_STEPS: readonly (readonly [lag: number, steer: number])[] = [
const MEDIA_STEER_RELEASE_SECONDS = 0.01;
// Audio never steers past 10%, where speech changes tempo; a lag held past 100 ms is seeked instead.
const AUDIO_MAX_STEER = 0.1;
const AUDIO_STEER_LIMIT_SECONDS = 0.1;
export const AUDIO_STEER_LIMIT_SECONDS = 0.1;
/** Direction (+1 fast, -1 slow) an element is being steered in; absent when it plays at its authored rate. */
const steering = new WeakMap<HTMLMediaElement, number>();