Skip to content

Ask ffmpeg for the frames the capturer actually wrote - #284

Merged
ralyodio merged 1 commit into
masterfrom
fix-gif-frame-pattern
Sep 24, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix-gif-frame-pattern

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Every animated banner encode failed in production. Caught by queueing one real render after the deploy and reading the worker log rather than assuming green CI meant working.

[worker] video render vr-56af1109… failed: validation failed: animated banners
expected three units, got Command failed: ffmpeg -y -framerate 12.5
  -i /tmp/ad-video-7CsTgp/gif_300x250-frames/frame-%04d.png …
[image2] Could find no file with path '…/frame-%04d.png' and index in the range 0-4

The frame pattern was written down twice and drifted

The capturer emits f-0000.png. The GIF encoder asked ffmpeg for frame-0000.png. There is an exported FRAME_PATTERN constant for exactly this reason and the GIF path should have used it — it does now.

Three things failed to catch it, and all three are fixed

The guard counted files instead of checking names. Fifty PNGs were present; none was named what ffmpeg was asked for, so captured.length !== GIF_FRAMES passed happily and the mismatch surfaced inside ffmpeg instead. It now verifies each expected filename is present and names the first one missing.

The test wrote the encoder's spelling. Its synthetic capturer used frame-%04d.png — the same wrong literal — so two hardcoded copies agreed with each other and disagreed with production. Both now derive from FRAME_PATTERN, which is what makes the test capable of catching this class of bug at all.

A failed banner failed the whole revision. The comment beside that catch claimed it wouldn't. Everything pushed onto problems fails the render, so three renders of a perfectly good pre-roll were thrown away over a GIF. The failure is now logged and the revision proceeds; the banners are optional profiles and their absence is already visible in the asset list.

That third one is the more serious of the two bugs: it means my "a banner must not fail the video" design existed only in prose.

Verification

2563 passed / 1 failed repo-wide — the pre-existing tracker-geo failure (mmdb not installed locally). Typechecks clean.

CI cannot prove this one end to end, since the real capturer needs Chromium. I'll queue a live render after this deploys and confirm three GIFs land before reporting the feature as working — and look at them.

Every animated banner encode failed in production. The frame filename pattern
was written down twice and the copies drifted: the capturer emits f-0000.png,
and the GIF encoder asked ffmpeg for frame-0000.png, so palettegen exited with
"Could find no file with path" on the first unit of every render.

There is an exported FRAME_PATTERN for exactly this reason and the GIF path
should have used it. It does now.

The guard that existed to catch this did not, because it counted PNG files
rather than looking at their names — fifty files were present, none of them
named what ffmpeg was asked for. It now checks that each expected filename is
present and names the first one missing, so the same class of mistake fails
here with a sentence that says what happened rather than inside ffmpeg.

The test could not have caught it either: its synthetic capturer wrote the
encoder's spelling instead of the real one, which is how a divergence between
two hardcoded copies passed CI. It derives both from FRAME_PATTERN now.

Separately, a failed banner was failing the whole revision. The comment beside
that catch claimed it would not — everything pushed onto `problems` fails the
render, and the video for this campaign was fine. Three renders of a perfectly
good pre-roll were thrown away over a GIF. The failure is logged and the
revision proceeds; the banners are optional profiles and their absence is
already visible in the asset list.

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 384cddf 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