diff --git a/dev/demux.html b/dev/demux.html index cd9415d..4df9367 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -21,7 +21,7 @@ const output = new Metamuxer.Output({ target, format }); const mediaSource = new Metamuxer.VideoFrameSource({ - codec: 'av1', + codec: 'vp9', bitrate: 1e6 }); output.addVideoTrack(mediaSource); diff --git a/src/codec.ts b/src/codec.ts index c3ad718..bc400ad 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -120,6 +120,9 @@ const AV1_LEVEL_TABLE = [ { maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 19 }, // Level 6.3 (High Tier) ]; +const VP9_DEFAULT_SUFFIX = '.01.01.01.01.00'; +const AV1_DEFAULT_SUFFIX = '.0.110.01.01.01.0'; + export const buildVideoCodecString = (codec: VideoCodec, width: number, height: number, bitrate: number) => { if (codec === 'avc') { const profileIndication = 0x64; // High Profile @@ -166,7 +169,7 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height: const bitDepth = '08'; // 8-bit - return `vp09.${profile}.${levelInfo.level.toString().padStart(2, '0')}.${bitDepth}`; + return `vp09.${profile}.${levelInfo.level.toString().padStart(2, '0')}.${bitDepth}${VP9_DEFAULT_SUFFIX}`; } else if (codec === 'av1') { const profile = 0; // Main Profile, single digit @@ -174,10 +177,11 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height: const levelInfo = AV1_LEVEL_TABLE.find( level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate, ) ?? last(AV1_LEVEL_TABLE)!; + const level = levelInfo.level.toString().padStart(2, '0'); const bitDepth = '08'; // 8-bit - return `av01.${profile}.${levelInfo.level.toString().padStart(2, '0')}${levelInfo.tier}.${bitDepth}`; + return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}${AV1_DEFAULT_SUFFIX}`; } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -188,22 +192,32 @@ export type Vp9CodecInfo = { profile: number; level: number; bitDepth: number; + chromaSubsampling: number; + videoFullRangeFlag: number; + colourPrimaries: number; + transferCharacteristics: number; + matrixCoefficients: number; }; export type Av1CodecInfo = { - seqProfile: number; - seqLevelIdx0: number; - seqTier0: number; + profile: number; + level: number; + tier: number; bitDepth: number; + monochrome: number; + chromaSubsamplingX: number; + chromaSubsamplingY: number; + chromaSamplePosition: number; }; export const extractVideoCodecString = (trackInfo: { codec: VideoCodec | null; codecDescription: Uint8Array | null; + colorSpace: VideoColorSpaceInit | null; vp9CodecInfo: Vp9CodecInfo | null; av1CodecInfo: Av1CodecInfo | null; }) => { - const { codec, codecDescription: description, vp9CodecInfo, av1CodecInfo } = trackInfo; + const { codec, codecDescription: description, colorSpace, vp9CodecInfo, av1CodecInfo } = trackInfo; if (codec === 'avc') { if (!description || description.byteLength < 4) { @@ -266,19 +280,60 @@ export const extractVideoCodecString = (trackInfo: { const profile = vp9CodecInfo.profile.toString().padStart(2, '0'); const level = vp9CodecInfo.level.toString().padStart(2, '0'); const bitDepth = vp9CodecInfo.bitDepth.toString().padStart(2, '0'); + const chromaSubsampling = vp9CodecInfo.chromaSubsampling.toString().padStart(2, '0'); + const colourPrimaries = vp9CodecInfo.colourPrimaries.toString().padStart(2, '0'); + const transferCharacteristics = vp9CodecInfo.transferCharacteristics.toString().padStart(2, '0'); + const matrixCoefficients = vp9CodecInfo.matrixCoefficients.toString().padStart(2, '0'); + const videoFullRangeFlag = vp9CodecInfo.videoFullRangeFlag.toString().padStart(2, '0'); - return `vp09.${profile}.${level}.${bitDepth}`; + let string = `vp09.${profile}.${level}.${bitDepth}.${chromaSubsampling}`; + string += `.${colourPrimaries}.${transferCharacteristics}.${matrixCoefficients}.${videoFullRangeFlag}`; + + const defaultSuffix = '.01.01.01.01.00'; + if (string.endsWith(defaultSuffix)) { + string = string.slice(0, -defaultSuffix.length); + } + + return string; } else if (codec === 'av1') { if (!av1CodecInfo) { throw new Error('Missing AV1 codec info - unable to construct codec string.'); } - const profile = av1CodecInfo.seqProfile; // Single digit - const level = av1CodecInfo.seqLevelIdx0.toString().padStart(2, '0'); - const tier = av1CodecInfo.seqTier0 ? 'H' : 'M'; + // https://aomediacodec.github.io/av1-isobmff/#codecsparam + const profile = av1CodecInfo.profile; // Single digit + const level = av1CodecInfo.level.toString().padStart(2, '0'); + const tier = av1CodecInfo.tier ? 'H' : 'M'; const bitDepth = av1CodecInfo.bitDepth.toString().padStart(2, '0'); + const monochrome = av1CodecInfo.monochrome ? '1' : '0'; + const chromaSubsampling = 100 * av1CodecInfo.chromaSubsamplingX + + 10 * av1CodecInfo.chromaSubsamplingY + + 1 * ( + av1CodecInfo.chromaSubsamplingX && av1CodecInfo.chromaSubsamplingY + ? av1CodecInfo.chromaSamplePosition + : 0 + ); - return `av01.${profile}.${level}${tier}.${bitDepth}`; + // The defaults are 1 (ITU-R BT.709) + const colorPrimaries = colorSpace?.primaries ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 1; + const transferCharacteristics = colorSpace?.transfer ? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer] : 1; + const matrixCoefficients = colorSpace?.matrix ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 1; + + const videoFullRangeFlag = colorSpace?.fullRange ? 1 : 0; + + let string = `av01.${profile}.${level}${tier}.${bitDepth}`; + string += `.${monochrome}.${chromaSubsampling.toString().padStart(3, '0')}`; + string += `.${colorPrimaries.toString().padStart(2, '0')}`; + string += `.${transferCharacteristics.toString().padStart(2, '0')}`; + string += `.${matrixCoefficients.toString().padStart(2, '0')}`; + string += `.${videoFullRangeFlag}`; + + const defaultSuffix = '.0.110.01.01.01.0'; + if (string.endsWith(defaultSuffix)) { + string = string.slice(0, -defaultSuffix.length); + } + + return string; } throw new TypeError(`Unhandled codec '${codec}'.`); diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts index 9deb4f0..e7ccf10 100644 --- a/src/isobmff/isobmff-boxes.ts +++ b/src/isobmff/isobmff-boxes.ts @@ -601,18 +601,25 @@ export const vpcC = (trackData: IsobmffVideoTrackData) => { const level = Number(parts[2]); const bitDepth = Number(parts[3]); - const chromaSubsampling = 0; - const thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + Number(decoderConfig.colorSpace.fullRange); + const chromaSubsampling = parts[4] ? Number(parts[4]) : 1; // 4:2:0 colocated with luma (0,0) + const videoFullRangeFlag = parts[8] ? Number(parts[8]) : Number(decoderConfig.colorSpace.fullRange); + const thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + videoFullRangeFlag; - const colourPrimaries = decoderConfig.colorSpace.primaries - ? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries] - : 2; // Default to undetermined - const transferCharacteristics = decoderConfig.colorSpace.transfer - ? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer] - : 2; - const matrixCoefficients = decoderConfig.colorSpace.matrix - ? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix] - : 2; + const colourPrimaries = parts[5] + ? Number(parts[5]) + : decoderConfig.colorSpace.primaries + ? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries] + : 2; // Default to undetermined + const transferCharacteristics = parts[6] + ? Number(parts[6]) + : decoderConfig.colorSpace.transfer + ? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer] + : 2; + const matrixCoefficients = parts[7] + ? Number(parts[7]) + : decoderConfig.colorSpace.matrix + ? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix] + : 2; return fullBox('vpcC', 1, 0, [ u8(profile), // Profile diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 6c009a7..4d775b1 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -728,11 +728,22 @@ export class IsobmffDemuxer extends Demuxer { const profile = this.isobmffReader.readU8(); const level = this.isobmffReader.readU8(); const thirdByte = this.isobmffReader.readU8(); + const bitDepth = thirdByte >> 4; + const chromaSubsampling = (thirdByte >> 1) & 0b111; + const videoFullRangeFlag = thirdByte & 1; + const colourPrimaries = this.isobmffReader.readU8(); + const transferCharacteristics = this.isobmffReader.readU8(); + const matrixCoefficients = this.isobmffReader.readU8(); track.info.vp9CodecInfo = { - profile: profile, - level: level, - bitDepth: thirdByte >> 4, + profile, + level, + bitDepth, + chromaSubsampling, + videoFullRangeFlag, + colourPrimaries, + transferCharacteristics, + matrixCoefficients, }; }; break; @@ -743,22 +754,30 @@ export class IsobmffDemuxer extends Demuxer { this.isobmffReader.pos += 1; // Marker + version const secondByte = this.isobmffReader.readU8(); - const seqProfile = secondByte >> 5; - const seqLevelIdx0 = secondByte & 0b11111; + const profile = secondByte >> 5; + const level = secondByte & 0b11111; const thirdByte = this.isobmffReader.readU8(); - const seqTier0 = thirdByte >> 7; + const tier = thirdByte >> 7; const highBitDepth = (thirdByte >> 6) & 1; const twelveBit = (thirdByte >> 5) & 1; + const monochrome = (thirdByte >> 4) & 1; + const chromaSubsamplingX = (thirdByte >> 3) & 1; + const chromaSubsamplingY = (thirdByte >> 2) & 1; + const chromaSamplePosition = thirdByte & 0b11; // Logic from https://aomediacodec.github.io/av1-spec/av1-spec.pdf - const bitDepth = seqProfile == 2 && highBitDepth ? (twelveBit ? 12 : 10) : (highBitDepth ? 10 : 8); + const bitDepth = profile == 2 && highBitDepth ? (twelveBit ? 12 : 10) : (highBitDepth ? 10 : 8); track.info.av1CodecInfo = { - seqProfile, - seqLevelIdx0, - seqTier0, + profile, + level, + tier, bitDepth, + monochrome, + chromaSubsamplingX, + chromaSubsamplingY, + chromaSamplePosition, }; }; break;