Skip to content

Stop -frames:v from cutting the audio short - #299

Merged
ralyodio merged 1 commit into
masterfrom
audiofix
Sep 24, 2026
Merged

ralyodio merged 1 commit into
masterfrom
audiofix

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Every narrated render failed validation with audio around 3.3 of 5 seconds. The cause was not the padding.

-frames:v ends the whole output the moment the video stream reaches its limit, which cuts the audio wherever the encoder happened to have flushed to.

Why none of the obvious fixes worked

Reproduced inside the deployed container, one at a time:

Attempt Audio duration
-af apad -shortest 2.346s
-af apad -t 5 3.306s
-af "apad,atrim=0:5,asetpts=N/SR/TB" 3.306s
-af apad=whole_dur=5 3.306s
input WAV already exactly 5.000s 3.264s
no -frames:v, -af apad -t 5 5.000s ✓

The fifth row is the one that settles it: an input already padded to exactly five seconds still came out short. The padding was fine the whole time — the output was being terminated early.

The change

An encode that carries sound is bounded by duration instead. The frame count is still exactly 150, because 150 frames at 30fps is five seconds — the guarantee now comes from arithmetic rather than a flag, and validation counts the frames either way (the test asserts 150 decoded frames).

Silent encodes keep -frames:v: they have no audio to truncate, and a frame count cannot be satisfied by a timebase rounding error the way a duration can. That distinction is now tested in both directions.

-shortest goes from the music-bed path too — with both streams bounded to the same explicit length, there is no shorter one to find. I updated #293's assertion rather than deleting it, with the reasoning.

Why this took several attempts, honestly

The behaviour is version dependent. On ffmpeg 8 — what this repo's suite runs against locally — the narrated encode produces a correct 5.000s track, and the test I added for it passes. On ffmpeg 4.4.2, which is what ships in the image, the same arguments produce 3.306s.

So a green local suite was not evidence, and I treated it as evidence twice. This was finally found by running ffmpeg inside the deployed container and bisecting the arguments.

Verification

2590 passed / 1 failed repo-wide — the pre-existing tracker-geo failure. Typechecks clean. The decisive check is on dev2 after deploy, against 4.4.2, and I'll report the probed result rather than the suite's.

Every narrated render failed validation with audio around 3.3 of 5 seconds, and
the cause was not the padding. -frames:v ends the whole output the moment the
video stream reaches its limit, which cuts the audio wherever the encoder
happened to have flushed to.

That is why none of the obvious fixes worked. apad, apad=whole_dur=5, atrim=0:5
with asetpts, -shortest: all of them left the audio short, and an input already
padded to exactly 5.000s still came out at 3.264s. The padding was fine the
whole time; the output was being terminated early.

An encode that carries sound is now bounded by duration instead. The frame
count is still exactly 150, because 150 frames at 30fps is five seconds — the
guarantee now comes from arithmetic rather than from a flag, and validation
counts the frames either way. Silent encodes keep -frames:v, since they have no
audio to truncate and a frame count cannot be satisfied by a timebase rounding
error the way a duration can.

-shortest goes from the music path as well: with both streams bounded to the
same explicit length there is no shorter one to find.

Worth recording why this took so long. The behaviour is version dependent: on
ffmpeg 8, which is what this repo's test suite runs against locally, the
narrated encode produces a correct 5.000s track and the test I added passes. On
ffmpeg 4.4.2, which is what ships in the image, the same arguments produce
3.306s. A test that runs only on the newer binary cannot catch this, so it was
reproduced by running ffmpeg inside the deployed container.

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

Copy link
Copy Markdown

ThreatCrush Security Scan

48 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM js-dynamic-code-execution lib/crawl-limits.ts:67
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:14
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:24
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:20
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:24
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:25
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:26
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:31
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:35
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 2c524f1 into master Sep 24, 2026
10 checks passed
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