Skip to content

Commit c6150eb

Browse files
authored
fix(export): tag browser exports as limited range so Safari's whites stay white (#277)
1 parent f431c6b commit c6150eb

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

‎src/core/export/export-coordinator.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,16 @@ export class ExportCoordinator {
138138
const ctx = canvas.getContext("2d");
139139
if (!ctx) throw new Error("No 2D context");
140140

141-
const videoSource = new CanvasSource(canvas, { codec: "avc", bitrate: cfg.bitrate });
141+
const videoSource = new CanvasSource(canvas, {
142+
codec: "avc",
143+
bitrate: cfg.bitrate,
144+
// Chrome, Safari and Firefox all encode canvas frames as limited range, but Safari reports full range,
145+
// which players honour by showing white as grey.
146+
onEncodedPacket: (_packet, meta) => {
147+
const colorSpace = meta?.decoderConfig?.colorSpace;
148+
if (colorSpace) colorSpace.fullRange = false;
149+
}
150+
});
142151
output.addVideoTrack(videoSource);
143152

144153
report(0.15, "Mixing audio...");

‎tests/export-coordinator.test.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,18 @@ describe("output settings", () => {
408408
await expect(exporter.exportBlob()).resolves.toBeInstanceOf(Blob);
409409
exporter.dispose();
410410
});
411+
412+
it("tags the video as limited range even when the encoder reports full range", async () => {
413+
const { exporter } = createHarness();
414+
await exporter.exportBlob();
415+
416+
const [, config] = jest.mocked(CanvasSource).mock.calls[0];
417+
const meta = { decoderConfig: { codec: "avc1.42001f", colorSpace: { fullRange: true, matrix: "bt709" as const } } };
418+
config.onEncodedPacket?.({} as never, meta);
419+
420+
expect(meta.decoderConfig.colorSpace).toEqual({ fullRange: false, matrix: "bt709" });
421+
exporter.dispose();
422+
});
411423
});
412424

413425
describe("changes during export", () => {
@@ -656,7 +668,10 @@ describe("output size", () => {
656668

657669
expect(RenderTexture.create).toHaveBeenCalledWith(expect.objectContaining({ width: 720, height: 1280, resolution: 1 }));
658670
expect(canEncodeVideo).toHaveBeenCalledWith("avc", { width: 720, height: 1280, bitrate: 2_222_222 });
659-
expect(CanvasSource).toHaveBeenCalledWith(expect.objectContaining({ width: 720, height: 1280 }), { codec: "avc", bitrate: 2_222_222 });
671+
expect(CanvasSource).toHaveBeenCalledWith(
672+
expect.objectContaining({ width: 720, height: 1280 }),
673+
expect.objectContaining({ codec: "avc", bitrate: 2_222_222 })
674+
);
660675
exporter.dispose();
661676
});
662677
});

0 commit comments

Comments
 (0)