Preserve the correct colour range in video exports

This commit is contained in:
young
2026-08-25 19:06:02 +10:00
parent 7da7e22ebe
commit 49fb397c98
6 changed files with 99 additions and 33 deletions
+6 -12
View File
@@ -93,6 +93,7 @@ import type {
ExportRenderBackend,
ExportResult,
} from "./types";
import { ENCODED_H264_COLOR_SPACE_FALLBACK, EXPORT_CANVAS_COLOR_SPACE } from "./videoColorSpace";
interface VideoExporterConfig extends ExportConfig {
videoUrl: string;
@@ -2726,9 +2727,11 @@ export class ModernVideoExporter {
if (this.nativeEncoderError) throw this.nativeEncoderError;
}
const canvas = this.renderer!.getCanvas();
// @ts-expect-error - colorSpace is supported at runtime but missing from this DOM typing.
const frame = new VideoFrame(canvas, {
timestamp,
duration: frameDuration,
colorSpace: EXPORT_CANVAS_COLOR_SPACE,
});
this.nativeH264Encoder.encode(frame, { keyFrame: frameIndex % 300 === 0 });
frame.close();
@@ -2957,12 +2960,7 @@ export class ModernVideoExporter {
const exportFrame = new VideoFrame(canvas, {
timestamp,
duration: frameDuration,
colorSpace: {
primaries: "bt709",
transfer: "iec61966-2-1",
matrix: "rgb",
fullRange: true,
},
colorSpace: EXPORT_CANVAS_COLOR_SPACE,
});
while (
@@ -3377,12 +3375,8 @@ export class ModernVideoExporter {
try {
if (isFirstChunk && this.videoDescription) {
// Add decoder config for the first chunk
const colorSpace = this.videoColorSpace || {
primaries: "bt709",
transfer: "iec61966-2-1",
matrix: "rgb",
fullRange: true,
};
const colorSpace =
this.videoColorSpace || ENCODED_H264_COLOR_SPACE_FALLBACK;
const metadata: EncodedVideoChunkMetadata = {
decoderConfig: {
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { ENCODED_H264_COLOR_SPACE_FALLBACK, EXPORT_CANVAS_COLOR_SPACE } from "./videoColorSpace";
describe("export colour metadata", () => {
it("does not confuse full-range RGB input with encoded YUV output", () => {
expect(EXPORT_CANVAS_COLOR_SPACE).toMatchObject({ matrix: "rgb", fullRange: true });
expect(ENCODED_H264_COLOR_SPACE_FALLBACK).toMatchObject({
matrix: "bt709",
transfer: "bt709",
fullRange: false,
});
});
});
+18
View File
@@ -0,0 +1,18 @@
/** The renderer composites into an sRGB canvas, whose pixels are full-range RGB. */
export const EXPORT_CANVAS_COLOR_SPACE = {
primaries: "bt709",
transfer: "iec61966-2-1",
matrix: "rgb",
fullRange: true,
} as const satisfies VideoColorSpaceInit;
/**
* H.264 encoders normally convert the canvas to video-range YUV. Use this only
* when the encoder does not report its own output colour metadata.
*/
export const ENCODED_H264_COLOR_SPACE_FALLBACK = {
primaries: "bt709",
transfer: "bt709",
matrix: "bt709",
fullRange: false,
} as const satisfies VideoColorSpaceInit;
+6 -19
View File
@@ -8,8 +8,8 @@ import type {
CursorStyle,
CursorTelemetryPoint,
Padding,
SpeedRegion,
SourceAudioTrackSettings,
SpeedRegion,
TrimRegion,
WebcamOverlaySettings,
ZoomMotionBlurTuning,
@@ -38,6 +38,7 @@ import type {
ExportProgress,
ExportResult,
} from "./types";
import { ENCODED_H264_COLOR_SPACE_FALLBACK, EXPORT_CANVAS_COLOR_SPACE } from "./videoColorSpace";
const DEFAULT_MAX_ENCODE_QUEUE = 240;
const PROGRESS_SAMPLE_WINDOW_MS = 1_000;
@@ -825,12 +826,7 @@ export class VideoExporter {
const frame = new VideoFrame(canvas, {
timestamp,
duration: frameDuration,
colorSpace: {
primaries: "bt709",
transfer: "iec61966-2-1",
matrix: "rgb",
fullRange: true,
},
colorSpace: EXPORT_CANVAS_COLOR_SPACE,
});
this.nativeH264Encoder.encode(frame, { keyFrame: frameIndex % 300 === 0 });
frame.close();
@@ -1077,12 +1073,7 @@ export class VideoExporter {
const exportFrame = new VideoFrame(canvas, {
timestamp,
duration: frameDuration,
colorSpace: {
primaries: "bt709",
transfer: "iec61966-2-1",
matrix: "rgb",
fullRange: true,
},
colorSpace: EXPORT_CANVAS_COLOR_SPACE,
});
while (
@@ -1270,12 +1261,8 @@ export class VideoExporter {
try {
if (isFirstChunk && this.videoDescription) {
// Add decoder config for the first chunk
const colorSpace = this.videoColorSpace || {
primaries: "bt709",
transfer: "iec61966-2-1",
matrix: "rgb",
fullRange: true,
};
const colorSpace =
this.videoColorSpace || ENCODED_H264_COLOR_SPACE_FALLBACK;
const metadata: EncodedVideoChunkMetadata = {
decoderConfig: {