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
34 changes: 34 additions & 0 deletions docs/guides/rendering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,40 @@ 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>
These tags are an unauthenticated diagnostic hint, not proof of origin. They are ordinary unsigned
container keys, so anything can write the same two values with a single `ffmpeg -metadata`
command: a tag that is present means the file *claims* to be HyperFrames output, not that
HyperFrames wrote it. A tag that is absent means just as little, because re-encoding, remuxing, or
any tool that drops unknown keys strips it, and files rendered by older versions never carried it.
Treat it as a "what probably produced this file?" hint for support and debugging, never as an
authenticity, attribution, or licensing check. Verifiable provenance needs signed claims such as
C2PA.
</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
29 changes: 17 additions & 12 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 @@ -611,18 +615,14 @@ export async function encodeFramesChunkedConcat(
const concatInput = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
writeFileSync(concatListPath, concatInput, "utf-8");

const concatArgs = [
"-f",
"concat",
"-safe",
"0",
"-i",
concatListPath,
"-c",
"copy",
"-y",
outputPath,
];
const concatArgs = ["-f", "concat", "-safe", "0", "-i", concatListPath, "-c", "copy"];
// The concat demuxer does not carry per-chunk container metadata into the
// output, so the chunks' provenance is dropped here even though every chunk
// carries it. Re-assert on the concatenated file: for a no-audio mov/webm
// this is the last container write, since mux is skipped and applyFaststart
// only copies those two formats.
appendRenderProvenanceArgs(concatArgs, outputPath);
concatArgs.push("-y", outputPath);
const encodeTimeout = config?.ffmpegEncodeTimeout ?? DEFAULT_CONFIG.ffmpegEncodeTimeout;
const concatProcessResult = await runFfmpeg(concatArgs, { signal, timeout: encodeTimeout });
const concatResult = {
Expand Down Expand Up @@ -701,6 +701,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 +746,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