Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions packages/producer/src/services/htmlCompiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { join } from "node:path";
import { runInThisContext } from "node:vm";
import { parseHTML } from "linkedom";
import { interpolateVolumeGain } from "@hyperframes/core/media-volume-envelope";
import { redactTelemetryString } from "@hyperframes/core";
import { defaultLogger } from "../logger.js";
import { NotMediaPayloadError } from "@hyperframes/engine";
import {
Expand Down Expand Up @@ -2763,3 +2764,70 @@ describe("duplicate media ids across nested compositions", () => {
expect(compiled.audios[1]).toMatchObject({ start: 3, end: 6, mediaStart: 50 });
});
});

describe("STUDIO-5433 — ffprobe failure includes src URL for attribution", () => {
function writeCorruptVideoProject(videoSrc: string, assetBytes: Buffer): string {
const projectDir = mkdtempSync(join(tmpdir(), "hf-studio-5433-"));
mkdirSync(join(projectDir, "assets"), { recursive: true });
writeFileSync(join(projectDir, "assets", "clip.mp4"), assetBytes);
writeFileSync(
join(projectDir, "index.html"),
`<!DOCTYPE html>
<html>
<body>
<div id="root" data-composition-id="root" data-start="0" data-duration="4" data-width="640" data-height="360">
<video
id="clip"
src="${videoSrc}"
data-start="0"
data-duration="4"
data-width="640"
data-height="360"
></video>
</div>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["root"] = { duration: () => 4 };
</script>
</body>
</html>`,
);
return projectDir;
}

it("wraps the ffprobe error with [src=<relative-path>] when the local video is corrupt", async () => {
// 0-byte mp4 — ffprobe reports "Invalid data found when processing input",
// the same class as the STUDIO-5433 moov failure. Fail-fast semantics remain
// (video branch throws, unlike audio's graceful-degrade to duration=0).
const projectDir = writeCorruptVideoProject("assets/clip.mp4", Buffer.alloc(0));

let thrown: unknown;
try {
await compileForRender(projectDir, join(projectDir, "index.html"), projectDir);
} catch (error) {
thrown = error;
}

expect(thrown).toBeInstanceOf(Error);
const message = (thrown as Error).message;
// A bare relative path IS the redactor's `BARE_RELATIVE_PATH` shape (one
// separator + a media extension), so it lands as `[path]`. The attribution
// that matters is the remote-URL case below; a local relative src carries
// no host to attribute and the redactor is right to drop it.
expect(message).toContain("[src=[path]]");
// Original ffprobe diagnostic must still be present so failure classifiers
// downstream (e.g. hyperframes_render_metrics.py) continue to match.
expect(message).toMatch(/ffprobe|Invalid data|No video stream/i);
});

// The STUDIO-5433 case is a remote src, and that is the shape whose
// attribution has to survive redaction: host + path kept, query dropped so a
// pre-signed signature never reaches telemetry. Pinned on the redactor
// directly — driving a remote src through `compileForRender` would need a
// download stub, and the wrapper's only transform IS this call.
it("keeps host and path but drops the query when redacting a remote src", () => {
expect(redactTelemetryString("https://cdn.example.com/renders/clip.mp4?sig=abc123&exp=1")).toBe(
"https://cdn.example.com/renders/clip.mp4?\u2026",
);
});
});
34 changes: 32 additions & 2 deletions packages/producer/src/services/htmlCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
isNonRelativeUrl,
parseStrictFiniteTimingNumber,
readMediaStart,
redactTelemetryString,
resolveNaturalMediaTimelineDurationFromValues,
type ResolvedDuration,
type UnresolvedElement,
Expand Down Expand Up @@ -444,6 +445,31 @@ async function resolveMediaDuration(
return { duration: null, resolvedPath: filePath };
}

// STUDIO-5433: attach the remote `src` to any ffprobe failure surfaced from
// this branch. `extractMediaMetadata` → `runFfprobe` intentionally redacts
// its local `filePath` out of the error message (see
// engine/utils/ffprobe.ts::redactFfprobeInput), so a bare `moov atom not
// found` in Datadog carries no attribution and requires a Temporal history
// dump to identify the offending source. Re-throwing with the `src`
// (query-string redacted via `redactTelemetryString` so pre-signed URL
// signatures never reach telemetry) makes the next occurrence diagnosable
// directly from the render error. Fail-fast semantics for the video branch
// are preserved — only the message is enriched.
const withSrcContext = (error: unknown): Error => {
// A NotMediaPayloadError already carries its own attribution AND the
// routing metadata downstream keys on — `.code = "NOT_MEDIA_PAYLOAD"`,
// `.owner = "user"`, `.retryable = false`, `.elementFingerprints`. Wrapping
// it in a bare Error drops all four, flipping a user-input bug to
// generic/system/retryable: it pages ops and re-runs the render. Pass it
// through untouched.
if (error instanceof NotMediaPayloadError) return error;
const originalMessage = error instanceof Error ? error.message : String(error);
const safeSrc = redactTelemetryString(src);
const wrapped = new Error(`${originalMessage} [src=${safeSrc}]`);
if (error instanceof Error && error.stack) wrapped.stack = error.stack;
return wrapped;
};

return withMediaProbeSlot(async () => {
let profile: MediaProbeProfile;
try {
Expand Down Expand Up @@ -471,13 +497,17 @@ async function resolveMediaDuration(
}
return { duration: null, resolvedPath: filePath };
}
throw error;
throw withSrcContext(error);
}
assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);

let metadata: { durationSeconds: number };
if (tagName === "video") {
metadata = await extractMediaMetadata(filePath);
try {
metadata = await extractMediaMetadata(filePath);
} catch (error) {
throw withSrcContext(error);
}
} else {
try {
metadata = await extractAudioMetadata(filePath);
Expand Down
Loading