Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 29 additions & 0 deletions docs/guides/rendering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,35 @@ For several variable-driven versions, use batch rendering. For remote infrastruc

Those workflows involve output naming, credentials, concurrency, and infrastructure choices. Start in the [CLI guide](/developers/cli) and use the complete [CLI reference](/packages/cli) when you need every flag.

## Render provenance

Rendered video carries two container metadata tags that say which tool wrote the file:

```bash
ffprobe -v error -show_entries format_tags -of json out.mp4
```

```json
{ "hyperframes_renderer": "hyperframes", "hyperframes_version": "0.7.107" }
```

That is the whole of it. The tags name the renderer and its version, and nothing else: no file
paths, usernames, machine names, project names, or anything about the composition. They are
container metadata, not a visible watermark, so no pixel of your video changes. Matroska
uppercases tag names on read, so a `.webm` reports `HYPERFRAMES_RENDERER`.

Strip them whenever you like:

```bash
ffmpeg -i out.mp4 -map_metadata -1 -c copy clean.mp4
```

<Note>
Treat the tags as a positive signal only. If they are present, HyperFrames wrote the file. If they

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Presence cannot establish that HyperFrames wrote the file: these are ordinary, unsigned metadata keys, and anyone can stamp the same renderer/version pair with one ffmpeg command. The current sentence turns a self-identification marker into an authenticity/provenance guarantee that downstream code may trust. Please say that presence means the file claims to be HyperFrames output / is a useful diagnostic hint, and explicitly state that it is unauthenticated and must not be used as a security or attribution boundary. Verifiable positive provenance would require a signature (for example C2PA), not writable container tags. The matching module comment at renderProvenance.ts:16-19 needs the same correction.

are absent that proves nothing, because re-encoding, remuxing, or any tool that drops unknown keys
removes them, and files rendered by older versions never carried them.
</Note>

## If rendering fails

Run:
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/background-removal/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { extname } from "node:path";
import { findFFmpeg, findFFprobe, getFFmpegInstallHint } from "../browser/ffmpeg.js";
import { createSession, type Session } from "./inference.js";
import { type Device, type ModelId } from "./manager.js";
import { DEFAULT_VP9_CPU_USED } from "@hyperframes/engine";
import { DEFAULT_VP9_CPU_USED, renderProvenanceArgs } from "@hyperframes/engine";

export type OutputFormat = "webm" | "mov" | "png";

Expand Down Expand Up @@ -182,6 +182,7 @@ export function buildEncoderArgs(
"-metadata:s:v:0",
"alpha_mode=1",
"-an",
...renderProvenanceArgs(outputPath),
outputPath,
];
}
Expand All @@ -197,6 +198,7 @@ export function buildEncoderArgs(
"-pix_fmt",
"yuva444p10le",
"-an",
...renderProvenanceArgs(outputPath),
outputPath,
];
}
Expand Down
10 changes: 10 additions & 0 deletions packages/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,13 @@ export {
type HdrMasteringMetadata,
} from "./utils/hdr.js";
export type { VideoColorSpace } from "./utils/ffprobe.js";
export {
renderProvenanceArgs,
appendRenderProvenanceArgs,
readRenderProvenance,
PROVENANCE_RENDERER_TAG,
PROVENANCE_VERSION_TAG,
PROVENANCE_RENDERER_NAME,
PROVENANCE_VERSION,
type RenderProvenance,
} from "./utils/renderProvenance.js";
6 changes: 6 additions & 0 deletions packages/engine/src/services/chunkEncoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, it, expect, vi } from "vitest";
import { ENCODER_PRESETS, getEncoderPreset, buildEncoderArgs } from "./chunkEncoder.js";
import { renderProvenanceArgs } from "../utils/renderProvenance.js";

const TINY_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAEElEQVR4nGP8wwACLGCSAQANBAECv1AVswAAAABJRU5ErkJggg==",
Expand Down Expand Up @@ -403,13 +404,18 @@ describe("muxVideoWithAudio audio codec handling", () => {
"+faststart",
"-avoid_negative_ts",
"make_zero",
...renderProvenanceArgs("/tmp/output.mp4"),
"-r",
"30",
"-y",
"/tmp/output.mp4",
]);
expect(calls[0]!.args).not.toContain("-shortest");
expect(calls[0]!.args).not.toContain("-use_editlist");
// The faststart flag set above must survive the provenance flag: ffmpeg
// takes the last -movflags occurrence, and a non-additive one would drop it.
expect(calls[0]!.args.filter((a) => a === "-movflags")).toHaveLength(2);
expect(calls[0]!.args).toContain("+faststart");

emitClose(calls[0]!.proc, 0);
await expect(muxPromise).resolves.toMatchObject({
Expand Down
9 changes: 9 additions & 0 deletions packages/engine/src/services/chunkEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { extractAudioMetadata } from "../utils/ffprobe.js";
import { type Fps, fpsToFfmpegArg } from "@hyperframes/core";
import type { EncoderOptions, EncodeResult, MuxResult } from "./chunkEncoder.types.js";
import { appendVp9CpuUsedArg } from "./vp9Options.js";
import { appendRenderProvenanceArgs } from "../utils/renderProvenance.js";

export type { EncoderOptions, EncodeResult, MuxResult } from "./chunkEncoder.types.js";

Expand Down Expand Up @@ -361,6 +362,7 @@ export function buildEncoderArgs(
} else if (codec === "prores") {
args.push("-c:v", "prores_ks", "-profile:v", preset, "-vendor", "apl0");
args.push("-pix_fmt", pixelFormat);
appendRenderProvenanceArgs(args, outputPath);
return [...args, "-y", outputPath];
}

Expand Down Expand Up @@ -445,6 +447,8 @@ export function buildEncoderArgs(

args.push("-avoid_negative_ts", "make_zero");

appendRenderProvenanceArgs(args, outputPath);

args.push("-y", outputPath);
return args;
}
Expand Down Expand Up @@ -701,6 +705,10 @@ export async function muxVideoWithAudio(
// AAC priming packet. `make_zero` discards that edit and shifts copied video
// forward by one AAC frame (~21ms), creating a visible first-frame offset.
if (!copiesContainerizedAac) args.push("-avoid_negative_ts", "make_zero");
// Re-assert provenance here: this stage re-muxes into the delivered
// container, and the mp4 muxer drops the encode stage's tags without the
// use_metadata_tags flag that appendRenderProvenanceArgs adds.
appendRenderProvenanceArgs(args, outputPath);
if (fps !== undefined) {
// Set the exact output framerate so the muxer doesn't PTS-average a
// fractional rational like `360000/12001` instead of `30/1` into the
Expand Down Expand Up @@ -742,6 +750,7 @@ export async function applyFaststart(
return { success: true, outputPath, durationMs: 0 };
}
const args = ["-i", inputPath, "-c", "copy", "-movflags", "+faststart"];
appendRenderProvenanceArgs(args, outputPath);
if (fps !== undefined) {
// Set the exact output framerate so the final remux doesn't PTS-average
// a fractional rational like `360000/12001` instead of `30/1` into the
Expand Down
4 changes: 4 additions & 0 deletions packages/engine/src/services/streamingEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { withEvenDimensionPad } from "../utils/evenDimensions.js";
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
import { fpsToFfmpegArg, type Fps } from "@hyperframes/core";
import { appendVp9CpuUsedArg } from "./vp9Options.js";
import { appendRenderProvenanceArgs } from "../utils/renderProvenance.js";

// Re-export EncoderOptions so callers can reference the type via this module.
export type { EncoderOptions } from "./chunkEncoder.types.js";
Expand Down Expand Up @@ -350,6 +351,7 @@ export function buildStreamingArgs(
} else if (codec === "prores") {
args.push("-c:v", "prores_ks", "-profile:v", preset, "-vendor", "apl0");
args.push("-pix_fmt", pixelFormat);
appendRenderProvenanceArgs(args, outputPath);
return [...args, "-y", outputPath];
}

Expand Down Expand Up @@ -428,6 +430,8 @@ export function buildStreamingArgs(
// for the full explanation; same playback compatibility class.
args.push("-avoid_negative_ts", "make_zero");

appendRenderProvenanceArgs(args, outputPath);

args.push("-y", outputPath);
return args;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/engine/src/utils/ffprobe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,10 @@ function extractStillImageMetadata(filePath: string): StillImageMetadata | null
* in newer ones; HDR tags vary similarly. Use this for any sidecar tag where
* you want to be resilient across muxer versions.
*/
function readTagCI(tags: Record<string, string | undefined> | undefined, name: string): string {
export function readTagCI(
tags: Record<string, string | undefined> | undefined,
name: string,
): string {
if (!tags) return "";
const target = name.toLowerCase();
for (const [key, value] of Object.entries(tags)) {
Expand Down
Loading
Loading