Skip to content

render: clamp composite output to the base video duration - #123

Open
eldermoraes wants to merge 1 commit into
browser-use:mainfrom
eldermoraes:fix/clamp-composite-duration
Open

render: clamp composite output to the base video duration#123
eldermoraes wants to merge 1 commit into
browser-use:mainfrom
eldermoraes:fix/clamp-composite-duration

Conversation

@eldermoraes

@eldermoraes eldermoraes commented Aug 8, 2026

Copy link
Copy Markdown

Problem

In build_final_composite(), overlay's framesync extends the output whenever an overlay input outlasts the base video: the last base frame is frozen and repeated until the overlay ends. An overlay even slightly longer than the base — e.g. a caption layer with a trailing hold on its last page — silently stretches the final video with frozen frames at the end.

Change

Probe the base duration with ffprobe and pass -t <base_dur> to the composite command. Falls back to previous behavior if the probe fails, so the failure mode is the status quo.

How it was found

Compositing a caption overlay (68.98s, rendered with a 300ms hold after the last word) over a 68.73s base: the output came out 250ms longer than the EDL's total_duration_s, with the last frame frozen.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HTd52WxWgWPYeakcJAiAnR


Summary by cubic

Clamp the final composite to the base video’s duration to prevent frozen-frame tails when an overlay outlasts the base. We probe the base duration with ffprobe and pass -t to ffmpeg; if probing fails, we keep the previous behavior.

Written for commit 44be066. Summary will update on new commits.

Review in cubic

In build_final_composite(), overlay's framesync extends the output
whenever an overlay input outlasts the base video: the last base frame is
frozen and repeated until the overlay ends. An overlay layer that is even
slightly longer (e.g. a caption layer with a trailing hold on its last
page) silently stretches the final video with frozen frames.

Probe the base duration and pass -t to the composite command. Falls back
to previous behavior when the probe fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="helpers/render.py">

<violation number="1" location="helpers/render.py:567">
P3: The broad `except Exception: pass` silently swallows every probe failure, so if ffprobe is unavailable, returns no video-stream duration, or `-select_streams v:0` matches nothing, the clamp silently reverts to the old frozen-frame-extension behavior with zero indication. Since this whole feature depends on that probe succeeding, a warning log (e.g. `warnings.warn` / print) would make regressions much more diagnosable. Consider narrowing the catch to expected failures and logging the fallback.</violation>

<violation number="2" location="helpers/render.py:579">
P2: The new `-t base_dur` clamps the whole output, not just the video. Because the audio is copied (`-c:a copy`), it is truncated at packet boundaries rather than exactly at `base_dur`, so the copied audio can end a few ms before/after the clamped video (small end-of-file A/V duration mismatch). And since the clamp uses the base VIDEO stream duration only, any audio that extends beyond the video stream (e.g. a short trailing silence or AAC priming tail) is silently cut off. Consider clamping only the video stream so the copied audio isn't truncated away, or verify/round the clamp so audio packets are not dropped.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread helpers/render.py
"-c:v", "libx264", "-preset", "fast", "-crf", "18",
"-pix_fmt", "yuv420p",
"-c:a", "copy",
*(["-t", f"{base_dur:.3f}"] if base_dur else []),

@cubic-dev-ai cubic-dev-ai Bot Aug 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new -t base_dur clamps the whole output, not just the video. Because the audio is copied (-c:a copy), it is truncated at packet boundaries rather than exactly at base_dur, so the copied audio can end a few ms before/after the clamped video (small end-of-file A/V duration mismatch). And since the clamp uses the base VIDEO stream duration only, any audio that extends beyond the video stream (e.g. a short trailing silence or AAC priming tail) is silently cut off. Consider clamping only the video stream so the copied audio isn't truncated away, or verify/round the clamp so audio packets are not dropped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 579:

<comment>The new `-t base_dur` clamps the whole output, not just the video. Because the audio is copied (`-c:a copy`), it is truncated at packet boundaries rather than exactly at `base_dur`, so the copied audio can end a few ms before/after the clamped video (small end-of-file A/V duration mismatch). And since the clamp uses the base VIDEO stream duration only, any audio that extends beyond the video stream (e.g. a short trailing silence or AAC priming tail) is silently cut off. Consider clamping only the video stream so the copied audio isn't truncated away, or verify/round the clamp so audio packets are not dropped.</comment>

<file context>
@@ -561,6 +576,7 @@ def build_final_composite(
         "-c:v", "libx264", "-preset", "fast", "-crf", "18",
         "-pix_fmt", "yuv420p",
         "-c:a", "copy",
+        *(["-t", f"{base_dur:.3f}"] if base_dur else []),
         "-movflags", "+faststart",
         str(out_path),
</file context>
Fix with cubic

Comment thread helpers/render.py
capture_output=True, text=True, check=True,
)
base_dur = float(out.stdout.strip().splitlines()[0])
except Exception:

@cubic-dev-ai cubic-dev-ai Bot Aug 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The broad except Exception: pass silently swallows every probe failure, so if ffprobe is unavailable, returns no video-stream duration, or -select_streams v:0 matches nothing, the clamp silently reverts to the old frozen-frame-extension behavior with zero indication. Since this whole feature depends on that probe succeeding, a warning log (e.g. warnings.warn / print) would make regressions much more diagnosable. Consider narrowing the catch to expected failures and logging the fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 567:

<comment>The broad `except Exception: pass` silently swallows every probe failure, so if ffprobe is unavailable, returns no video-stream duration, or `-select_streams v:0` matches nothing, the clamp silently reverts to the old frozen-frame-extension behavior with zero indication. Since this whole feature depends on that probe succeeding, a warning log (e.g. `warnings.warn` / print) would make regressions much more diagnosable. Consider narrowing the catch to expected failures and logging the fallback.</comment>

<file context>
@@ -552,6 +552,21 @@ def build_final_composite(
+            capture_output=True, text=True, check=True,
+        )
+        base_dur = float(out.stdout.strip().splitlines()[0])
+    except Exception:
+        pass
+
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant