Repository navigation
Description
Describe the bug
In Studio, moving or resizing an audio clip in a static data-no-timeline composition can permanently silence preview audio until the page is refreshed. Editing while playing can also leave the UI showing Pause while the actual transport is stopped.
Reproduced on 0.8.186, upgraded and reproduced again on 0.8.196. Two small local patches to 0.8.196 fixed the observed failures. Both corresponding source paths are still present at main commit 382a14d73e7ab5b77f2c118a012338364c4373b8; main was inspected, not built/tested.
Minimal reproduction
The original reproduction was a 321-second static slideshow with local media, no GSAP timeline, and timed audio. This reduced fixture preserves that triggering structure; the reduced fixture below has not independently been exercised in Studio. The move/trim failures and patched behavior were verified in the original project.
In an empty directory, generate a synthetic source (no private media required):
ffmpeg -y -f lavfi -i "sine=frequency=440:sample_rate=48000:duration=120" -af volume=2 -c:a pcm_s16le tone.wavSave this as index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Static audio timeline-edit reproduction</title>
<style>
html, body { margin: 0; width: 1920px; height: 1080px; background: #18202a; }
#root { position: relative; width: 1920px; height: 1080px; overflow: hidden; }
#card { position: absolute; inset: 80px; background: #d8f3dc;
display: grid; place-items: center; font: 64px sans-serif; }
</style>
</head>
<body>
<div id="root" data-composition-id="main" data-no-timeline
data-start="0" data-duration="321" data-width="1920" data-height="1080">
<div id="card" class="clip" data-start="0" data-duration="321"
data-track-index="0">Static composition — audio edit test</div>
<audio id="music" src="tone.wav" data-start="64.4"
data-duration="29.653" data-media-start="78.547"
data-track-index="2" data-volume="0.5"></audio>
</div>
</body>
</html>Run the unmodified package from that directory:
npx --yes [email protected] previewSteps to reproduce
- Open the composition in Studio, seek to about 70 seconds, enable audio meters, and play. Confirm nonzero output.
- Pause. Drag the audio clip body by roughly 2 seconds, keeping 70 seconds inside its new window. Play again.
- Repeat from a fresh page, shortening the clip using its right edge instead of moving it.
- Separately test both edits while playback is running.
- Keep the root duration at 321 seconds; do not register a GSAP timeline.
Expected behavior
- Moving/resizing preserves the declared composition duration.
- Playback after a paused edit produces audio.
- An edit performed during playback resumes the previously playing transport; an edit made while paused stays paused.
- The visible Play/Pause state matches the actual transport.
Actual behavior and diagnostic
In the original project, moving or trimming made the output meters stay at zero. Pause/Play did not recover the unpatched failure; reloading restored playback. The source file existed, the source window was valid, the track/element/monitor were unmuted, and the affected test clip had no automation.
The console reported:
[useTimelinePlayer] Selected adapter duration (0s) does not cover the document duration (321s); falling back to seek-driven playback, which never starts media elements or WebAudio. Audio will not play in preview. Extend the GSAP timeline to cover the declared data-duration.
Adding an artificial GSAP animation should not be necessary for a static composition.
Root cause and proposed fixes
1. No GSAP timeline is mistaken for a zero-length composition
reconcileTimeline in core/runtime/init.ts clears the clock duration whenever resolution.timeline is absent. That is a valid state for this static composition. A subsequent adapter selection sees 0 seconds and downgrades to seek-only playback, which does not start audio.
Keep clearing the stale timeline reference, but resolve the duration from the composition contract instead of hardcoding zero:
if (!resolution.timeline) {
state.capturedTimeline = null;
childrenBound = false;
- clock.setDuration(0);
+ clock.setDuration(getSafeTimelineDurationSeconds(null, 0));
syncTimedElementVisibility(state.currentTime);
return;
}This retains declared duration/media/adaptor duration inference without retaining a killed GSAP timeline; a genuinely empty composition can still resolve to zero.
2. Edit finalization seeks (pauses) without restoring playback
finalizeSoftReload in studio/utils/gsapSoftReload.ts calls __player.seek(currentTime), then rebinds and reapplies edits. Seek pauses the transport. Preserving the previous playing state fixes the remaining playing-edit freeze:
function finalizeSoftReload(win: IframeWindow, currentTime: number): void {
const player = win.__player;
const wasPlaying = player?.isPlaying?.();
player?.seek?.(currentTime);
win.__hfForceTimelineRebind?.();
win.__hfStudioManualEditsApply?.();
if (wasPlaying) player?.play?.();
}These are source-level equivalents of the local patches tested against the published 0.8.196 bundles, not an upstream PR or a full upstream-suite result. Related context: #4151 describes keeping playback running after a drop; the static/no-GSAP path here still exhibits the failure.
Validation performed
After applying both fixes locally:
- Paused move -> Play: meters produce sustained output and playhead advances.
- Paused right-edge trim -> Play: sustained output.
- Move while playing: audio/playhead automatically resume.
- Right-edge trim while playing: audio/playhead automatically resume.
- Verified sustained meters across subsequent observations, not just the first frame after an edit.
- Small assert-based checks exercised the actual patched runtime rebind branch for declared length 321 and empty length 0, and finalization for previously playing/paused states.
- Full
hyperframes checkpassed (0 errors; one pre-existing dense-track warning). The original project's authored parameters were restored afterward.
Suggested upstream regression tests: static declared-duration rebind, genuinely empty duration, paused edit staying paused, and playing move/trim maintaining native audio output without the static-seek downgrade.
Environment
Sanitized doctor --json / local environment:
- HyperFrames: 0.8.196 (doctor reported latest); initially reproduced on 0.8.186.
- Windows x64; Node.js v24.11.0.
- Studio served locally; Chromium-based embedded browser.
- Headless Chrome 152.0.7977.30 available for CLI checks.
- FFmpeg/FFprobe were not on PATH in doctor; project-local binaries were available. This report concerns live preview timeline editing, not an encoding failure.
No personal photos, music files, machine paths, credentials, or project archive are attached.
Activity
miguel-heygen commentedon Oct 25, 2026
Thanks for the thorough report and for narrowing it to two causes.
I haven't run the reduced fixture in Studio myself. I did check the source on current main, and both spots are as you describe:
packages/core/src/runtime/init.ts:2248sets the clock duration to 0 as soon as the timeline is cleared, so a staticdata-no-timelinecomposition ends up with the adapter duration of 0 seconds and the seek-only downgrade you saw in the console.packages/studio/src/utils/gsapSoftReload.ts:254(finalizeSoftReload) callsseek()and never restores play, which matches the paused-after-edit state during playback.Treating this as a confirmed bug on the source reading. Two fixes in two places is the right split.
You mentioned you have both patches working. A PR is welcome. Please send them as one PR, or two if you prefer, with these tests:
packages/core/src/runtime/init.rootLength.test.ts(or a newinit.*.test.tsnext to it): a static composition with a declared duration keeps that duration after the timeline is cleared, and an empty one still resolves to 0.packages/studio/src/utils/gsapSoftReload.test.ts: finalization resumes playback when it was playing, and stays paused when it wasn't.Each test should fail without its fix.