diff --git a/src/codec-data.ts b/src/codec-data.ts index 26fa41e..4882752 100644 --- a/src/codec-data.ts +++ b/src/codec-data.ts @@ -212,17 +212,6 @@ export const concatNalUnitsInLengthPrefixed = (nalUnits: Uint8Array[], lengthSiz return result; }; -/** Converts an AVC packet in Annex B format to length-prefixed format. */ -export const transformAnnexBToLengthPrefixed = (packetData: Uint8Array) => { - const nalUnits = findNalUnitsInAnnexB(packetData); - if (nalUnits.length === 0) { - // It's not valid Annex B data - return null; - } - - return concatNalUnitsInLengthPrefixed(nalUnits, 4); -}; - // Data specified in ISO 14496-15 export type AvcDecoderConfigurationRecord = { configurationVersion: number; diff --git a/src/isobmff/isobmff-muxer.ts b/src/isobmff/isobmff-muxer.ts index e866f9d..fd73b50 100644 --- a/src/isobmff/isobmff-muxer.ts +++ b/src/isobmff/isobmff-muxer.ts @@ -25,11 +25,12 @@ import { import { BufferTarget } from '../target'; import { EncodedPacket, PacketType } from '../packet'; import { + concatNalUnitsInLengthPrefixed, extractAvcDecoderConfigurationRecord, extractHevcDecoderConfigurationRecord, + findNalUnitsInAnnexB, serializeAvcDecoderConfigurationRecord, serializeHevcDecoderConfigurationRecord, - transformAnnexBToLengthPrefixed, } from '../codec-data'; import { buildIsobmffMimeType } from './isobmff-misc'; import { MAX_BOX_HEADER_SIZE, MIN_BOX_HEADER_SIZE } from './isobmff-reader'; @@ -464,15 +465,18 @@ export class IsobmffMuxer extends Muxer { let packetData = packet.data; if (trackData.info.requiresAnnexBTransformation) { - const transformedData = transformAnnexBToLengthPrefixed(packetData); - if (!transformedData) { + const nalUnits = findNalUnitsInAnnexB(packetData); + if (nalUnits.length === 0) { + // It's not valid Annex B data throw new Error( 'Failed to transform packet data. Make sure all packets are provided in Annex B format, as' + ' specified in ITU-T-REC-H.264 and ITU-T-REC-H.265.', ); } - packetData = transformedData; + // We don't strip things like SPS or PPS NALUs here, mainly because they can also appear in the middle + // of a stream and potentially modify the parameters of it. So, let's just leave them in to be sure. + packetData = concatNalUnitsInLengthPrefixed(nalUnits, 4); } const timestamp = this.validateAndNormalizeTimestamp( diff --git a/test/node/annex-b-conversion.test.ts b/test/node/annex-b-conversion.test.ts new file mode 100644 index 0000000..3e86555 --- /dev/null +++ b/test/node/annex-b-conversion.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from 'vitest'; +import { Input } from '../../src/input.js'; +import { BufferSource, FilePathSource } from '../../src/source.js'; +import path from 'node:path'; +import { ALL_FORMATS } from '../../src/input-format.js'; +import { Output } from '../../src/output.js'; +import { Mp4OutputFormat } from '../../src/output-format.js'; +import { BufferTarget } from '../../src/target.js'; +import { Conversion } from '../../src/conversion.js'; +import { EncodedPacketSink } from '../../src/media-sink.js'; +import { extractAvcNalUnits } from '../../src/codec-data.js'; + +const __dirname = new URL('.', import.meta.url).pathname; + +test('Annex B to length-prefixed conversion, MP4', async () => { + using originalInput = new Input({ + source: new FilePathSource(path.join(__dirname, '..', 'public/annex-b-avc.mkv')), + formats: ALL_FORMATS, + }); + const originalVideoTrack = (await originalInput.getPrimaryVideoTrack())!; + const originalDecoderConfig = (await originalVideoTrack.getDecoderConfig())!; + expect(originalDecoderConfig.description).toBeUndefined(); + expect(originalVideoTrack.codec).toBe('avc'); + + const originalSink = new EncodedPacketSink(originalVideoTrack); + const originalFirstPacket = await originalSink.getFirstPacket(); + expect([...originalFirstPacket!.data.slice(0, 4)]).toEqual([0, 0, 0, 1]); + + const originalNalUnits = extractAvcNalUnits(originalFirstPacket!.data, originalDecoderConfig); + + const output = new Output({ + format: new Mp4OutputFormat(), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ input: originalInput, output }); + await conversion.execute(); + + using newInput = new Input({ + source: new BufferSource(output.target.buffer!), + formats: ALL_FORMATS, + }); + const newVideoTrack = (await newInput.getPrimaryVideoTrack())!; + const newDecoderConfig = (await newVideoTrack.getDecoderConfig())!; + expect(newDecoderConfig.description).toBeDefined(); + expect(newVideoTrack.codec).toBe('avc'); + + const newSink = new EncodedPacketSink(newVideoTrack); + const newFirstPacket = await newSink.getFirstPacket(); + expect([...newFirstPacket!.data.slice(0, 4)]).not.toEqual([0, 0, 0, 1]); // Successfully converted + + const newNalUnits = extractAvcNalUnits(newFirstPacket!.data, newDecoderConfig); + expect(newNalUnits).toEqual(originalNalUnits); // Content is the same though +}); diff --git a/test/public/annex-b-avc.mkv b/test/public/annex-b-avc.mkv new file mode 100644 index 0000000..699dfd2 Binary files /dev/null and b/test/public/annex-b-avc.mkv differ