From 228721d68f72b033a046815d2ddf5623aba295ba Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:47:50 +0100 Subject: [PATCH] Add VideoSampleColorSpace --- src/index.ts | 1 + src/sample.ts | 42 +++++++++++++++++++++++--- test/node/node-sample-creation.test.ts | 27 +++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 test/node/node-sample-creation.test.ts diff --git a/src/index.ts b/src/index.ts index f5dbdf1..5434d1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -168,6 +168,7 @@ export { AudioSampleCopyToOptions, VideoSample, VideoSampleInit, + VideoSampleColorSpace, CropRectangle, } from './sample'; export { diff --git a/src/sample.ts b/src/sample.ts index fae15bf..02e454c 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -125,7 +125,7 @@ export class VideoSample implements Disposable { /** The duration of the frame in seconds. */ readonly duration!: number; /** The color space of the frame. */ - readonly colorSpace!: VideoColorSpace; + readonly colorSpace!: VideoSampleColorSpace; /** The width of the frame in pixels after rotation. */ get displayWidth() { @@ -216,7 +216,7 @@ export class VideoSample implements Disposable { this.rotation = init.rotation ?? 0; this.timestamp = init.timestamp!; this.duration = init.duration ?? 0; - this.colorSpace = new VideoColorSpace(init.colorSpace); + this.colorSpace = new VideoSampleColorSpace(init.colorSpace); } else if (typeof VideoFrame !== 'undefined' && data instanceof VideoFrame) { if (init?.rotation !== undefined && ![0, 90, 180, 270].includes(init.rotation)) { throw new TypeError('init.rotation, when provided, must be 0, 90, 180, or 270.'); @@ -239,7 +239,7 @@ export class VideoSample implements Disposable { this.rotation = init?.rotation ?? 0; this.timestamp = init?.timestamp ?? data.timestamp / 1e6; this.duration = init?.duration ?? (data.duration ?? 0) / 1e6; - this.colorSpace = data.colorSpace; + this.colorSpace = new VideoSampleColorSpace(data.colorSpace); } else if ( (typeof HTMLImageElement !== 'undefined' && data instanceof HTMLImageElement) || (typeof SVGImageElement !== 'undefined' && data instanceof SVGImageElement) @@ -308,7 +308,7 @@ export class VideoSample implements Disposable { this.rotation = init.rotation ?? 0; this.timestamp = init.timestamp!; this.duration = init.duration ?? 0; - this.colorSpace = new VideoColorSpace({ + this.colorSpace = new VideoSampleColorSpace({ matrix: 'rgb', primaries: 'bt709', transfer: 'iec61966-2-1', @@ -805,6 +805,40 @@ export class VideoSample implements Disposable { } } +/** + * Describes the color space of a {@link VideoSample}. Corresponds to the WebCodecs API's VideoColorSpace. + * @group Samples + * @public + */ +export class VideoSampleColorSpace { + /** The color primaries standard used. */ + readonly primaries: VideoColorPrimaries | null; + /** The transfer characteristics used. */ + readonly transfer: VideoTransferCharacteristics | null; + /** The color matrix coefficients used. */ + readonly matrix: VideoMatrixCoefficients | null; + /** Whether the color values use the full range or limited range. */ + readonly fullRange: boolean | null; + + /** Creates a new VideoSampleColorSpace. */ + constructor(init?: VideoColorSpaceInit) { + this.primaries = init?.primaries ?? null; + this.transfer = init?.transfer ?? null; + this.matrix = init?.matrix ?? null; + this.fullRange = init?.fullRange ?? null; + } + + /** Serializes the color space to a JSON object. */ + toJSON(): VideoColorSpaceInit { + return { + primaries: this.primaries, + transfer: this.transfer, + matrix: this.matrix, + fullRange: this.fullRange, + }; + } +} + const isVideoFrame = (x: unknown): x is VideoFrame => { return typeof VideoFrame !== 'undefined' && x instanceof VideoFrame; }; diff --git a/test/node/node-sample-creation.test.ts b/test/node/node-sample-creation.test.ts new file mode 100644 index 0000000..0cf4064 --- /dev/null +++ b/test/node/node-sample-creation.test.ts @@ -0,0 +1,27 @@ +import { test } from 'vitest'; +import { AudioSample, VideoSample } from '../../src/sample.js'; + +test('VideoSample creation from bytes', async () => { + const bytes = new Uint8Array(1024); + const sample = new VideoSample(bytes, { + codedWidth: 1280, + codedHeight: 720, + format: 'RGBA', + timestamp: 0, + }); + + sample.close(); +}); + +test('AudioSample creation from bytes', async () => { + const bytes = new Uint8Array(1024); + const sample = new AudioSample({ + data: bytes, + numberOfChannels: 2, + format: 's16', + sampleRate: 48000, + timestamp: 0, + }); + + sample.close(); +});