mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 19:06:04 +00:00
fix(core,player,studio): bound trimmed audio playback to the clip window (#1430)
* fix(player): bound the parent audio proxy to its clip window When iframe autoplay is blocked, audible playback is promoted to a parent-frame audio proxy. The proxy read the clip's data-start/data-duration once at adopt time and mirrorTime() only skipped (never paused) the element outside that window — so a trimmed/moved music clip kept playing the full source past its on-timeline end, even though the iframe element was correctly paused. Fix: the proxy keeps a reference to its source iframe element and re-reads data-start/data-duration each mirror tick (live trims/moves apply), pauses the proxy when the playhead leaves [start, start+duration), and resumes it when the playhead re-enters during parent-owned playback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> * fix(core,studio): bound trimmed audio playback to the clip window Trimmed audio played to the source file's natural end instead of stopping at the clip edge, on every audio path: - WebAudio (the audible path in Studio): schedulePlayback now passes the clip's data-duration as the third start() arg, so the decoded buffer stops at the trimmed edge instead of running to the file end. - Runtime element gating: the duration resolver caps each clip by its own data-duration (min of source length, host window, authored duration), so a trimmed <audio>/<video> element pauses at its edge. Studio trim UX: - Resize live-patches the media-start/playback-start offset, so a start-edge drag trims into the source instead of only repositioning the clip. - AudioWaveform windows the rendered peaks to the trimmed slice so the waveform tracks the clip edges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> * fix(player,core): gate proxy playback to the live clip window Review follow-ups on the parent-audio-proxy / WebAudio bound: - seekAll now re-reads live source bounds (_refreshEntryBounds) before gating, so a paused scrub right after a trim/move uses the current clip window instead of the adopt-time one. - playAll and clip adoption only start a proxy when the playhead is inside the clip's window (_playEntryIfActive), so bulk starts / promotion no longer blip audio for clips outside their window until the next tick. - The WebAudio buffer is now bounded by the host-composition window too (matching resolveDurationSeconds), so a sub-composition-nested clip stops at the same edge on the WebAudio and HTMLMedia paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> * fix(core,player): reschedule bounded WebAudio on rate change; guard NaN bounds A bounded WebAudio source's wall-clock length is baked into start()'s duration arg (in buffer-sample seconds) at its scheduling rate. Mutating playbackRate in place on a later rate change does not rescale that bound, so a trimmed clip ends early (fast) or late (slow). setRate now reports whether the rate changed and exposes hasBoundedActiveSources(); the runtime stopAll()+reschedules active clips at the new rate when any bounded source is live. The per-clip schedule loop is extracted to a shared closure so play() and the rate path agree. Also guard _refreshEntryBounds against a non-numeric duration attribute parsing to NaN, which would make every window check false and let the proxy play past its clip end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> --------- Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
Miguel Ángel
parent
5c8b637369
commit
a95e49dbda
@@ -171,6 +171,43 @@ describe("WebAudioTransport", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("clip duration bound (trim)", () => {
|
||||
it("bounds an in-progress clip to its remaining authored window", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
// compStart=5, mediaStart=0, compTime=8 → elapsed=3; clipDuration=10 → 7 left
|
||||
await transport.schedulePlayback(mockEl, mockBuffer, 5, 0, 8, 1, gen, 1, 10);
|
||||
expect(mock.startFn).toHaveBeenCalledWith(0, 3, 7);
|
||||
});
|
||||
|
||||
it("bounds a future clip to its full authored window", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
// compStart=10, mediaStart=1.5, compTime=2 → elapsed=-8 → delay 8; clipDuration=4
|
||||
await transport.schedulePlayback(mockEl, mockBuffer, 10, 1.5, 2, 1, gen, 1, 4);
|
||||
expect(mock.startFn).toHaveBeenCalledWith(108, 1.5, 4);
|
||||
});
|
||||
|
||||
it("does not schedule a clip whose window has already elapsed", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
// elapsed=15 > clipDuration=10 → nothing to play
|
||||
const result = await transport.schedulePlayback(mockEl, mockBuffer, 5, 0, 20, 1, gen, 1, 10);
|
||||
expect(result).toBeNull();
|
||||
expect(mock.startFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("scales the bound by playback rate (buffer seconds)", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
// rate=2, clipDuration=10 → clipSourceLen=20; elapsed=3 → 17 buffer seconds left
|
||||
await transport.schedulePlayback(mockEl, mockBuffer, 5, 0, 8, 1, gen, 2, 10);
|
||||
expect(mock.startFn).toHaveBeenCalledWith(0, 3, 17);
|
||||
});
|
||||
|
||||
it("plays unbounded when clipDuration is omitted (legacy behavior)", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
await transport.schedulePlayback(mockEl, mockBuffer, 5, 0, 8, 1, gen);
|
||||
expect(mock.startFn).toHaveBeenCalledWith(0, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("playback rate", () => {
|
||||
it("sets sourceNode.playbackRate.value when rate is provided", async () => {
|
||||
const { transport, mock, gen } = setupTransport(100);
|
||||
|
||||
Reference in New Issue
Block a user