fix(producer): decode percent-encoded video src in HDR pre-extract - #2759
fix(producer): decode percent-encoded video src in HDR pre-extract#2759vanceingalls wants to merge 3 commits into
Conversation
miga-heygen
left a comment
There was a problem hiding this comment.
SSOT — duplicated path resolution eliminated
Textbook SSOT fix. The HDR planner had a hand-rolled path join that duplicated the decision resolveProjectRelativeSrc already owns — same structure (compiledDir → projectDir fallback) but missing the percent-decode pass. The fix deletes the duplicate and delegates to the canonical resolver.
Verified
resolveProjectRelativeSrcsignature:(src, baseDir, compiledDir?)— matches the call site(v.src, projectDir, compiledDir). ✓- Absolute path handling: the shared resolver preserves absolute paths that exist on disk, treats non-existent absolute paths as origin-root URLs. The old code's
startsWith("/")passthrough was less robust (passed through even for non-existent absolute paths). Behavioral improvement, not regression. ✓ - Fallback on missing file:
join(baseDir, cleanSrc)— same as old behavior minus query strings (which the old code also didn't strip, so this is strictly better). ✓ existsSyncremoval: still used at line 328 incaptureHdrStage.ts— no dead import. ✓- Tests use real filesystem (
mkdtempSync+writeFileSync), not mocks. ✓
CI
Producer: integration tests was cancelled (not failed) — may need a re-run before merge. Unit tests passed.
Clean fix, good tests, SSOT improvement. Ship it.
— Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 0fcf792.
Correctly shaped fix — replaces the divergent hand-rolled join with the shared resolveProjectRelativeSrc, which is exactly the SDR-path resolver whose absence caused the HDR-only regression. Test uses a real CJK literal ("视频1.mp4") with a real tmpdir round-trip, not a %E4%B8%AD proxy, so the encode→decode→existsSync chain is exercised end-to-end. existsSync param removed cleanly from both planHdrResources and the runCaptureHdrStage call site. Root-cause explanation in the PR body is accurate — I traced decodeUrlPathVariants (returns [decoded, original]), resolveProjectRelativeSrc's candidate loop (compiledDir → baseDir, plus browser-style .. clamp), and confirmed the shape matches the video-probe callsite at extractVideosStage.ts:148-150 that this fix now brings HDR into parity with.
One concern — peer image-probe divergence in the same file:
packages/producer/src/services/render/stages/extractVideosStage.ts:173-179 still has the exact pre-fix hand-rolled pattern for the HDR image probe:
let imgPath = img.src;
if (!imgPath.startsWith("/")) {
const fromCompiled = existsSync(join(compiledDir, imgPath))
? join(compiledDir, imgPath)
: join(projectDir, imgPath);
imgPath = fromCompiled;
}Same bug reproduces for HDR image sources — a 16-bit PNG tagged BT.2020 PQ/HLG named 图1.png compiles to %E5%9B%BE1.png in the DOM URL attribute, join(projectDir, encoded) misses, existsSync(imgPath) at line 180 returns false → return null → the image never enters nativeHdrImageIds → HDR pipeline sees no HDR sources → composition renders through the SDR fallback with silently wrong color (the customer sees a rendered output, no error, wrong appearance). That's arguably worse than the video symptom, which at least errored out audibly at ffmpeg.
The video probe two blocks above (lines 148-150) already uses isAbsolute(v.src) ? v.src : resolveProjectRelativeSrc(v.src, projectDir, compiledDir) — copying that exact shape to the image loop is ~4 lines. Would strongly recommend either bundling it into this PR (natural cohesive fix, matches the "one root cause" framing) or filing a same-day follow-up before PRINFRA-349 symptom (c) is marked closed — otherwise the ticket claims a fix that doesn't cover HDR image compositions.
Nits:
-
NFC/NFD coverage: test uses the NFC-normalized literal
视频1.mp4on Linux CI where filesystem stores raw UTF-8 bytes — round-trip works because JS strings are already NFC. Won't catch NFC-vs-NFD divergence (macOS HFS+ normalizes to NFD on write, APFS doesn't). Not a production concern for the Linux-based render pipeline, but if user-machine renders on macOS ever land the same code path, the shared resolver'sdecodeURIComponentproduces NFC while a stored NFD filename would miss theexistsSynccheck. Worth documenting as an assumption; anormalize("NFC")/normalize("NFD")variant indecodeUrlPathVariantswould harden it if that scope ever expands (would live inpackages/parsers/src/utils/urlPath.ts, not this PR). -
CANCELLED integration test: the "Producer: integration tests" job in CI came back
cancelledafter 20m — worth confirming that's a matrix-cascade cancel from another failing check, not this fix hitting a real timeout in an integration path. All other CI (Producer unit, Windows render, all 8 regression shards includinghdr-regressionandhdr-hlg-regression, Windows tests, CodeQL, typecheck) is green.
Scope note is well-drawn: honestly separating symptom (c) — this cross-platform URL-decode bug — from symptoms (a)/(b) — Windows-native init issues needing Windows repro — is the right shape. The PR fixes what it claims and doesn't overreach.
What I didn't verify:
- Whether
img.srcin production carriesfile://prefix (would break both old and new code) vs relative path (works with the shared resolver). Same question applies tov.srcfor HDR — the test assumes relative path, which matches Vance's reproduction report, but the composition-metadata build path could be a hidden divergence. - Behavior when
resolveProjectRelativeSrchits the total-miss fallback (line 63 invideoFrameExtractor.ts) — it returnsjoin(baseDir, cleanSrc)with the encoded string, so the ffmpeg error path still shows the encoded name. Not a regression; inherited from the shared resolver. - Cross-check with the audio path —
packages/producer/src/services/audioExtractor.ts:276has a similarly-shapedstartsWith("/") && startsWith("http")guard that I didn't fully trace; if it also hand-rolls, CJK audio-track paths would be a peer symptom.
Stamp routing per convention. Leaving as COMMENTED.
miguel-heygen
left a comment
There was a problem hiding this comment.
Exact head 0fcf7928fe9096413b85a417a87a3562fbd2c844. The video-path SSOT change itself is correct: planHdrResources now delegates to the same resolver as SDR, the CJK test uses a real on-disk filename, and the current-main resolver still preserves the intended compiled-dir/base-dir/decode ordering. The branch also merges conflict-free with current main.
Blocker — the same HDR media-path contract is still violated for images. As Rames D Jusso noted, packages/producer/src/services/render/stages/extractVideosStage.ts:342-348 still resolves img.src with the old hand-rolled startsWith("/") + existsSync(join(...)) pattern. A compiled HDR image named 图1.png arrives percent-encoded, misses both filesystem joins, and silently fails HDR detection/materialization. This is the same precondition and failure class as the video bug, inside the same HDR pipeline; per the shared-boundary audit it should be fixed here, not left as a follow-up. Route that image path through resolveProjectRelativeSrc(img.src, projectDir, compiledDir) and add the real-filesystem percent-encoded HDR-image regression alongside the video case.
CI also is not currently stampable: required Producer: integration tests is cancelled. The log shows it hit the job’s ~20-minute limit while apt-get install ffmpeg was still downloading (the tests never started), so this is infrastructure rather than a code failure, but the required lane needs a successful rerun before merge.
Verdict: REQUEST CHANGES
Reasoning: The canonical resolver fix is right but leaves a sibling HDR source type with the identical CJK failure, and one required lane has no test result.
— Magi
The HDR image probe still hand-rolled the path join the video probe had already delegated to resolveProjectRelativeSrc, so a percent-encoded non-ASCII `<img src>` (`图1.png` -> `%E5%9B%BE1.png`) never resolved: the image never entered nativeHdrImageIds, resolveEffectiveHdrMode saw no HDR sources, and the composition rendered through the SDR fallback with wrong color -- silently, unlike the video path which errored at ffmpeg. Both probes now call resolveProjectRelativeSrc directly, with no isAbsolute() pre-check. The resolver already returns an absolute path that exists and otherwise treats a leading slash as a browser origin-root URL, so a pre-check would hand back `/assets/%E5%9B%BE1.png` undecoded and re-open the same bug for root-relative srcs. This matches planHdrResources, so the two halves of the fix can no longer disagree. Widening resolution also makes previously-unresolvable files reachable for the first time, including truncated or 0-byte assets on which ffprobe exits non-zero. These probes run inside a bare Promise.all, so an unguarded throw aborted the whole render over one unreadable image; probeColorSpaceSafely now logs and treats such a source as SDR. Tests cover percent-encoded CJK, origin-root percent-encoded CJK, compiledDir-over-projectDir precedence, and existing-absolute passthrough, with distinct projectDir/compiledDir so the precedence is actually pinned. Fault-injection verified: reintroducing the isAbsolute short-circuit fails the origin-root test. Refs PRINFRA-349 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Good catch on the image probe — fixed in c8f7542, and chasing it turned up a deeper case worth flagging. Your finding: the HDR image probe still hand-rolled the join. Fixed — it now goes through the shared resolver, so The deeper case: my first pass extracted a Final shape: helper deleted, both probes call One more thing the widening exposed: resolving more srcs means files that previously never resolved are now reachable, including truncated / 0-byte assets where ffprobe exits non-zero. Both probes run inside a bare Tests: four cases (percent-encoded CJK, origin-root percent-encoded CJK, compiledDir-over-projectDir precedence, existing-absolute passthrough), with distinct Known gap, not fixed: the image-probe call site still has no regression coverage — |
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
R1 concerns from both reviews resolved. The image probe now delegates to resolveProjectRelativeSrc (same shared resolver as the video probe), and the video probe drops its isAbsolute pre-check so root-relative CJK URLs like /assets/%E5%9B%BE1.png reach the decode step instead of being handed back verbatim.
Verified
extractVideosStage.ts:205— image probe now usesresolveProjectRelativeSrc(img.src, projectDir, compiledDir), mirroring:178. Both HDR probes are on the same code path; PRINFRA-349 symptom (c) closes with the same shape as (a)/(b).extractVideosStage.ts:168-178— theisAbsolutepre-check drop on the video probe is intentional and load-bearing (comment spells out why: root-relative CJK URLs are POSIX-isAbsolute:trueand would otherwise round-trip percent-encoded).- Tests at
extractVideosStage.test.ts:96-152pin four contract shapes (percent-encoded relative, root-relative browser origin URL, compiledDir precedence, absolute unchanged). Real-filesystem, no mocks — matches the R1 test style. - New
probeColorSpaceSafelywrapper (extractVideosStage.ts:124-135) is a legitimate defensive move: the widened resolver now finds files that previously silently failed to resolve — including truncated / 0-byte assets on whichextractMediaMetadatathrows — and both probes run inside a barePromise.allwhere an unguarded throw would abort the whole render. Log-and-treat-as-SDR is the right behavior for one unreadable file.
Same family, out of scope for this PR
audioExtractor.ts:275-277has the same hand-rolled shape for<audio>srcs:if (!srcPath.startsWith("/") && !srcPath.startsWith("http")) srcPath = join(projectDir, srcPath);. No percent-decode, no compiledDir, no Windows-isAbsolute. A CJK audio filename (音频1.mp3) served with a percent-encoded attribute reproduces the same silent-fail. Engine-sideaudioMixer.ts:552already usesresolveProjectRelativeSrc— the producer-side extractor is the asymmetric one. Worth a same-day follow-up under the PRINFRA-349 umbrella; not a blocker for this PR since it's a distinct pipeline.
CI
- Required checks at
c8f7542show only WIP/Mintlify at HEAD. The producer integration / Windows render / regression suites don't appear to have re-run on the fix commit — either the fix push landed after the workflow trigger window orproducer/*path filters didn't hit. Worth a manual re-run before merge; without it, the previous R1 CANCELLED producer-integration state carries forward untouched.
Fix itself LGTM from my side.
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-review at c8f7542b35f12397b95512ded445abd47fe1c6a1.
The R1 blocker is resolved at the mechanism. extractVideosStage.ts:178-180 and :205-207 now route both video and image sources through resolveProjectRelativeSrc without the harmful isAbsolute() short-circuit, so percent-encoded CJK and origin-root sources share the canonical resolver. probeColorSpaceSafely at :123-137 also prevents a newly-resolvable but unreadable asset from aborting the whole probe batch.
Important — the regression test still pins the helper, not the integration seam. extractVideosStage.test.ts:89-155 imports and calls resolveProjectRelativeSrc directly. It never invokes runExtractVideosStage, so reverting either production call site at extractVideosStage.ts:178 or :205 leaves all four new tests green. The author already called this out honestly; it is not a code blocker for this focused fix, but a follow-up integration test should pin that CJK image reaches nativeHdrImageIds / hdrImageSrcPaths.
Mechanical gate: GitHub reports this head as DIRTY / conflicting with current main, and the only head checks are WIP success plus Mintlify skipped. No Producer, typecheck, or regression result exists for c8f7542b. I cannot clear the prior change request until the branch is rebased/restacked and the normal CI suite runs green.
Verdict: COMMENT (Ready: No)
Reasoning: The HDR image-path blocker is fixed correctly, but the current head conflicts with main and has no code CI evidence; rebase and green CI are still required.
— Magi
…k-paths # Conflicts: # packages/producer/src/services/render/stages/extractVideosStage.test.ts # packages/producer/src/services/render/stages/extractVideosStage.ts
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-review at exact head 459dcdba9ee0c04b5997e269795e61b1574379db.
The prior blockers are cleared:
- Both HDR media probes now use the canonical
resolveProjectRelativeSrcpath. The video probe atextractVideosStage.ts:325no longer short-circuits origin-root URLs throughisAbsolute, and the image probe at:378uses the same decode/query/traversal contract. - The distributed HDR plan path is aligned at
captureHdrResources.ts:86, so percent-encoded non-ASCII video sources resolve identically before ffmpeg extraction. probeImageColorSpaceSafelyatextractVideosStage.ts:135preserves the best-effort image-probe behavior after widening the resolver; one bad image no longer rejects the whole probe batch.- The branch is mergeable and the current head is fully green, including Typecheck, Producer unit + integration, Windows render/tests, and all regression shards.
Non-blocking follow-ups remain as previously noted: the resolver tests exercise the real filesystem contract but do not drive the full runExtractVideosStage image seam, and audioExtractor.ts has the adjacent hand-rolled source-path shape. Neither reopens this PR's fixed video/image HDR path.
Verdict: APPROVE
Reasoning: The shared resolver is now the single source of truth at every modified HDR path, the previous merge/CI blocker is gone, and the exact head has comprehensive green validation.
— Magi
Fixes symptom (c) of PRINFRA-349 (non-ASCII / CJK media path breaks HDR render).
Root cause
HDR pre-extraction resolved a
<video>source to a filesystem path by naively joining the rawsrcattribute:But
v.srcis the compiled DOM's URL attribute, so a source named视频1.mp4arrives percent-encoded as%E8%A7%86%E9%A2%911.mp4. That encoded string was passed straight to ffmpeg as a path →Error opening input file <project>/%E8%A7%86%E9%A2%911.mp4: No such file or directory. The SDR extractor never hit this because it resolves throughresolveProjectRelativeSrc, which triesdecodeUrlPathVariantsagainst the filesystem; the HDR planner had its own hand-rolled join that skipped the decode.Fix
planHdrResourcesnow resolves via the sharedresolveProjectRelativeSrc— the exact resolver the SDR path uses — so the encoded src decodes back to the real on-disk filename (and query strings, origin-root URLs, and..traversal now resolve identically across the two paths). Removed the redundant injectedexistsSyncparam.Test
planHdrResourcestests: a percent-encoded CJK src resolves to the decoded on-disk path; an ASCII src is unchanged. (captureHdrResources.test.ts, 5/5)tsc/oxlint/oxfmtclean.Scope note
The ticket aggregates three symptoms under "one root cause," but (c) is independent of (a)/(b): it's a cross-platform URL-vs-filesystem decode bug in the producer (reproduces on macOS, per the report), not Windows-native path handling. (a) Windows init access-violation and (b) Windows init EIO are a separate Windows-native theme — see the ticket comment for triage; they need a Windows repro to fix safely and aren't touched here.
Related: PRINFRA-349
🤖 Generated with Claude Code