From 42196e9f649d38cbe6cf700e453090e459d955b4 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:11:31 +0200 Subject: [PATCH 1/5] Add proper non-null check --- src/media-sink.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/media-sink.ts b/src/media-sink.ts index 6fd28b8..0edc4dd 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -2095,7 +2095,7 @@ class AudioDecoderWrapper extends DecoderWrapper { const sampleHandler = (sample: AudioSample) => { let sampleTimestamp = sample.timestamp; - if (this.expectedFirstTimestamp && this.currentTimestamp === null) { + if (this.expectedFirstTimestamp !== null && this.currentTimestamp === null) { this.timestampOffset = this.expectedFirstTimestamp - sampleTimestamp; ; } From 7d1eb5b063571073d11cda9a526aa323e5160ca0 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:48:20 +0200 Subject: [PATCH 2/5] Fall back to color space information from bitstream, fill missing color space fields when passing it to Chromium's VideoDecoder, fix AV1 Sequence Header OBU parsing, fix vpcC box generation, still mux partial color space info into container (fixes #474, closes #475) --- dev/demux.html | 11 +- src/codec-data.ts | 147 +++++++++++++++----- src/codec.ts | 137 ++++++++++++++++++- src/isobmff/isobmff-boxes.ts | 43 ++++-- src/isobmff/isobmff-demuxer.ts | 90 +++++++++++-- src/matroska/matroska-demuxer.ts | 101 ++++++++------ src/matroska/matroska-muxer.ts | 20 +-- src/media-sink.ts | 47 +++++-- src/misc.ts | 19 +++ test/browser/color-space.test.ts | 222 +++++++++++++++++++++++++++++++ 10 files changed, 710 insertions(+), 127 deletions(-) create mode 100644 test/browser/color-space.test.ts diff --git a/dev/demux.html b/dev/demux.html index 6d93d39..ab1d1c6 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -17,14 +17,11 @@ source: new Mediabunny.BlobSource(file), }); - const track = await input.getPrimaryAudioTrack(); - const sink = new Mediabunny.AudioSampleSink(track); - - for await (const sample of sink.samples()) { - console.log(sample); - sample.close() - } + const track = await input.getPrimaryVideoTrack(); + const sink = new Mediabunny.EncodedPacketSink(track); + const packet = await sink.getFirstPacket(); + console.log(packet); /* for await (const packet of packetSink.packets()) { diff --git a/src/codec-data.ts b/src/codec-data.ts index 071720d..5d80fee 100644 --- a/src/codec-data.ts +++ b/src/codec-data.ts @@ -1868,6 +1868,12 @@ export const extractVp9CodecInfoFromPacket = ( }; }; +export const vp9CodecInfoHasColorInfo = (info: Vp9CodecInfo) => { + return info.colourPrimaries !== 2 + || info.transferCharacteristics !== 2 + || info.matrixCoefficients !== 2; +}; + export type Av1CodecInfo = { profile: number; level: number; @@ -1877,6 +1883,10 @@ export type Av1CodecInfo = { chromaSubsamplingX: number; chromaSubsamplingY: number; chromaSamplePosition: number; + videoFullRangeFlag: number; + colourPrimaries: number; + transferCharacteristics: number; + matrixCoefficients: number; }; /** Iterates over all OBUs in an AV1 packet bitstream. */ @@ -1891,7 +1901,8 @@ export const iterateAv1PacketObus = function* (packet: Uint8Array) { for (let i = 0; i < 8; i++) { const byte = bitstream.readAlignedByte(); - value |= ((byte & 0x7f) << (i * 7)); + // No bit shift since the value can get big + value += (byte & 0x7f) * 2 ** (i * 7); if (!(byte & 0x80)) { break; @@ -1904,7 +1915,7 @@ export const iterateAv1PacketObus = function* (packet: Uint8Array) { } // Spec requirement - if (value >= 2 ** 32 - 1) { + if (value > 2 ** 32 - 1) { return null; } @@ -1978,43 +1989,43 @@ export const extractAv1CodecInfoFromPacket = ( if (reducedStillPictureHeader) { seqLevel = bitstream.readBits(5); } else { - // Parse timing_info_present_flag const timingInfoPresentFlag = bitstream.readBits(1); + let decoderModelInfoPresentFlag = 0; + if (timingInfoPresentFlag) { - // Skip timing info (num_units_in_display_tick, time_scale, equal_picture_interval) bitstream.skipBits(32); // num_units_in_display_tick bitstream.skipBits(32); // time_scale const equalPictureInterval = bitstream.readBits(1); if (equalPictureInterval) { - // Skip num_ticks_per_picture_minus_1 (uvlc) - // Since this is variable length, we'd need to implement uvlc reading - // For now, we'll return null as this is rare - return null; + // num_ticks_per_picture_minus_1 is uvlc-coded, so we need to seek past it manually + let leadingZeros = 0; + while (leadingZeros < 32 && !bitstream.readBits(1)) { + leadingZeros++; + } + + if (leadingZeros < 32) { + bitstream.skipBits(leadingZeros); + } + } + + decoderModelInfoPresentFlag = bitstream.readBits(1); + + if (decoderModelInfoPresentFlag) { + bufferDelayLengthMinus1 = bitstream.readBits(5); + bitstream.skipBits(32); // num_units_in_decoding_tick + bitstream.skipBits(5); // buffer_removal_time_length_minus_1 + bitstream.skipBits(5); // frame_presentation_time_length_minus_1 } } - // Parse decoder_model_info_present_flag - const decoderModelInfoPresentFlag = bitstream.readBits(1); - - if (decoderModelInfoPresentFlag) { - // Store buffer_delay_length_minus_1 instead of just skipping - bufferDelayLengthMinus1 = bitstream.readBits(5); - bitstream.skipBits(32); // num_units_in_decoding_tick - bitstream.skipBits(5); // buffer_removal_time_length_minus_1 - bitstream.skipBits(5); // frame_presentation_time_length_minus_1 - } - - // Parse operating_points_cnt_minus_1 + const initialDisplayDelayPresentFlag = bitstream.readBits(1); const operatingPointsCntMinus1 = bitstream.readBits(5); - // For each operating point for (let i = 0; i <= operatingPointsCntMinus1; i++) { - // operating_point_idc[i] - bitstream.skipBits(12); + bitstream.skipBits(12); // operating_point_idc[i] - // seq_level_idx[i] const seqLevelIdx = bitstream.readBits(5); if (i === 0) { @@ -2022,7 +2033,6 @@ export const extractAv1CodecInfoFromPacket = ( } if (seqLevelIdx > 7) { - // seq_tier[i] const seqTierTemp = bitstream.readBits(1); if (i === 0) { seqTier = seqTierTemp; @@ -2030,7 +2040,6 @@ export const extractAv1CodecInfoFromPacket = ( } if (decoderModelInfoPresentFlag) { - // decoder_model_present_for_this_op[i] const decoderModelPresentForThisOp = bitstream.readBits(1); if (decoderModelPresentForThisOp) { @@ -2041,12 +2050,12 @@ export const extractAv1CodecInfoFromPacket = ( } } - // initial_display_delay_present_flag - const initialDisplayDelayPresentFlag = bitstream.readBits(1); - if (initialDisplayDelayPresentFlag) { - // initial_display_delay_minus_1[i] - bitstream.skipBits(4); + const initialDisplayDelayPresentForThisOp = bitstream.readBits(1); + + if (initialDisplayDelayPresentForThisOp) { + bitstream.skipBits(4); // initial_display_delay_minus_1[i] + } } } } @@ -2129,11 +2138,32 @@ export const extractAv1CodecInfoFromPacket = ( monochrome = bitstream.readBits(1); } + let colourPrimaries = 2; // CP_UNSPECIFIED + let transferCharacteristics = 2; // TC_UNSPECIFIED + let matrixCoefficients = 2; // MC_UNSPECIFIED + + const colorDescriptionPresentFlag = bitstream.readBits(1); + if (colorDescriptionPresentFlag) { + colourPrimaries = bitstream.readBits(8); + transferCharacteristics = bitstream.readBits(8); + matrixCoefficients = bitstream.readBits(8); + } + + let videoFullRangeFlag = 0; let chromaSubsamplingX = 1; let chromaSubsamplingY = 1; - let chromaSamplePosition = 0; + let chromaSamplePosition = 0; // CSP_UNKNOWN + + if (monochrome) { + videoFullRangeFlag = bitstream.readBits(1); + } else if (colourPrimaries === 1 && transferCharacteristics === 13 && matrixCoefficients === 0) { + // sRGB with an identity matrix, which is always full range 4:4:4 + videoFullRangeFlag = 1; + chromaSubsamplingX = 0; + chromaSubsamplingY = 0; + } else { + videoFullRangeFlag = bitstream.readBits(1); - if (!monochrome) { if (seqProfile === 0) { chromaSubsamplingX = 1; chromaSubsamplingY = 1; @@ -2143,9 +2173,10 @@ export const extractAv1CodecInfoFromPacket = ( } else { if (bitDepth === 12) { chromaSubsamplingX = bitstream.readBits(1); - if (chromaSubsamplingX) { - chromaSubsamplingY = bitstream.readBits(1); - } + chromaSubsamplingY = chromaSubsamplingX ? bitstream.readBits(1) : 0; + } else { + chromaSubsamplingX = 1; + chromaSubsamplingY = 0; } } @@ -2163,12 +2194,56 @@ export const extractAv1CodecInfoFromPacket = ( chromaSubsamplingX, chromaSubsamplingY, chromaSamplePosition, + videoFullRangeFlag, + colourPrimaries, + transferCharacteristics, + matrixCoefficients, }; } return null; }; +export const av1CodecInfoHasColorInfo = (info: Av1CodecInfo) => { + return info.colourPrimaries !== 2 + || info.transferCharacteristics !== 2 + || info.matrixCoefficients !== 2; +}; + +export type ProresCodecInfo = { + colourPrimaries: number; + transferCharacteristics: number; + matrixCoefficients: number; + fullRange: boolean; +}; + +export const extractProresCodecInfoFromPacket = (packet: Uint8Array): ProresCodecInfo | null => { + // https://wiki.multimedia.cx/index.php/Apple_ProRes + + const frameHeaderStart = 8; + if (packet.length < frameHeaderStart + 28) { + return null; + } + + const view = toDataView(packet); + + if (view.getUint32(4) !== 0x69637066) { // 'icpf' + return null; + } + + const headerSize = view.getUint16(frameHeaderStart); + if (headerSize < 28) { + return null; + } + + return { + fullRange: false, // ProRes is always limited range + colourPrimaries: view.getUint8(frameHeaderStart + 14), + transferCharacteristics: view.getUint8(frameHeaderStart + 15), + matrixCoefficients: view.getUint8(frameHeaderStart + 16), + }; +}; + export const parseOpusIdentificationHeader = (bytes: Uint8Array) => { const view = toDataView(bytes); diff --git a/src/codec.ts b/src/codec.ts index 63c41d1..23f1be0 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -10,13 +10,22 @@ import { parseAacAudioSpecificConfig } from '../shared/aac-misc'; import { Av1CodecInfo, AvcDecoderConfigurationRecord, + deserializeAvcDecoderConfigurationRecord, + deserializeHevcDecoderConfigurationRecord, HevcDecoderConfigurationRecord, + HevcNalUnitType, + parseAvcSps, + parseHevcSps, + ProresCodecInfo, Vp9CodecInfo, } from './codec-data'; import { COLOR_PRIMARIES_MAP, + COLOR_PRIMARIES_MAP_INVERSE, MATRIX_COEFFICIENTS_MAP, + MATRIX_COEFFICIENTS_MAP_INVERSE, TRANSFER_CHARACTERISTICS_MAP, + TRANSFER_CHARACTERISTICS_MAP_INVERSE, assert, assertNever, base64ToBytes, @@ -370,7 +379,7 @@ export const generateAv1CodecConfigurationFromCodecString = (codecString: string const tier = levelAndTier.slice(-1) === 'H' ? 1 : 0; const bitDepth = Number(parts[3]); const highBitDepth = bitDepth === 8 ? 0 : 1; - const twelveBit = 0; + const twelveBit = bitDepth === 12 ? 1 : 0; const monochrome = parts[4] ? Number(parts[4]) : 0; const chromaSubsamplingX = parts[5] ? Number(parts[5][0]) : 1; const chromaSubsamplingY = parts[5] ? Number(parts[5][1]) : 1; @@ -578,6 +587,132 @@ export const extractVideoCodecString = (trackInfo: { throw new TypeError(`Unhandled codec '${codec}'.`); }; +export const extractColorSpace = (info: { + codec: VideoCodec | null; + codecDescription: Uint8Array | null; + avcCodecInfo: AvcDecoderConfigurationRecord | null; + hevcCodecInfo: HevcDecoderConfigurationRecord | null; + vp9CodecInfo: Vp9CodecInfo | null; + av1CodecInfo: Av1CodecInfo | null; + proresCodecInfo: ProresCodecInfo | null; +}): VideoColorSpaceInit => { + switch (info.codec) { + case 'avc': { + let spsData = info.avcCodecInfo?.sequenceParameterSets[0]; + if (!spsData && info.codecDescription) { + spsData = deserializeAvcDecoderConfigurationRecord(info.codecDescription)?.sequenceParameterSets[0]; + } + + if (spsData) { + const spsInfo = parseAvcSps(spsData); + if (spsInfo) { + return { + primaries: + COLOR_PRIMARIES_MAP_INVERSE[spsInfo.colourPrimaries] as + VideoColorPrimaries | undefined, + transfer: + TRANSFER_CHARACTERISTICS_MAP_INVERSE[spsInfo.transferCharacteristics] as + VideoTransferCharacteristics | undefined, + matrix: + MATRIX_COEFFICIENTS_MAP_INVERSE[spsInfo.matrixCoefficients] as + VideoMatrixCoefficients | undefined, + fullRange: !!spsInfo.fullRangeFlag, + }; + } + } + }; break; + + case 'hevc': { + let spsData = info.hevcCodecInfo?.arrays + .find(x => x.nalUnitType === HevcNalUnitType.SPS_NUT)?.nalUnits[0]; + if (!spsData && info.codecDescription) { + spsData = deserializeHevcDecoderConfigurationRecord(info.codecDescription)?.arrays + .find(x => x.nalUnitType === HevcNalUnitType.SPS_NUT)?.nalUnits[0]; + } + + if (spsData) { + const spsInfo = parseHevcSps(spsData); + if (spsInfo) { + return { + primaries: + COLOR_PRIMARIES_MAP_INVERSE[spsInfo.colourPrimaries] as + VideoColorPrimaries | undefined, + transfer: + TRANSFER_CHARACTERISTICS_MAP_INVERSE[spsInfo.transferCharacteristics] as + VideoTransferCharacteristics | undefined, + matrix: + MATRIX_COEFFICIENTS_MAP_INVERSE[spsInfo.matrixCoefficients] as + VideoMatrixCoefficients | undefined, + fullRange: !!spsInfo.fullRangeFlag, + }; + } + } + }; break; + + case 'vp8': { + // The situation is fucky; do nothing for now. + }; break; + + case 'vp9': { + if (info.vp9CodecInfo) { + return { + primaries: + COLOR_PRIMARIES_MAP_INVERSE[info.vp9CodecInfo.colourPrimaries] as + VideoColorPrimaries | undefined, + transfer: + TRANSFER_CHARACTERISTICS_MAP_INVERSE[info.vp9CodecInfo.transferCharacteristics] as + VideoTransferCharacteristics | undefined, + matrix: + MATRIX_COEFFICIENTS_MAP_INVERSE[info.vp9CodecInfo.matrixCoefficients] as + VideoMatrixCoefficients | undefined, + fullRange: !!info.vp9CodecInfo.videoFullRangeFlag, + }; + } + }; break; + + case 'av1': { + if (info.av1CodecInfo) { + return { + primaries: + COLOR_PRIMARIES_MAP_INVERSE[info.av1CodecInfo.colourPrimaries] as + VideoColorPrimaries | undefined, + transfer: + TRANSFER_CHARACTERISTICS_MAP_INVERSE[info.av1CodecInfo.transferCharacteristics] as + VideoTransferCharacteristics | undefined, + matrix: + MATRIX_COEFFICIENTS_MAP_INVERSE[info.av1CodecInfo.matrixCoefficients] as + VideoMatrixCoefficients | undefined, + fullRange: !!info.av1CodecInfo.videoFullRangeFlag, + }; + } + }; break; + + case 'prores': { + if (info.proresCodecInfo) { + return { + primaries: + COLOR_PRIMARIES_MAP_INVERSE[info.proresCodecInfo.colourPrimaries] as + VideoColorPrimaries | undefined, + transfer: + TRANSFER_CHARACTERISTICS_MAP_INVERSE[info.proresCodecInfo.transferCharacteristics] as + VideoTransferCharacteristics | undefined, + matrix: + MATRIX_COEFFICIENTS_MAP_INVERSE[info.proresCodecInfo.matrixCoefficients] as + VideoMatrixCoefficients | undefined, + fullRange: info.proresCodecInfo.fullRange, + }; + } + }; break; + } + + return { + primaries: undefined, + transfer: undefined, + matrix: undefined, + fullRange: undefined, + }; +}; + export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: number, sampleRate: number) => { if (codec === 'aac') { // If stereo or higher channels and lower sample rate, likely using HE-AAC v2 with PS diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts index ca3ba8b..ae07a7c 100644 --- a/src/isobmff/isobmff-boxes.ts +++ b/src/isobmff/isobmff-boxes.ts @@ -16,7 +16,7 @@ import { COLOR_PRIMARIES_MAP, TRANSFER_CHARACTERISTICS_MAP, MATRIX_COEFFICIENTS_MAP, - colorSpaceIsComplete, + colorSpaceIsEmpty, UNDETERMINED_LANGUAGE, assertNever, keyValueIterator, @@ -750,7 +750,9 @@ export const videoSampleDescription = ( ], [ VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec]?.(trackData) ?? null, pasp(trackData), - colorSpaceIsComplete(trackData.info.decoderConfig.colorSpace) ? colr(trackData) : null, + colorSpaceIsEmpty(trackData.info.decoderConfig.colorSpace) + ? null + : colr(trackData), ]); /** Pixel Aspect Ratio Box: Specifies pixel width:height spacing for non-square pixels. */ @@ -766,15 +768,28 @@ export const pasp = (trackData: IsobmffVideoTrackData) => { }; /** Colour Information Box: Specifies the color space of the video. */ -export const colr = (trackData: IsobmffVideoTrackData) => box('colr', [ - ascii(trackData.muxer.isQuickTime ? 'nclc' : 'nclx'), // Colour type - u16(COLOR_PRIMARIES_MAP[trackData.info.decoderConfig.colorSpace!.primaries!]), // Colour primaries - u16(TRANSFER_CHARACTERISTICS_MAP[trackData.info.decoderConfig.colorSpace!.transfer!]), // Transfer characteristics - u16(MATRIX_COEFFICIENTS_MAP[trackData.info.decoderConfig.colorSpace!.matrix!]), // Matrix coefficients - trackData.muxer.isQuickTime - ? [] // Doesn't have it - : u8((trackData.info.decoderConfig.colorSpace!.fullRange ? 1 : 0) << 7), // Full range flag -]); +export const colr = (trackData: IsobmffVideoTrackData) => { + const colorSpace = trackData.info.decoderConfig.colorSpace; + + return box('colr', [ + // Colour type + ascii(trackData.muxer.isQuickTime ? 'nclc' : 'nclx'), + + // Colour primaries + u16(colorSpace?.primaries != null ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 2), + + // Transfer characteristics + u16(colorSpace?.transfer != null ? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer] : 2), + + // Matrix coefficients + u16(colorSpace?.matrix != null ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 2), + + // Full range flag + trackData.muxer.isQuickTime + ? [] // Doesn't have it + : u8((colorSpace?.fullRange ? 1 : 0) << 7), + ]); +}; /** AVC Configuration Box: Provides additional information to the decoder. */ export const avcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoderConfig && box('avcC', [ @@ -811,17 +826,17 @@ export const vpcC = (trackData: IsobmffVideoTrackData) => { ? Number(parts[5]) : decoderConfig.colorSpace?.primaries ? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries] - : 2; // Default to undetermined + : 1; const transferCharacteristics = parts[6] ? Number(parts[6]) : decoderConfig.colorSpace?.transfer ? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer] - : 2; + : 1; const matrixCoefficients = parts[7] ? Number(parts[7]) : decoderConfig.colorSpace?.matrix ? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix] - : 2; + : 1; return fullBox('vpcC', 1, 0, [ u8(profile), // Profile diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 0e0fe60..48711f6 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -14,6 +14,7 @@ import { DTS_FOURCCS, DtsFourCc, extractAudioCodecString, + extractColorSpace, extractVideoCodecString, MediaCodec, OPUS_SAMPLE_RATE, @@ -26,12 +27,16 @@ import { } from '../codec'; import { Av1CodecInfo, + av1CodecInfoHasColorInfo, AvcDecoderConfigurationRecord, extractAv1CodecInfoFromPacket, + extractProresCodecInfoFromPacket, extractVp9CodecInfoFromPacket, FlacBlockType, HevcDecoderConfigurationRecord, + ProresCodecInfo, Vp9CodecInfo, + vp9CodecInfoHasColorInfo, parseEac3Config, getEac3SampleRate, getEac3ChannelCount, @@ -72,6 +77,8 @@ import { roundIfAlmostInteger, hexStringToBytes, HEX_STRING_REGEX, + EMPTY_COLOR_SPACE, + colorSpaceIsComplete, } from '../misc'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; import { buildIsobmffMimeType, parsePsshBoxContents, psshBoxesAreEqual, PsshBox } from './isobmff-misc'; @@ -151,12 +158,13 @@ type InternalTrack = { squarePixelHeight: number; codec: VideoCodec | null; codecDescription: Uint8Array | null; - colorSpace: VideoColorSpaceInit | null; + colorSpace: VideoColorSpaceInit; avcType: 1 | 3 | null; avcCodecInfo: AvcDecoderConfigurationRecord | null; hevcCodecInfo: HevcDecoderConfigurationRecord | null; vp9CodecInfo: Vp9CodecInfo | null; av1CodecInfo: Av1CodecInfo | null; + proresCodecInfo: ProresCodecInfo | null; proresFormat: ProresFourCc | null; }; } | { @@ -1027,12 +1035,13 @@ export class IsobmffDemuxer extends Demuxer { squarePixelHeight: -1, codec: null, codecDescription: null, - colorSpace: null, + colorSpace: { ...EMPTY_COLOR_SPACE }, avcType: null, avcCodecInfo: null, hevcCodecInfo: null, vp9CodecInfo: null, av1CodecInfo: null, + proresCodecInfo: null, proresFormat: null, }; } else if (handlerType === 'soun') { @@ -1472,6 +1481,12 @@ export class IsobmffDemuxer extends Demuxer { // Logic from https://aomediacodec.github.io/av1-spec/av1-spec.pdf const bitDepth = profile === 2 && highBitDepth ? (twelveBit ? 12 : 10) : (highBitDepth ? 10 : 8); + slice.skip(1); // Reserved bits + initial presentation delay + + // Parse config OBUs if there are any + const configObus = readBytes(slice, boxInfo.contentSize - 4); + const configObuInfo = extractAv1CodecInfoFromPacket(configObus); + track.info.av1CodecInfo = { profile, level, @@ -1481,6 +1496,10 @@ export class IsobmffDemuxer extends Demuxer { chromaSubsamplingX, chromaSubsamplingY, chromaSamplePosition, + videoFullRangeFlag: configObuInfo?.videoFullRangeFlag ?? 0, + colourPrimaries: configObuInfo?.colourPrimaries ?? 2, + transferCharacteristics: configObuInfo?.transferCharacteristics ?? 2, + matrixCoefficients: configObuInfo?.matrixCoefficients ?? 2, }; }; break; @@ -3410,11 +3429,16 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo } async getColorSpace(): Promise { + const decoderConfig = await this.getDecoderConfig(); + if (!decoderConfig) { + return this.internalTrack.info.colorSpace; + } + return { - primaries: this.internalTrack.info.colorSpace?.primaries, - transfer: this.internalTrack.info.colorSpace?.transfer, - matrix: this.internalTrack.info.colorSpace?.matrix, - fullRange: this.internalTrack.info.colorSpace?.fullRange, + primaries: decoderConfig.colorSpace?.primaries, + transfer: decoderConfig.colorSpace?.transfer, + matrix: decoderConfig.colorSpace?.matrix, + fullRange: decoderConfig.colorSpace?.fullRange, }; } @@ -3439,12 +3463,56 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo const firstPacket = await this.getFirstPacket({}); this.internalTrack.info.hevcCodecInfo = firstPacket && extractHevcDecoderConfigurationRecord(firstPacket.data); - } else if (this.internalTrack.info.codec === 'vp9' && !this.internalTrack.info.vp9CodecInfo) { + } else if ( + this.internalTrack.info.codec === 'vp9' + && ( + !this.internalTrack.info.vp9CodecInfo + // The codec info extracted from vpcC may claim the color space is "undefined" + || !vp9CodecInfoHasColorInfo(this.internalTrack.info.vp9CodecInfo) + ) + ) { const firstPacket = await this.getFirstPacket({}); - this.internalTrack.info.vp9CodecInfo = firstPacket && extractVp9CodecInfoFromPacket(firstPacket.data); - } else if (this.internalTrack.info.codec === 'av1' && !this.internalTrack.info.av1CodecInfo) { + const packetInfo = firstPacket && extractVp9CodecInfoFromPacket(firstPacket.data); + + if (packetInfo) { + this.internalTrack.info.vp9CodecInfo = { + ...(this.internalTrack.info.vp9CodecInfo ?? packetInfo), + videoFullRangeFlag: packetInfo.videoFullRangeFlag, + colourPrimaries: packetInfo.colourPrimaries, + transferCharacteristics: packetInfo.transferCharacteristics, + matrixCoefficients: packetInfo.matrixCoefficients, + }; + } + } else if ( + this.internalTrack.info.codec === 'av1' + && ( + !this.internalTrack.info.av1CodecInfo + // The codec info extracted from av1C may not contain color space information + || !av1CodecInfoHasColorInfo( + this.internalTrack.info.av1CodecInfo, + ) + ) + ) { const firstPacket = await this.getFirstPacket({}); - this.internalTrack.info.av1CodecInfo = firstPacket && extractAv1CodecInfoFromPacket(firstPacket.data); + const packetInfo = firstPacket && extractAv1CodecInfoFromPacket(firstPacket.data); + + if (packetInfo) { + this.internalTrack.info.av1CodecInfo = packetInfo; + } + } else if (this.internalTrack.info.codec === 'prores' && !this.internalTrack.info.proresCodecInfo) { + const firstPacket = await this.getFirstPacket({}); + this.internalTrack.info.proresCodecInfo + = firstPacket && extractProresCodecInfoFromPacket(firstPacket.data); + } + + if (!colorSpaceIsComplete(this.internalTrack.info.colorSpace)) { + const colorSpace = extractColorSpace(this.internalTrack.info); + + // Fill the missing values + this.internalTrack.info.colorSpace.primaries ??= colorSpace.primaries; + this.internalTrack.info.colorSpace.transfer ??= colorSpace.transfer; + this.internalTrack.info.colorSpace.matrix ??= colorSpace.matrix; + this.internalTrack.info.colorSpace.fullRange ??= colorSpace.fullRange; } const config: VideoDecoderConfig = { @@ -3452,7 +3520,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo codedWidth: this.internalTrack.info.width, codedHeight: this.internalTrack.info.height, description: this.internalTrack.info.codecDescription ?? undefined, - colorSpace: this.internalTrack.info.colorSpace ?? undefined, + colorSpace: this.internalTrack.info.colorSpace, }; if ( diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 8f397c1..d9e199d 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -12,6 +12,7 @@ import { extractDtsFourCcFromPacket, extractAvcDecoderConfigurationRecord, extractHevcDecoderConfigurationRecord, + extractProresCodecInfoFromPacket, extractVp9CodecInfoFromPacket, } from '../codec-data'; import { @@ -19,6 +20,7 @@ import { AudioCodec, DtsFourCc, extractAudioCodecString, + extractColorSpace, extractVideoCodecString, MediaCodec, OPUS_SAMPLE_RATE, @@ -40,6 +42,8 @@ import { assert, binarySearchLessOrEqual, COLOR_PRIMARIES_MAP_INVERSE, + colorSpaceIsComplete, + EMPTY_COLOR_SPACE, findLastIndex, isIso639Dash2LanguageCode, isThenable, @@ -220,7 +224,7 @@ type InternalTrack = { rotation: Rotation; codec: VideoCodec | null; codecDescription: Uint8Array | null; - colorSpace: VideoColorSpaceInit | null; + colorSpace: VideoColorSpaceInit; alphaMode: boolean; proresFormat: ProresFourCc | null; } @@ -1199,7 +1203,7 @@ export class MatroskaDemuxer extends Demuxer { rotation: 0, codec: null, codecDescription: null, - colorSpace: null, + colorSpace: { ...EMPTY_COLOR_SPACE }, alphaMode: false, proresFormat: null, }; @@ -1363,37 +1367,39 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.Colour: { if (this.currentTrack?.info?.type !== 'video') break; - this.currentTrack.info.colorSpace = {}; this.readContiguousElements(slice.slice(dataStartPos, size)); }; break; case EBMLId.MatrixCoefficients: { - if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break; + if (this.currentTrack?.info?.type !== 'video') break; const matrixCoefficients = readUnsignedInt(slice, size); - const mapped = MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients] ?? null; + const mapped = MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients]; this.currentTrack.info.colorSpace.matrix = mapped as VideoColorSpaceInit['matrix']; }; break; case EBMLId.Range: { - if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break; + if (this.currentTrack?.info?.type !== 'video') break; - this.currentTrack.info.colorSpace.fullRange = readUnsignedInt(slice, size) === 2; + const range = readUnsignedInt(slice, size); + this.currentTrack.info.colorSpace.fullRange = range === 1 || range === 2 + ? range === 2 + : undefined; }; break; case EBMLId.TransferCharacteristics: { - if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break; + if (this.currentTrack?.info?.type !== 'video') break; const transferCharacteristics = readUnsignedInt(slice, size); - const mapped = TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics] ?? null; + const mapped = TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics]; this.currentTrack.info.colorSpace.transfer = mapped as VideoColorSpaceInit['transfer']; }; break; case EBMLId.Primaries: { - if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break; + if (this.currentTrack?.info?.type !== 'video') break; const primaries = readUnsignedInt(slice, size); - const mapped = COLOR_PRIMARIES_MAP_INVERSE[primaries] ?? null; + const mapped = COLOR_PRIMARIES_MAP_INVERSE[primaries]; this.currentTrack.info.colorSpace.primaries = mapped as VideoColorSpaceInit['primaries']; }; break; @@ -2490,11 +2496,16 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid } async getColorSpace(): Promise { + const decoderConfig = await this.getDecoderConfig(); + if (!decoderConfig) { + return this.internalTrack.info.colorSpace; + } + return { - primaries: this.internalTrack.info.colorSpace?.primaries, - transfer: this.internalTrack.info.colorSpace?.transfer, - matrix: this.internalTrack.info.colorSpace?.matrix, - fullRange: this.internalTrack.info.colorSpace?.fullRange, + primaries: decoderConfig.colorSpace?.primaries, + transfer: decoderConfig.colorSpace?.transfer, + matrix: decoderConfig.colorSpace?.matrix, + fullRange: decoderConfig.colorSpace?.fullRange, }; } @@ -2517,6 +2528,7 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid const needsPacketForAdditionalInfo = this.internalTrack.info.codec === 'vp9' || this.internalTrack.info.codec === 'av1' + || this.internalTrack.info.codec === 'prores' // Packets are in Annex B format: || (this.internalTrack.info.codec === 'avc' && !this.internalTrack.info.codecDescription) // Packets are in Annex B format: @@ -2526,32 +2538,47 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid firstPacket = await this.getFirstPacket({}); } + const codecInfo = { + width: this.internalTrack.info.width, + height: this.internalTrack.info.height, + codec: this.internalTrack.info.codec, + codecDescription: this.internalTrack.info.codecDescription, + colorSpace: this.internalTrack.info.colorSpace, + avcType: 1 as const, // We don't know better (or do we?) so just assume 'avc1' + avcCodecInfo: this.internalTrack.info.codec === 'avc' && firstPacket + ? extractAvcDecoderConfigurationRecord(firstPacket.data) + : null, + hevcCodecInfo: this.internalTrack.info.codec === 'hevc' && firstPacket + ? extractHevcDecoderConfigurationRecord(firstPacket.data) + : null, + vp9CodecInfo: this.internalTrack.info.codec === 'vp9' && firstPacket + ? extractVp9CodecInfoFromPacket(firstPacket.data) + : null, + av1CodecInfo: this.internalTrack.info.codec === 'av1' && firstPacket + ? extractAv1CodecInfoFromPacket(firstPacket.data) + : null, + proresCodecInfo: this.internalTrack.info.codec === 'prores' && firstPacket + ? extractProresCodecInfoFromPacket(firstPacket.data) + : null, + proresFormat: this.internalTrack.info.proresFormat, + }; + + if (!colorSpaceIsComplete(this.internalTrack.info.colorSpace)) { + const colorSpace = extractColorSpace(codecInfo); + + // Fill the missing values + this.internalTrack.info.colorSpace.primaries ??= colorSpace.primaries; + this.internalTrack.info.colorSpace.transfer ??= colorSpace.transfer; + this.internalTrack.info.colorSpace.matrix ??= colorSpace.matrix; + this.internalTrack.info.colorSpace.fullRange ??= colorSpace.fullRange; + } + const config: VideoDecoderConfig = { - codec: extractVideoCodecString({ - width: this.internalTrack.info.width, - height: this.internalTrack.info.height, - codec: this.internalTrack.info.codec, - codecDescription: this.internalTrack.info.codecDescription, - colorSpace: this.internalTrack.info.colorSpace, - avcType: 1, // We don't know better (or do we?) so just assume 'avc1' - avcCodecInfo: this.internalTrack.info.codec === 'avc' && firstPacket - ? extractAvcDecoderConfigurationRecord(firstPacket.data) - : null, - hevcCodecInfo: this.internalTrack.info.codec === 'hevc' && firstPacket - ? extractHevcDecoderConfigurationRecord(firstPacket.data) - : null, - vp9CodecInfo: this.internalTrack.info.codec === 'vp9' && firstPacket - ? extractVp9CodecInfoFromPacket(firstPacket.data) - : null, - av1CodecInfo: this.internalTrack.info.codec === 'av1' && firstPacket - ? extractAv1CodecInfoFromPacket(firstPacket.data) - : null, - proresFormat: this.internalTrack.info.proresFormat, - }), + codec: extractVideoCodecString(codecInfo), codedWidth: this.internalTrack.info.width, codedHeight: this.internalTrack.info.height, description: this.internalTrack.info.codecDescription ?? undefined, - colorSpace: this.internalTrack.info.colorSpace ?? undefined, + colorSpace: this.internalTrack.info.colorSpace, }; if ( diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index dc213a8..01b2dde 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -14,7 +14,7 @@ import { UNDETERMINED_LANGUAGE, assert, assertNever, - colorSpaceIsComplete, + colorSpaceIsEmpty, imageMimeTypeToExtension, keyValueIterator, normalizeRotation, @@ -405,29 +405,31 @@ export class MatroskaMuxer extends Muxer { (hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayHeight, data: trackData.info.aspectRatio!.den } : null), (hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayUnit, data: 3 } : null), // 3 = display aspect ratio trackData.info.alphaMode ? { id: EBMLId.AlphaMode, data: 1 } : null, - (colorSpaceIsComplete(colorSpace) - ? { + (colorSpaceIsEmpty(colorSpace) + ? null + : { id: EBMLId.Colour, data: [ { id: EBMLId.MatrixCoefficients, - data: MATRIX_COEFFICIENTS_MAP[colorSpace.matrix], + data: colorSpace?.matrix != null ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 2, }, { id: EBMLId.TransferCharacteristics, - data: TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer], + data: colorSpace?.transfer != null + ? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer] + : 2, }, { id: EBMLId.Primaries, - data: COLOR_PRIMARIES_MAP[colorSpace.primaries], + data: colorSpace?.primaries != null ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 2, }, { id: EBMLId.Range, - data: colorSpace.fullRange ? 2 : 1, + data: colorSpace?.fullRange != null ? (colorSpace.fullRange ? 2 : 1) : 0, }, ], - } - : null), + }), (flippedRotation ? { id: EBMLId.Projection, diff --git a/src/media-sink.ts b/src/media-sink.ts index 0edc4dd..f34b761 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -29,6 +29,7 @@ import { assertNever, CallSerializer, clamp, + colorSpaceIsComplete, getInt24, getUint24, insertSorted, @@ -944,20 +945,42 @@ class VideoDecoderWrapper extends DecoderWrapper { } }; - if (codec === 'avc' && this.decoderConfig.description && isChromium()) { - // Chromium has/had a bug with playing interlaced AVC (https://issues.chromium.org/issues/456919096) - // which can be worked around by requesting that software decoding be used. So, here we peek into the - // AVC description, if present, and switch to software decoding if we find interlaced content. - const record = deserializeAvcDecoderConfigurationRecord(toUint8Array(this.decoderConfig.description)); - if (record && record.sequenceParameterSets.length > 0) { - const sps = parseAvcSps(record.sequenceParameterSets[0]!); - if (sps && sps.frameMbsOnlyFlag === 0) { - this.decoderConfig = { - ...this.decoderConfig, - hardwareAcceleration: 'prefer-software', - }; + if (isChromium()) { + if (codec === 'avc' && this.decoderConfig.description) { + // Chromium has/had a bug with playing interlaced AVC (https://issues.chromium.org/issues/456919096) + // which can be worked around by requesting that software decoding be used. So, here we peek into + // the AVC description, if present, and switch to software decoding if we find interlaced content. + const record = deserializeAvcDecoderConfigurationRecord( + toUint8Array(this.decoderConfig.description), + ); + if (record && record.sequenceParameterSets.length > 0) { + const sps = parseAvcSps(record.sequenceParameterSets[0]!); + if (sps && sps.frameMbsOnlyFlag === 0) { + this.decoderConfig = { + ...this.decoderConfig, + hardwareAcceleration: 'prefer-software', + }; + } } } + + if (!colorSpaceIsComplete(this.decoderConfig.colorSpace)) { + // Found via https://github.com/remotion-dev/remotion/issues/10841. + // If the color space is incomplete (which is often that it's just partially filled), Chromium has + // some nasty logic where it doesn't pass that information along to the GPU at all. The result is + // that information is genuinely lost, like the color matrix for example. Chromium has other code + // paths where it just fills the missing values with a hardcoded default, so we do the exact same + // thing here, with the same hardcoded defaults: + this.decoderConfig = { + ...this.decoderConfig, + colorSpace: { + primaries: this.decoderConfig.colorSpace?.primaries ?? 'bt709', + matrix: this.decoderConfig.colorSpace?.matrix ?? 'bt709', + transfer: this.decoderConfig.colorSpace?.transfer ?? 'bt709', + fullRange: this.decoderConfig.colorSpace?.fullRange ?? false, + }, + }; + } } const stack = new Error('Decoding error').stack; diff --git a/src/misc.ts b/src/misc.ts index 3a247d3..ac89a48 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -162,6 +162,25 @@ export const colorSpaceIsComplete = ( ); }; +export const colorSpaceIsEmpty = (colorSpace: VideoColorSpaceInit | undefined) => { + return ( + !colorSpace + || ( + colorSpace.primaries == null + && colorSpace.transfer == null + && colorSpace.matrix == null + && colorSpace.fullRange == null + ) + ); +}; + +export const EMPTY_COLOR_SPACE: VideoColorSpaceInit = { + primaries: undefined, + transfer: undefined, + matrix: undefined, + fullRange: undefined, +}; + export const isAllowSharedBufferSource = (x: unknown) => { return ( x instanceof ArrayBuffer diff --git a/test/browser/color-space.test.ts b/test/browser/color-space.test.ts new file mode 100644 index 0000000..880d4fd --- /dev/null +++ b/test/browser/color-space.test.ts @@ -0,0 +1,222 @@ +import { expect, test } from 'vitest'; +import { VideoCodec } from '../../src/codec.js'; +import { ALL_FORMATS } from '../../src/input-format.js'; +import { Input } from '../../src/input.js'; +import { EncodedPacketSink } from '../../src/media-sink.js'; +import { EncodedVideoPacketSource } from '../../src/media-source.js'; +import { assert, colorSpaceIsComplete } from '../../src/misc.js'; +import { Output } from '../../src/output.js'; +import { MkvOutputFormat, Mp4OutputFormat } from '../../src/output-format.js'; +import { EncodedPacket } from '../../src/packet.js'; +import { BufferSource, Source, UrlSource } from '../../src/source.js'; +import { BufferTarget } from '../../src/target.js'; + +test('Color space extraction, AVC in MP4', async () => { + const source = await readPackets('/video.mp4'); + const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source); + + await expectColorSpace(new BufferSource(buffer), { + primaries: undefined, + transfer: undefined, + matrix: 'bt470bg', + fullRange: true, + }); +}); + +test('Color space extraction, HEVC in MP4', async () => { + const source = await readPackets('/video-h265.mp4'); + const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source); + + await expectColorSpace(new BufferSource(buffer), { + primaries: undefined, + transfer: undefined, + matrix: 'bt470bg', + fullRange: true, + }); +}); + +test('Color space extraction, VP9 in MP4', async () => { + const source = await encodePackets('vp9', 'vp09.00.10.08'); + const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source); + + await expectCompleteColorSpace(new BufferSource(buffer)); +}); + +test('Color space extraction, AV1 in MP4', async () => { + const source = await encodePackets('av1', 'av01.0.04M.08'); + const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source); + + await expectCompleteColorSpace(new BufferSource(buffer)); +}); + +test('Color space extraction, AVC in Matroska', async () => { + const source = await readPackets('/video.mp4'); + const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source); + + await expectColorSpace(new BufferSource(buffer), { + primaries: undefined, + transfer: undefined, + matrix: 'bt470bg', + fullRange: true, + }); +}); + +test('Color space extraction, HEVC in Matroska', async () => { + const source = await readPackets('/video-h265.mp4'); + const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source); + + await expectColorSpace(new BufferSource(buffer), { + // The SPS only signals the matrix and the range, leaving the rest unspecified + primaries: undefined, + transfer: undefined, + matrix: 'bt470bg', + fullRange: true, + }); +}); + +test('Color space extraction, VP9 in Matroska', async () => { + const source = await encodePackets('vp9', 'vp09.00.10.08'); + const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source); + + await expectCompleteColorSpace(new BufferSource(buffer)); +}); + +test('Color space extraction, AV1 in Matroska', async () => { + const source = await encodePackets('av1', 'av01.0.04M.08'); + const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source); + + await expectCompleteColorSpace(new BufferSource(buffer)); +}); + +const readPackets = async (path: string) => { + using input = new Input({ + source: new UrlSource(path), + formats: ALL_FORMATS, + }); + + const track = await input.getPrimaryVideoTrack(); + assert(track); + + const codec = await track.getCodec(); + assert(codec); + + const decoderConfig = await track.getDecoderConfig(); + assert(decoderConfig); + + const sink = new EncodedPacketSink(track); + const packets: EncodedPacket[] = []; + + for await (const packet of sink.packets()) { + packets.push(packet); + + if (packets.length === 10) { + break; + } + } + + return { codec, decoderConfig, packets }; +}; + +const encodePackets = async (codec: VideoCodec, codecString: string) => { + const width = 320; + const height = 240; + + const canvas = new OffscreenCanvas(width, height); + const context = canvas.getContext('2d')!; + + let decoderConfig: VideoDecoderConfig | null = null; + const packets: EncodedPacket[] = []; + + // We manually go through WebCodecs here so we can intercept the decoder config + const encoder = new VideoEncoder({ + output: (chunk, metadata) => { + decoderConfig ??= metadata?.decoderConfig ?? null; + packets.push(EncodedPacket.fromEncodedChunk(chunk)); + }, + error: (error) => { + throw error; + }, + }); + encoder.configure({ codec: codecString, width, height }); + + for (let i = 0; i < 10; i++) { + context.fillStyle = `hsl(${36 * i}, 100%, 50%)`; + context.fillRect(0, 0, width, height); + context.fillStyle = 'black'; + context.fillRect(20 * i, 10 * i, 80, 80); + + const frame = new VideoFrame(canvas, { timestamp: i * 1e6 / 30, duration: 1e6 / 30 }); + encoder.encode(frame, { keyFrame: i === 0 }); + frame.close(); + } + + await encoder.flush(); + encoder.close(); + + assert(decoderConfig); + + return { codec, decoderConfig, packets }; +}; + +const remuxWithoutColorSpace = async ( + format: Mp4OutputFormat | MkvOutputFormat, + source: { codec: VideoCodec; decoderConfig: VideoDecoderConfig; packets: EncodedPacket[] }, +) => { + const output = new Output({ + format, + target: new BufferTarget(), + }); + + const videoSource = new EncodedVideoPacketSource(source.codec); + output.addVideoTrack(videoSource); + + await output.start(); + + const decoderConfig = { ...source.decoderConfig }; + delete decoderConfig.colorSpace; + + let isFirstPacket = true; + for (const packet of source.packets) { + await videoSource.add(packet, isFirstPacket ? { decoderConfig } : undefined); + isFirstPacket = false; + } + + await output.finalize(); + + const buffer = output.target.buffer; + assert(buffer); + + return buffer; +}; + +const expectColorSpace = async (source: Source, expected: VideoColorSpaceInit) => { + using input = new Input({ + source, + formats: ALL_FORMATS, + }); + + const track = await input.getPrimaryVideoTrack(); + assert(track); + + const decoderConfig = await track.getDecoderConfig(); + assert(decoderConfig); + + expect(await track.getColorSpace()).toEqual(expected); + expect({ ...decoderConfig.colorSpace }).toEqual(expected); +}; + +const expectCompleteColorSpace = async (source: Source) => { + using input = new Input({ + source, + formats: ALL_FORMATS, + }); + + const track = await input.getPrimaryVideoTrack(); + assert(track); + + const decoderConfig = await track.getDecoderConfig(); + assert(decoderConfig); + + expect(colorSpaceIsComplete(await track.getColorSpace())).toBe(true); + expect(colorSpaceIsComplete(decoderConfig.colorSpace)).toBe(true); +}; From 3c4c2a459c0686bbfa0e40c5685aa2efe13e37c8 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:53:42 +0200 Subject: [PATCH 3/5] Remove Chromium version check --- src/codec-data.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/codec-data.ts b/src/codec-data.ts index 5d80fee..9afcab2 100644 --- a/src/codec-data.ts +++ b/src/codec-data.ts @@ -22,7 +22,6 @@ import { textEncoder, toDataView, toUint8Array, - getChromiumVersion, isChromium, popcount, setUint24, @@ -2412,8 +2411,8 @@ export const determineVideoPacketType = ( // In addition to IDR, Recovery Point SEI also counts as a valid H.264 keyframe by current consensus. // See https://github.com/w3c/webcodecs/issues/650 for the relevant discussion. WebKit and Firefox have - // always supported them, but Chromium hasn't, therefore the (admittedly dirty) version check. - if (type === AvcNalUnitType.SEI && (!isChromium() || getChromiumVersion()! >= 144)) { + // always supported them, but Chromium hasn't. + if (type === AvcNalUnitType.SEI && !isChromium()) { const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length); const bytes = removeEmulationPreventionBytes(nalUnit); let pos = 1; // Skip NALU header From da18ceeb24d2c2f9207f9510cf5d6e16cb19d909 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:08:57 +0200 Subject: [PATCH 4/5] Add smpte170m vp9 test for CI stability, add missing VP9 color spaces in parser --- src/codec-data.ts | 34 +++++++++++++------------------ test/browser/color-space.test.ts | 18 ++++++++++++---- test/public/vp9-smpte170m.webm | Bin 0 -> 10178 bytes 3 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 test/public/vp9-smpte170m.webm diff --git a/src/codec-data.ts b/src/codec-data.ts index 9afcab2..acb5988 100644 --- a/src/codec-data.ts +++ b/src/codec-data.ts @@ -1726,6 +1726,16 @@ export type Vp9CodecInfo = { matrixCoefficients: number; }; +// The color spaces from section 7.2.2 of the VP9 spec The matrix column is taken from FFmpeg's libavcodec/vp9.c +const VP9_COLOR_SPACE_TABLE = { + 1: { colourPrimaries: 5, transferCharacteristics: 6, matrixCoefficients: 5 }, // CS_BT_601 + 2: { colourPrimaries: 1, transferCharacteristics: 1, matrixCoefficients: 1 }, // CS_BT_709 + 3: { colourPrimaries: 6, transferCharacteristics: 6, matrixCoefficients: 6 }, // CS_SMPTE_170 + 4: { colourPrimaries: 7, transferCharacteristics: 7, matrixCoefficients: 7 }, // CS_SMPTE_240 + 5: { colourPrimaries: 9, transferCharacteristics: 14, matrixCoefficients: 9 }, // CS_BT_2020 + 7: { colourPrimaries: 1, transferCharacteristics: 13, matrixCoefficients: 0 }, // CS_RGB (sRGB) +}; + export const extractVp9CodecInfoFromPacket = ( packet: Uint8Array, ): Vp9CodecInfo | null => { @@ -1834,26 +1844,10 @@ export const extractVp9CodecInfoFromPacket = ( } } - // Map color_space to standard values - const matrixCoefficients = colorSpace === 7 - ? 0 - : colorSpace === 2 - ? 1 - : colorSpace === 1 - ? 6 - : 2; - - const colourPrimaries = colorSpace === 2 - ? 1 - : colorSpace === 1 - ? 6 - : 2; - - const transferCharacteristics = colorSpace === 2 - ? 1 - : colorSpace === 1 - ? 6 - : 2; + const colorSpaceValues = VP9_COLOR_SPACE_TABLE[colorSpace as keyof typeof VP9_COLOR_SPACE_TABLE]; + const colourPrimaries = colorSpaceValues?.colourPrimaries ?? 2; + const transferCharacteristics = colorSpaceValues?.transferCharacteristics ?? 2; + const matrixCoefficients = colorSpaceValues?.matrixCoefficients ?? 2; return { profile, diff --git a/test/browser/color-space.test.ts b/test/browser/color-space.test.ts index 880d4fd..c54046d 100644 --- a/test/browser/color-space.test.ts +++ b/test/browser/color-space.test.ts @@ -36,10 +36,15 @@ test('Color space extraction, HEVC in MP4', async () => { }); test('Color space extraction, VP9 in MP4', async () => { - const source = await encodePackets('vp9', 'vp09.00.10.08'); + const source = await readPackets('/vp9-smpte170m.webm'); const buffer = await remuxWithoutColorSpace(new Mp4OutputFormat(), source); - await expectCompleteColorSpace(new BufferSource(buffer)); + await expectColorSpace(new BufferSource(buffer), { + primaries: 'smpte170m', + transfer: 'smpte170m', + matrix: 'smpte170m', + fullRange: false, + }); }); test('Color space extraction, AV1 in MP4', async () => { @@ -75,10 +80,15 @@ test('Color space extraction, HEVC in Matroska', async () => { }); test('Color space extraction, VP9 in Matroska', async () => { - const source = await encodePackets('vp9', 'vp09.00.10.08'); + const source = await readPackets('/vp9-smpte170m.webm'); const buffer = await remuxWithoutColorSpace(new MkvOutputFormat(), source); - await expectCompleteColorSpace(new BufferSource(buffer)); + await expectColorSpace(new BufferSource(buffer), { + primaries: 'smpte170m', + transfer: 'smpte170m', + matrix: 'smpte170m', + fullRange: false, + }); }); test('Color space extraction, AV1 in Matroska', async () => { diff --git a/test/public/vp9-smpte170m.webm b/test/public/vp9-smpte170m.webm new file mode 100644 index 0000000000000000000000000000000000000000..06c83ef0c617f2f85de9d450bc1bb23f9355a0f5 GIT binary patch literal 10178 zcmcI~^-mpK)9pF9JH?A@DemrW2P^Jc+`VvccXxM6p+M1tySuwn+@1Tp@AD=1et*E7 z%J=e9hO` z%J~%t6ln!tW`dzKe#L9Fg5fmk{)x7Kq6G~6j}#&SE-v||T>+LunUY0el1k#r5|XMC zVgH+ri_3~>SCr}heb;iR>=XNrWY_u<-rYnb=tV#{i__O>X9pLolG|n+g~J2ndu75;MZSM7xLLp(4f?10VzZ z*Pn&r4xQdU;x0%YA?tU-X$NN>-yb)t7M|l2ul(`)-!_SEUzKN%A+&pYo3wA}!_R)) zeXqI9^S*Jz*M68(6Gu<)ZSQTfohu%OHNHX>7anD_uV{^DeCH1v?@HdMrso}HI>#Fi zl|CCef2+z`?{oOK@09gtJ0w@`bC_4$)-Pr|Oo6#BW{z>wf4#ZF?;z#TRZrfiXPo}1 zW-+p&KcAyEm)^;6zl$OoR#5@`dovc|x`@TAcU0XkYN~42I}pe}4R%En_O95G3Z49d zVxs7xMSAsVr2uONd47beozrp6+7{~cp6fT}t<&h$v>TJFA>9-)#MCS4yGl~$tLA2A zU%sa$8$H&KE7{hS(1sSo8gN}t597|S`0~7U8*InGFA=p{T zQln~q7*l^@Pze+ypq@XTyz01nA3WSDOc_~KO0${Ct5*Uluo=?}wdu(cb=*|5wW`y} z72_6IJloX&N_)|B`cwBOOvlBU&bTYSxerO^YX)w3a|JA`qKGy1%3P|Y`mP?%Lr0yP zF81?}lVCF|9CuxVLoW|uN_JHZ{YpNMs;|f{M;J!#dEKtIwCP>fnGV-|lUWZL6J&Rj zX~AbkI3BI6aIIZHLh%6Cb=4QTD@EReGWr>OLkzfaDkp`%&GF6i@nxoHUIi=pkW#f! zTPfK7FeHeCvt=*4C=t}LYA1wrDwuk1mHX7Ga-G_x3^+uditB|g!4Ve*%U_5?9Px3G zq}9$_`}fy_GPbLP%FYa-72i9}&(Z2_S{AIIH3b|B{BziQ zIYQe@^z7#o=veDf7zU`Y=WfF#ds*Gj{tMvII7}FVN2?!$@7+gRVoAbqBa!eV);n)t zFqHI#8)D!NI>$s1QW;s~LsQ7H+HuTO*9I`^MZTn|IdU#65KpQ6DuHinMhONW$qZ zlD>rd1ZvQFYRg)uN~zts5})g^-ywbigx2bvjOUHSh;@= zK{RxwAt|Ms!uUbex7$6VsMj~bWtITE%9PHm^uK*>87RIweOR zpn-{gQISEty3|GClDYU)KtJ#J?jlc1JpMd;%D@}B5!>{ef*73t3rlSSE zQuv6OdEJ16f@Vdv^5anegQDI;SGN|#*%p46E?M2wy2tp70slLFeWt$1@Jg3zRDik6@vi6utl!WKe2Fdo?+e8>e^r zW|Q9L8be+M< z7yT~sb$6G2ap@8thnl0!A-=vsDQ-9diDqoRN)%g{7=i;YS~smT0GPf6p~srG`%f?a zXVOe`ZQgFRvGjan=&N>5q3;K1XVt8&coAL7-`7hItchfA(4b#$fAd+#smO9|gc;LI z44D+in>8*x(t%t6Z&ZHYcva=OKgVslVEZ>ljh7!Muo?o zTElG@YV&E1CJBt`%K61*$RF52{Zvb82V#WxA3la(6?b_9WKk|c%sf1}&h}o2l(6Xl z1z+@XDA4gM#km$z>kdvUy_on})DBL_f9pU3vWf)EgA4P|P}UP0W>@)mKE&50kWxYz z7La!n91=~4((HvQzjKZM)MOy_Yy!{d0^ce~I2RKADT^e70JH5}ML?SaLn(eK}PC-nh5c5Sgns$pq`?0h)b(@svAP;!?`U>f*@+sE!LT8 z!l-*2bM~O$?|>hI5lJURn?YvX7BKU8mDfRow=N;FyhB+c!tW5W4$h)C-AqPHVtq=~ zVrrQ+kR&YUGh8<$%N#dVagE3Fq7r2+u(rAM z6Yj<-mUoD(y4K6UHZx{!O)H6g%;%1ZJC4DF&ygnHB}8|k1TDoyC>@}HocY|w7T
F@A;OY(bGZ-N%}oPG-u|)8+@5|9qMb2eQIFlBq8SyKg`!1FiZCsEAb!CjS_BY|WBNv|-%2_}!D zY^qj$XfE?HoQo8R3Mt>pOH%yWMcx|387@LJ2KcF#FUlh4!(y?|86sdO4f+H4Wr~P< z7BhIf6%Zp-T&$9>RtT@7bZx?~>YqbXe%*X2%w>_YOT4;5pChuEt(&WW^tt7IW#dD{ zRcKdsP#}N#A-fa9>a4I$Rw+ytm8g;?jIdRmpU}{H`?qe4yww7f>DDb>Uuyoqi+-MI z12Nw0dp_hbyiZr$Xxp(`zt$fA`{e)tU&VENO*Z!VRzo{I9Mp=%pAVm%A`qA(yIm5A zc`Y2gwR*CdVm1HC!B9{=HD%XDGScxT?=xK4 z>X-QAaMuk;V3T!}NS}YOS3^E}-tvGp#uMy)#CFh4c*|?8bxg4nL+t z!H5yBL`(M&#sP#c*z2-=>EB8b58Vf)wDt|*g5*HLb8;cH(2$t z7|^9qJ~$*_!^75|QfzuGE@#8Un^pNcK|UdW5PX*Sx>j2M+dKU+ zv70zbh){kQ=i)3N_3P4cI5XIsy?uMOYN6Z2X{H$sH5f%sKO|~F_OIEl;KF{f=B2+s zff%OJ2+u(4CPqj6y_eCbTq3JE!7GU{K&}Mg{2X@)LI9T+Y9-AjT>JU_0QwtUkny4D zMS$3x*>WgzPKi@r#>PHZex;A15f9Q;b0<9jWQ+>eU*8r8AZghw8cd zlmbQA*$`!X*YK4mipm6!->g^%BlPUc*pPs7z8Q9QP70ZJEfPBzCvu?0`^zE(VYp{~ zku=La_;W`4F1DO<@$aBcH50thc#MI|9}bG^&^5**KnVA-Y-SIlDcMhr3}$+`3z2RE zFDww1BD64tCGNEP`|WnCOS6H(f-;8J$8%h1=7%$f=34o0ALsA(@xMlTX?1%-l)n7@ zonG%66%8?&t(ig&+OYFO)JPtpr~g%vfp5@soq0RM?pyS=RoTZh`rNKsw$RXzs>@D1 zxrAs|_)u_A&NL%isL{BoKq~kt0dO^jPA4`5p`imOh^Q_Yr@igJ+Pv={ zk8i~4s7GuJ@QhxTr@gMDB2))vAVnIBj?T=dLVxXa)A#gz)ff`oh6^^+0mOM;LsWCJl^lnX(TXfj6>kP?u@G?phMM3 z87Xns3^x!A z5P1qCd}7@4v0KpIInt?mjIU1tAD7lwFDu%u?pkl@&kGFkU=K|J@%mlLqhKIWk8-6J z*hsRr(MSA~hplLZld)emuauiUJTgXP4gI~0U`w!WL*Pk);RW3HIWpqvHCqZ)Xd779 z=M8Rp7O9`&6TTz!8BYX9x?-bNvZ=%#tm3Zr=D^;c>520(MB!K2YxK->!bVU_hf>g! zYxLT5se5NzJ->HG)b{@JoB9}eG_&E9fwdkLI^ox{^;w1>6 zXl0%RcW2)2RYnM+?bbT#)z0up~J#N7)#=oC9F!DmR=}~RPFlY<6(bC zBdvlPsA3d|qb$fF`>4QFqH|DMX;oZIZcyP8iBEKyx!ozdECJTC`AOJ4wsaqpDSyp~qow5;C+IlSXpAdMBdwf^1*L;gH^+Xj^$ z$J2#pPtRVgYh6E}E4so2hOPPqI-0;#Z8=~R!QC?Tf7uY@I&V;-AIFkV^d zyQNUfbr_P<)$S}C%n|sF2sp<4oe4+Vemz_RrQiZdm1O5l?$%!UN_j}s(87#aCeKn- zTgx(a&kYP$`FBhps8}1tR`pIzh8N$gsSxBak z0tn~2nUG%B@4%_*B$75x)GS`D1*RA_5i3{IsiIW73}JDvtcZ>%7!AKX`VbYQDF-dy zIa)1ekBDL4Mt$h|n*@nlBu6DWAhX8@*V;M@hh%WIss3~$a*XS~Z3tSt-KxGIvt*x= z6b8(Xal3twBMqt^)98{BdR^dKNGl1TSTT`N-$F`v8T!oMQJAuFoh=cKgw@>HB*pM{ zBU-INOKe36Y9WwMf{=S{X?X2%=rIF}X5g`MY3k0_Sw07)a1}3c5PfN=DrH7(*!f-m znWA}x@l&Uo|J6s@x8ZW>96r4fOcTQ3D2^G%=DTsel8%m8pG#F$_0nG~(!*&kcRUfB zr+-n;;T%lO+LhViw#HEfclX3}P z-q{ODynVqv&}0s;;N)IR6Nl;;pvMIudd2&$ndu= zGw%H;7ntO{18slDMRr{@^zO}u$7^)!M z&swhw_)rVsJHx6vZ;~LYzxr!IR4HzsN0AS)lRgf944P#s5}7$h6$ zG|1dfW_jO!^H+#ZGmx0FKboV>r(THsR3Rdffb3T@$OqDxo<&;bu(shPOi{yrW%I-n z%_T_dk%&)F6{hw4Wad%^eR~w#RXwG>=-96>-~Qf-STt7K8!=9bUtEa4{)p*F;650! z+b~kZoDSd^uz_$)LVj^{!ug&AM`AzfvmU5lKqI64D3VaCDdIKlH)+oZFap_A!|}{A@~>yrd|1p4@$Pnaewd*MN0k3?tgQ|5@5ZC(s9 z9pxx7=;wByglK#rUG2TQNTF z)_ka)p>>25oSH&pp+$3#b{U!RELX}ClRJGwT*wky*N(BN^E!JVDB(I?7l936PPzYM z1sju9g}<4?U5r1&bNvdexyAVX`eOLgK>^6FlI`Ox@T}-|a=r)(wGxY}?I{Ekhjfu| zBtwdy2U{SDhZ?DaBn>Gk>?gX3>jZ>cd6wYuMC~pakyF^bEev7EyR4|Bwv)fEW}7`b zeybzjtz7jyuRyI?Qb_#Rvk1fciuRH%q)@=mzW58qDBxTe0n2YdXFrTq=AwrGA%3`ztR4b8%^>prdeq0Ng5E~%( zjZ}#HT4(?1F)@#mek^!*<5~R5w*ycleFp}R1402JiI<|(06@I|;M4wto5UH3ccHAA zi|}jS=5_87wJJzY+9=Zx_VvfWDjC54!icY6*|F132z!95kj{m~XRX&JB=N8Z-^N$u z5EL%*8K3Vl^Se-iGL7CpBVj6N6tfWH7jJn*P8N&wN3zt=U7oN#pdj>Ip^bIy*rU0w z6LU-C%XUAHPVKigwlZPLn9(x7ru}x7{3pStpJ+LOM$(!#GYuHUKOG?~=8_Q6NBM=S8q_yW{hvWB@`nGjg3mYdzJlQ2V(M)nKdSRzq0c z*UAKc5?X8H2a5*~Tf|sXPbAoIWAD}sakHh;L$?3kd5xmwM6*QrYur6=TtEkgA`wr4 zQEpN!Mn4kMkSfo_;&LE{Vk_{??~xk~yxX@sE#_|c^)hGk;Y}I}M$?6(ED7n`T4=fB zyenL#k{C%^0KrM+Ex){?7nS(1u;+{t@BA}x@>A@F7~Z3-$*Z0a)=0Q%>68Yvff!x` zhi(}eCbx$6*FqjP-vz60nOQ;e~H8*v0}i?fVlha2=y62PMR`g4`ftP2F5KRT(Ri!Z_?jzJY}av6Ypum?S}r$5YMAzUT2ZqXtc%U z1OmZHtm6+C_$2RrAX}1dvH(XZA+*`=u))`-CZjO!#qORXouDaPV@UQvK9^>KO@MZ^ zrx;J8X*B_Q;q>r!YE7wwrP)SG8;$pPtGaNR${OHl>q(b z3VZS+D#_UeQ+)mWk^-;vVk0GtQi5SU!MxLgh<0hEhP|tVt`5E))+3T5QlYtZ)5G2j z^zE!osvXsJ9%h2T6tVJAPrkija45PRyr*Y(;9!Sz*g{OsXj}NDk71(hsjNI9X*aP^ z0HyS;Z+4Nc2O)?&Dl-k&*^#?l6dxn@aTzV=8Z_d}|xXQ>q^-axh}fIWE&u6JEl-$MiaX z>#rp{e-wJ25;F49xxg)AxmFI#V*U^-g`c(xp5gI82f1xZ_2NTrorbCc7G`RTIAQzz zrZ*lzecM9}GL501e9Oe2)sZbhvaR3v7S>tfF3#$z#Id=>g~?yGh%f7?ppaD@$|zG? z2WY!Q6p#lZtpuVIo?Ym}zP1a$(}e^NB9(diF!@v%NIj&M!y6}oKa=-5IhPS8em3%2 z%un6BzMSrG%YoG}SvHQ|3FbSp0~Z2oZBYX0$!ql=SdC&eNmrU*7Dmd5klknYZouATQr1##J7ehk{>XAY96GIJ zyQr5F-K7xyxgI{|kIk}90t%+pt<5elGVW1y75CR$NgmcM1IDZcw&T}$7Gn*|$t8c4at)V@g%f`dr zyv`LN>t}qmY14DCDAoM+sJA^E!C@EOMU?pBIw^UZz*TSUFd02}G-vb44u z1i~eG+w3Nx-Ht*}!9O)5L}E^tB6O`B2?Scr$vs~2mdiZ?ezn@#_5t0gEPwON^L{GiUJxe5*1>+T;Vo^L?4dv0Y{buZ z5(+0J>OWdI&gh+}Ij?9k^>ojah-6|TZh$~ft7HTEIv@AIhe)c+dU?FTkjDsiUQw;` zP=zXqBTKcZDEjlKl7?`mKT^k)ksRsIV%}-T3*VY=qg~aQSa?C5Q!f2##~(M)>X#&9 z{H>%GeGsI?i!hLl6&Wd1Q5Gs=tS#ZqbWc2e-w08;Kqxk9w;D-bk*K6zgJ5HQ@ugW?-ii z=m;hX1%eP*1tW!@kY|1i{&GQP(qiBQTEuMEvM^niY_CgKIqhOyuTQo;@QXk^zfleb>~XR29p#tAzbM zANZ&(PSA4Utc5BjZ(u|}#T4L-PjuC)aC*2O%>MmR+eQEYv3sa4K$aTt;|Lt-K!MAG7y!nNc2Apysf~T@<+z3Y=9fKGN&olb zhYA{;OITOzxurYkk&F6opEGOT>0$S;qc@_dl~ z-WQr*S6Ay1OOzUs>ODn)Duer4;J;s6n{#Ynda#Z?&u zpnLH#OJ1zzq%t zMmG9LSkWbb8k7TE7Y};uNp(!~b~P7S%|EJ2f&n4_syb}~0N^;{HUt2E9EUl7n?uwl zH4rL?5kXRbAE521D;*#z>TPMIl2mKiCPJU5&`$v?k4X@H=*++4Z%#Gt6+6o!0`STW zgUVVh+;=z(Okom2ZPb29f&Gj(Y{2&^zoFw|i68WZKg|lpor%LUB2H3! zcvR@5zxXHRT|dXnlB#YF&fl5i{+)q-?sp;=mk0BlbA8_5>{wt*?Ii9m`H~VB`-ipD z$`zlhdRtCWuM{9_plkl71K@EKWO)kIjlwyYY+m{^z@KVhol^(V`t0y~vuzu5%_ z$qbehz0SjGNNseOWUU$cNSDKOK;}hR9y}c&e7z2w$iuYxL3|N&g zyNFPS*yKPFVHWKC!(jSyR}8K@sqT>T932a34Sn9fhoq|kNr+~;?Hg?snsC6q zQMIiwRl0Ttb>NU3qE4EvoZ^R43O7inUQmBvIa{acl-hSmN(x8-u0RD)U5*EX4H>o{9R=xFkj^;%On##+`y2IDE=+$bg1 zHD_IaZwXi6DX6-metliyZ|EkCE$XQsfyBVd?^sIWaFzaNsNF?aK&-zl*p&gMdKAir z3QI;Sb>W(j6L|xWO$C)O_P$I;Q+DJ9Meg5?dT;%*lP`10^4>^+TR2SYmGPA`7_;J> zdK_yvQZKP!DwZu4gHj2YzXYrs(jrb>>n$_*os1`*pF2$6&$Qu+LvXL8AAx4Q{bze* ca_c`^bO~*7zrlb%k^gM#T?GQ0FTv3N2hqrg@c;k- literal 0 HcmV?d00001 From 398d172152e746bb591ce66ae423dc99d6238d76 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:11:58 +0200 Subject: [PATCH 5/5] Bump patch --- package-lock.json | 20 ++++++++++---------- package.json | 2 +- packages/aac-encoder/package.json | 2 +- packages/ac3/package.json | 2 +- packages/dts/package.json | 2 +- packages/flac-encoder/package.json | 2 +- packages/mp3-encoder/package.json | 2 +- packages/prores/package.json | 2 +- packages/server/package.json | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7cfc67e..c79006e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.55.3", + "version": "1.55.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.55.3", + "version": "1.55.4", "license": "MPL-2.0", "workspaces": [ ".", @@ -12958,7 +12958,7 @@ }, "packages/aac-encoder": { "name": "@mediabunny/aac-encoder", - "version": "1.55.3", + "version": "1.55.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12973,7 +12973,7 @@ }, "packages/ac3": { "name": "@mediabunny/ac3", - "version": "1.55.3", + "version": "1.55.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12988,7 +12988,7 @@ }, "packages/dts": { "name": "@mediabunny/dts", - "version": "1.55.3", + "version": "1.55.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -13003,7 +13003,7 @@ }, "packages/flac-encoder": { "name": "@mediabunny/flac-encoder", - "version": "1.55.3", + "version": "1.55.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -13018,7 +13018,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.55.3", + "version": "1.55.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -13033,7 +13033,7 @@ }, "packages/prores": { "name": "@mediabunny/prores", - "version": "1.55.3", + "version": "1.55.4", "license": "MPL-2.0", "dependencies": { "turbores": "^1.2.2" @@ -13048,10 +13048,10 @@ }, "packages/server": { "name": "@mediabunny/server", - "version": "1.55.3", + "version": "1.55.4", "license": "MPL-2.0", "dependencies": { - "@mediabunny/prores": "^1.55.3", + "@mediabunny/prores": "^1.55.4", "node-av": "^6.0.0" }, "funding": { diff --git a/package.json b/package.json index 526d6a3..7caa9cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.55.3", + "version": "1.55.4", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ diff --git a/packages/aac-encoder/package.json b/packages/aac-encoder/package.json index 9b6b8de..19f9660 100644 --- a/packages/aac-encoder/package.json +++ b/packages/aac-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/aac-encoder", "author": "Vanilagy", - "version": "1.55.3", + "version": "1.55.4", "description": "AAC encoder extension for Mediabunny, based on libavcodec.", "main": "./dist/bundles/mediabunny-aac-encoder.mjs", "module": "./dist/bundles/mediabunny-aac-encoder.mjs", diff --git a/packages/ac3/package.json b/packages/ac3/package.json index d4ef024..7714330 100644 --- a/packages/ac3/package.json +++ b/packages/ac3/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/ac3", "author": "Vanilagy", - "version": "1.55.3", + "version": "1.55.4", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on libavcodec.", "main": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs", diff --git a/packages/dts/package.json b/packages/dts/package.json index ed17be8..8b8035b 100644 --- a/packages/dts/package.json +++ b/packages/dts/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/dts", "author": "Vanilagy", - "version": "1.55.3", + "version": "1.55.4", "description": "DTS decoder and encoder extension for Mediabunny, based on libavcodec.", "main": "./dist/bundles/mediabunny-dts.mjs", "module": "./dist/bundles/mediabunny-dts.mjs", diff --git a/packages/flac-encoder/package.json b/packages/flac-encoder/package.json index 7b037e2..0443825 100644 --- a/packages/flac-encoder/package.json +++ b/packages/flac-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/flac-encoder", "author": "Vanilagy", - "version": "1.55.3", + "version": "1.55.4", "description": "FLAC encoder extension for Mediabunny, based on libFLAC.", "main": "./dist/bundles/mediabunny-flac-encoder.mjs", "module": "./dist/bundles/mediabunny-flac-encoder.mjs", diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index 27fe74b..570cb40 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.55.3", + "version": "1.55.4", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", diff --git a/packages/prores/package.json b/packages/prores/package.json index ba84e35..413328e 100644 --- a/packages/prores/package.json +++ b/packages/prores/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/prores", "author": "Vanilagy", - "version": "1.55.3", + "version": "1.55.4", "description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.", "main": "./dist/bundles/mediabunny-prores.mjs", "module": "./dist/bundles/mediabunny-prores.mjs", diff --git a/packages/server/package.json b/packages/server/package.json index 772a3df..757d61a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/server", "author": "Vanilagy", - "version": "1.55.3", + "version": "1.55.4", "description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.", "main": "./dist/bundles/mediabunny-server.cjs", "module": "./dist/bundles/mediabunny-server.mjs", @@ -35,7 +35,7 @@ }, "dependencies": { "node-av": "^6.0.0", - "@mediabunny/prores": "^1.55.3" + "@mediabunny/prores": "^1.55.4" }, "peerDependencies": { "mediabunny": "^1.45.0"