diff --git a/src/input-track.ts b/src/input-track.ts index 9857a1e..fcaeb9e 100644 --- a/src/input-track.ts +++ b/src/input-track.ts @@ -691,7 +691,7 @@ export class InputVideoTrack extends InputTrack { const colorSpace = await this._backing.getColorSpace(); return (colorSpace.primaries as string) === 'bt2020' || (colorSpace.primaries as string) === 'smpte432' - || (colorSpace.transfer as string) === 'pg' || (colorSpace.transfer as string) === 'hlg' + || (colorSpace.transfer as string) === 'pq' || (colorSpace.transfer as string) === 'hlg' || (colorSpace.matrix as string) === 'bt2020-ncl'; } diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts index 21f69d5..d3aac41 100644 --- a/src/isobmff/isobmff-boxes.ts +++ b/src/isobmff/isobmff-boxes.ts @@ -741,11 +741,13 @@ export const pasp = (trackData: IsobmffVideoTrackData) => { /** Colour Information Box: Specifies the color space of the video. */ export const colr = (trackData: IsobmffVideoTrackData) => box('colr', [ - ascii('nclx'), // Colour type + 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 - u8((trackData.info.decoderConfig.colorSpace!.fullRange ? 1 : 0) << 7), // Full range flag + trackData.muxer.isQuickTime + ? [] // Doesn't have it + : u8((trackData.info.decoderConfig.colorSpace!.fullRange ? 1 : 0) << 7), // Full range flag ]); /** AVC Configuration Box: Provides additional information to the decoder. */ diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 7e468f6..450c6ce 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -1448,20 +1448,24 @@ export class IsobmffDemuxer extends Demuxer { assert(track.info?.type === 'video'); const colourType = readAscii(slice, 4); - if (colourType !== 'nclx') { + if (colourType !== 'nclx' && colourType !== 'nclc') { break; } const colourPrimaries = readU16Be(slice); const transferCharacteristics = readU16Be(slice); const matrixCoefficients = readU16Be(slice); - const fullRangeFlag = Boolean(readU8(slice) & 0x80); + + let fullRange: boolean | undefined = undefined; + if (colourType === 'nclx') { + fullRange = Boolean(readU8(slice) & 0x80); + } track.info.colorSpace = { primaries: COLOR_PRIMARIES_MAP_INVERSE[colourPrimaries], transfer: TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics], matrix: MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients], - fullRange: fullRangeFlag, + fullRange, } as VideoColorSpaceInit; }; break; diff --git a/test/node/read-mp4.test.ts b/test/node/read-mp4.test.ts index d986ed3..9ec5970 100644 --- a/test/node/read-mp4.test.ts +++ b/test/node/read-mp4.test.ts @@ -1,6 +1,19 @@ import { expect, test } from 'vitest'; import path from 'node:path'; -import { ALL_FORMATS, MP4, EncodedPacketSink, Input, FilePathSource } from '../../src/index.js'; +import { + ALL_FORMATS, + BufferSource, + BufferTarget, + EncodedPacket, + EncodedPacketSink, + EncodedVideoPacketSource, + FilePathSource, + Input, + MP4, + MovOutputFormat, + Mp4OutputFormat, + Output, +} from '../../src/index.js'; const __dirname = new URL('.', import.meta.url).pathname; @@ -33,3 +46,97 @@ test('Should be able to get packets from a .MP4 file', async () => { 0, 0.16, 0.08, 0.04, 0.12, 0.32, 0.24, 0.2, 0.28, 0.48, ]); }); + +test('MP4 nclx color information', async () => { + const output = new Output({ + format: new Mp4OutputFormat(), + target: new BufferTarget(), + }); + const source = new EncodedVideoPacketSource('vp8'); + output.addVideoTrack(source); + + await output.start(); + await source.add( + new EncodedPacket(new Uint8Array(1024), 'key', 0, 0.1), + { + decoderConfig: { + codec: 'vp8', + codedWidth: 1280, + codedHeight: 720, + colorSpace: { + primaries: 'bt2020' as VideoColorPrimaries, + transfer: 'pq' as VideoTransferCharacteristics, + matrix: 'bt2020-ncl' as VideoMatrixCoefficients, + fullRange: false, + }, + }, + }, + ); + await output.finalize(); + + const str = String.fromCharCode(...new Uint8Array(output.target.buffer!)); + expect(str.includes('nclc')).toBe(false); + expect(str.includes('nclx')).toBe(true); + + using input = new Input({ + source: new BufferSource(output.target.buffer!), + formats: ALL_FORMATS, + }); + const track = await input.getPrimaryVideoTrack(); + if (!track) throw new Error('No video track found'); + + expect(await track.getColorSpace()).toEqual({ + primaries: 'bt2020', + transfer: 'pq', + matrix: 'bt2020-ncl', + fullRange: false, + }); + expect(await track.hasHighDynamicRange()).toBe(true); +}); + +test('QuickTime nclc color information', async () => { + const output = new Output({ + format: new MovOutputFormat(), + target: new BufferTarget(), + }); + const source = new EncodedVideoPacketSource('vp8'); + output.addVideoTrack(source); + + await output.start(); + await source.add( + new EncodedPacket(new Uint8Array(1024), 'key', 0, 0.1), + { + decoderConfig: { + codec: 'vp8', + codedWidth: 1280, + codedHeight: 720, + colorSpace: { + primaries: 'bt2020' as VideoColorPrimaries, + transfer: 'pq' as VideoTransferCharacteristics, + matrix: 'bt2020-ncl' as VideoMatrixCoefficients, + fullRange: false, + }, + }, + }, + ); + await output.finalize(); + + const str = String.fromCharCode(...new Uint8Array(output.target.buffer!)); + expect(str.includes('nclc')).toBe(true); + expect(str.includes('nclx')).toBe(false); + + using input = new Input({ + source: new BufferSource(output.target.buffer!), + formats: ALL_FORMATS, + }); + const track = await input.getPrimaryVideoTrack(); + if (!track) throw new Error('No video track found'); + + expect(await track.getColorSpace()).toEqual({ + primaries: 'bt2020', + transfer: 'pq', + matrix: 'bt2020-ncl', + fullRange: undefined, + }); + expect(await track.hasHighDynamicRange()).toBe(true); +});