From 18d64ffb3394bc5301be52c52b4706faa538144c Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:18:53 +0100 Subject: [PATCH] Change ProRes codec string, remove ProRes RAW variants for now --- src/codec.ts | 29 +++++++++++++---------------- src/isobmff/isobmff-boxes.ts | 2 +- src/matroska/matroska-demuxer.ts | 11 ++++++++++- src/matroska/matroska-muxer.ts | 5 +---- test/node/prores.test.ts | 16 ++++++++-------- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/codec.ts b/src/codec.ts index 2ff9788..14cfea0 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -215,14 +215,12 @@ const VP9_DEFAULT_SUFFIX = '.01.01.01.01.00'; const AV1_DEFAULT_SUFFIX = '.0.110.01.01.01.0'; export const PRORES_FOURCCS = [ - 'ap4x', - 'ap4h', - 'apch', - 'apcn', - 'apcs', - 'apco', - 'aprh', - 'aprn', + 'ap4x', // ProRes 4444 XQ + 'ap4h', // ProRes 4444 + 'apch', // ProRes 422 High Quality + 'apcn', // ProRes 422 Standard Definition + 'apcs', // ProRes 422 LT + 'apco', // ProRes 422 Proxy ]; export const buildVideoCodecString = (codec: VideoCodec, width: number, height: number, bitrate: number) => { @@ -285,7 +283,7 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height: return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}`; } else if (codec === 'prores') { - return 'apr1.apch'; + return 'apch'; } else { assertNever(codec); } @@ -528,7 +526,7 @@ export const extractVideoCodecString = (trackInfo: { return string; } else if (codec === 'prores') { - return `apr1.${proresFormat ?? 'apch'}`; + return proresFormat ?? 'apch'; } else if (codec !== null) { assertNever(codec); } @@ -816,7 +814,7 @@ export const getAudioEncoderConfigExtension = (codec: AudioCodec) => { return {}; }; -const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01', 'apr1']; +const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01', ...PRORES_FOURCCS]; const AVC_CODEC_STRING_REGEX = /^(avc1|avc3)\.[0-9a-fA-F]{6}$/; const HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\.(?:[ABC]?\d+)\.[0-9a-fA-F]{1,8}\.[LH]\d+(?:\.[0-9a-fA-F]{1,2}){0,6}$/; const VP9_CODEC_STRING_REGEX = /^vp09(?:\.\d{2}){3}(?:(?:\.\d{2}){5})?$/; @@ -952,14 +950,13 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata | + ' specified in Section "Codecs Parameter String" of https://aomediacodec.github.io/av1-isobmff/.', ); } - } else if (metadata.decoderConfig.codec.startsWith('apr1')) { + } else if (PRORES_FOURCCS.some(x => metadata.decoderConfig!.codec.startsWith(x))) { // ProRes-specific validation - const parts = metadata.decoderConfig.codec.split('.'); - if (parts.length !== 2 || parts[0] !== 'apr1' || !PRORES_FOURCCS.includes(parts[1]!)) { + if (!PRORES_FOURCCS.some(x => metadata.decoderConfig!.codec === x)) { throw new TypeError( - 'Video chunk metadata decoder configuration codec string for ProRes must be a valid ProRes codec' - + ' string as specified in the Mediabunny Codec Registry.', + 'Video chunk metadata decoder configuration codec string for ProRes must be one of the valid ProRes' + + ` four-character codes: ${PRORES_FOURCCS.join(', ')}.`, ); } } diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts index 4c2a871..c257657 100644 --- a/src/isobmff/isobmff-boxes.ts +++ b/src/isobmff/isobmff-boxes.ts @@ -1584,7 +1584,7 @@ const videoCodecToBoxName = (codec: VideoCodec, fullCodecString: string) => { case 'vp8': return 'vp08'; case 'vp9': return 'vp09'; case 'av1': return 'av01'; - case 'prores': return fullCodecString.split('.')[1]!; + case 'prores': return fullCodecString; } }; diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 510f990..8cec3de 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -19,6 +19,7 @@ import { extractVideoCodecString, MediaCodec, OPUS_SAMPLE_RATE, + PRORES_FOURCCS, VideoCodec, } from '../codec'; import { Demuxer } from '../demuxer'; @@ -1044,7 +1045,15 @@ export class MatroskaDemuxer extends Demuxer { } else if (codecIdWithoutSuffix === CODEC_STRING_MAP.av1) { this.currentTrack.info.codec = 'av1'; } else if (codecIdWithoutSuffix === CODEC_STRING_MAP.prores) { - this.currentTrack.info.codec = 'prores'; + const format = this.currentTrack.codecPrivate + ? textDecoder.decode(this.currentTrack.codecPrivate) + : ''; + + if (PRORES_FOURCCS.includes(format)) { + this.currentTrack.info.codec = 'prores'; + } else { + // We don't support ProRes RAW yet + } } const videoTrack = this.currentTrack as InternalVideoTrack; diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index d0327ba..467f50f 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -747,11 +747,8 @@ export class MatroskaMuxer extends Muxer { generateAv1CodecConfigurationFromCodecString(newTrackData.info.decoderConfig.codec), ); } else if (track.source._codec === 'prores') { - const format = meta.decoderConfig.codec.split('.')[1]; - assert(format); - // "The Private Data contains the FourCC as found in MP4 movies" - newTrackData.codecPrivate = textEncoder.encode(format); + newTrackData.codecPrivate = textEncoder.encode(meta.decoderConfig.codec); } this.trackDatas.push(newTrackData); diff --git a/test/node/prores.test.ts b/test/node/prores.test.ts index 76d6eda..642208c 100644 --- a/test/node/prores.test.ts +++ b/test/node/prores.test.ts @@ -9,7 +9,7 @@ import { Conversion } from '../../src/conversion.js'; test.concurrent('ProRes MOV file reading', async () => { using input = new Input({ - source: new UrlSource('https://pub-cf9fcfcb5c0a44e9b1bb5ff890e041ae.r2.dev/IMG_0158-prores-raw.MOV'), + source: new UrlSource('https://pub-cf9fcfcb5c0a44e9b1bb5ff890e041ae.r2.dev/IMG_0158-prores-log.MOV'), formats: ALL_FORMATS, }); @@ -19,13 +19,13 @@ test.concurrent('ProRes MOV file reading', async () => { expect(videoTrack.codedHeight).toBe(1080); const decoderConfig = (await videoTrack.getDecoderConfig())!; - expect(decoderConfig.codec).toBe('apr1.apch'); + expect(decoderConfig.codec).toBe('apch'); expect(decoderConfig.description).toBeUndefined(); }); -test.concurrent('ProRes transcoding into MOV', { timeout: 60_000 }, async () => { +test.concurrent('ProRes transmuxing into MOV', { timeout: 60_000 }, async () => { using input = new Input({ - source: new UrlSource('https://pub-cf9fcfcb5c0a44e9b1bb5ff890e041ae.r2.dev/IMG_0158-prores-raw.MOV'), + source: new UrlSource('https://pub-cf9fcfcb5c0a44e9b1bb5ff890e041ae.r2.dev/IMG_0158-prores-log.MOV'), formats: ALL_FORMATS, }); @@ -56,13 +56,13 @@ test.concurrent('ProRes transcoding into MOV', { timeout: 60_000 }, async () => expect((await videoTrack.computePacketStats()).packetCount).toBe(15); const decoderConfig = (await videoTrack.getDecoderConfig())!; - expect(decoderConfig.codec).toBe('apr1.apch'); + expect(decoderConfig.codec).toBe('apch'); expect(decoderConfig.description).toBeUndefined(); }); -test.concurrent('ProRes transcoding into MKV', { timeout: 60_000 }, async () => { +test.concurrent('ProRes transmuxing into MKV', { timeout: 60_000 }, async () => { using input = new Input({ - source: new UrlSource('https://pub-cf9fcfcb5c0a44e9b1bb5ff890e041ae.r2.dev/IMG_0158-prores-raw.MOV'), + source: new UrlSource('https://pub-cf9fcfcb5c0a44e9b1bb5ff890e041ae.r2.dev/IMG_0158-prores-log.MOV'), formats: ALL_FORMATS, }); @@ -96,6 +96,6 @@ test.concurrent('ProRes transcoding into MKV', { timeout: 60_000 }, async () => expect((await videoTrack.computePacketStats()).packetCount).toBe(15); const decoderConfig = (await videoTrack.getDecoderConfig())!; - expect(decoderConfig.codec).toBe('apr1.apch'); + expect(decoderConfig.codec).toBe('apch'); expect(decoderConfig.description).toBeUndefined(); });