diff --git a/.gitignore b/.gitignore index 8546073..1043866 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ node_modules /dist /dist-docs .DS_Store -/docs/.vitepress/cache +docs/.vitepress/cache /docs/api *.tsbuildinfo diff --git a/dev/demux.html b/dev/demux.html index 3afb290..5354400 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -18,7 +18,7 @@ }); const track = await input.getPrimaryVideoTrack(); - console.log(await track.computeFrameRateMetrics()); + console.log(await track.getDecoderConfig()); /* diff --git a/package-lock.json b/package-lock.json index 5d5b233..e0c691e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.55.1", + "version": "1.55.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.55.1", + "version": "1.55.2", "license": "MPL-2.0", "workspaces": [ ".", @@ -12958,7 +12958,7 @@ }, "packages/aac-encoder": { "name": "@mediabunny/aac-encoder", - "version": "1.55.1", + "version": "1.55.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12973,7 +12973,7 @@ }, "packages/ac3": { "name": "@mediabunny/ac3", - "version": "1.55.1", + "version": "1.55.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12988,7 +12988,7 @@ }, "packages/dts": { "name": "@mediabunny/dts", - "version": "1.55.1", + "version": "1.55.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -13003,7 +13003,7 @@ }, "packages/flac-encoder": { "name": "@mediabunny/flac-encoder", - "version": "1.55.1", + "version": "1.55.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -13018,7 +13018,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.55.1", + "version": "1.55.2", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -13033,7 +13033,7 @@ }, "packages/prores": { "name": "@mediabunny/prores", - "version": "1.55.1", + "version": "1.55.2", "license": "MPL-2.0", "dependencies": { "turbores": "^1.2.2" @@ -13048,10 +13048,10 @@ }, "packages/server": { "name": "@mediabunny/server", - "version": "1.55.1", + "version": "1.55.2", "license": "MPL-2.0", "dependencies": { - "@mediabunny/prores": "^1.55.1", + "@mediabunny/prores": "^1.55.2", "node-av": "^6.0.0" }, "funding": { diff --git a/package.json b/package.json index 9233b30..bcec159 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.55.1", + "version": "1.55.2", "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 72502fd..cc781a5 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.1", + "version": "1.55.2", "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 7b4cdbb..a4980c8 100644 --- a/packages/ac3/package.json +++ b/packages/ac3/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/ac3", "author": "Vanilagy", - "version": "1.55.1", + "version": "1.55.2", "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 aabbcf0..6e38903 100644 --- a/packages/dts/package.json +++ b/packages/dts/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/dts", "author": "Vanilagy", - "version": "1.55.1", + "version": "1.55.2", "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 c13a455..cec00fa 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.1", + "version": "1.55.2", "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 6a3e4c9..5b29a83 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.1", + "version": "1.55.2", "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 9b71867..40e789b 100644 --- a/packages/prores/package.json +++ b/packages/prores/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/prores", "author": "Vanilagy", - "version": "1.55.1", + "version": "1.55.2", "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 b7b7444..2cb58f1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/server", "author": "Vanilagy", - "version": "1.55.1", + "version": "1.55.2", "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.1" + "@mediabunny/prores": "^1.55.2" }, "peerDependencies": { "mediabunny": "^1.45.0" diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 8419780..814dc8e 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -39,6 +39,8 @@ import { parseDtsSpecificBox, DTS_SPECIFIC_BOX_SIZE, AC3_ACMOD_CHANNEL_COUNTS, + extractAvcDecoderConfigurationRecord, + extractHevcDecoderConfigurationRecord, } from '../codec-data'; import { Demuxer } from '../demuxer'; import { Input } from '../input'; @@ -1390,6 +1392,11 @@ export class IsobmffDemuxer extends Demuxer { } assert(track.info); + if (boxInfo.contentSize === 0) { + // avcC box is empty, let's treat this like an Annex B stream + break; + } + track.info.codecDescription = readBytes(slice, boxInfo.contentSize); }; break; @@ -1400,6 +1407,11 @@ export class IsobmffDemuxer extends Demuxer { } assert(track.info); + if (boxInfo.contentSize === 0) { + // hvcC box is empty, let's treat this like an Annex B stream + break; + } + track.info.codecDescription = readBytes(slice, boxInfo.contentSize); }; break; @@ -3407,7 +3419,15 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo } return this.decoderConfigPromise ??= (async (): Promise => { - if (this.internalTrack.info.codec === 'vp9' && !this.internalTrack.info.vp9CodecInfo) { + if (this.internalTrack.info.codec === 'avc' && !this.internalTrack.info.codecDescription) { + const firstPacket = await this.getFirstPacket({}); + this.internalTrack.info.avcCodecInfo + = firstPacket && extractAvcDecoderConfigurationRecord(firstPacket.data); + } else if (this.internalTrack.info.codec === 'hevc' && !this.internalTrack.info.codecDescription) { + const firstPacket = await this.getFirstPacket({}); + this.internalTrack.info.hevcCodecInfo + = firstPacket && extractHevcDecoderConfigurationRecord(firstPacket.data); + } else if (this.internalTrack.info.codec === 'vp9' && !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) { diff --git a/src/media-sink.ts b/src/media-sink.ts index 01d82cf..6fd28b8 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -38,6 +38,7 @@ import { isWebKit, last, mapAsyncGenerator, + missingWebCodecsClassMessage, promiseWithResolvers, removeItem, Rotation, @@ -1691,8 +1692,12 @@ export class VideoSampleSink extends BaseMediaSampleSink { onError: (error: unknown) => unknown, ) { if (!(await this._track.canDecode())) { + if (typeof VideoDecoder === 'undefined') { + throw new Error(missingWebCodecsClassMessage('VideoDecoder')); + } + throw new Error( - 'This video track cannot be decoded by this browser. Make sure to check decodability before using' + 'This video track cannot be decoded in this environment. Make sure to check decodability before using' + ' a track.', ); } @@ -2426,8 +2431,12 @@ export class AudioSampleSink extends BaseMediaSampleSink { onError: (error: unknown) => unknown, ) { if (!(await this._track.canDecode())) { + if (typeof AudioDecoder === 'undefined') { + throw new Error(missingWebCodecsClassMessage('AudioDecoder')); + } + throw new Error( - 'This audio track cannot be decoded by this browser. Make sure to check decodability before using' + 'This audio track cannot be decoded in this environment. Make sure to check decodability before using' + ' a track.', ); } diff --git a/src/media-source.ts b/src/media-source.ts index 74bf8a7..c724b96 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -29,6 +29,7 @@ import { clearIntervalUnthrottled, floorToDivisor, last, + missingWebCodecsClassMessage, promiseWithResolvers, roundToDivisor, setInt24, @@ -718,7 +719,7 @@ class VideoEncoderWrapper { if (!selected) { if (typeof VideoEncoder === 'undefined') { - throw new Error('VideoEncoder is not supported by this browser.'); + throw new Error(missingWebCodecsClassMessage('VideoEncoder')); } // The candidates only differ in their rate control, so we describe them as one config with a @@ -731,7 +732,7 @@ class VideoEncoderWrapper { throw new Error( `This specific encoder configuration (${firstConfig.codec}, ${rateControls.join(' / ')},` + ` ${firstConfig.width}x${firstConfig.height}, hardware acceleration:` - + ` ${firstConfig.hardwareAcceleration ?? 'no-preference'}) is not supported by this browser.` + + ` ${firstConfig.hardwareAcceleration ?? 'no-preference'}) is not supported in this environment.` + ` Consider using another codec or changing your video parameters.`, ); } @@ -1694,8 +1695,8 @@ export class MediaStreamVideoTrackSource extends VideoSource { }); } else { throw new Error( - 'When no explicit frame rate is set, MediaStreamTrackProcessor is required; but it\'s not supported' - + ' by this browser.', + 'When no explicit frame rate is set, MediaStreamTrackProcessor is required; but it\'s not available' + + ' in this environment.', ); } } @@ -2210,7 +2211,7 @@ class AudioEncoderWrapper { this.initPcmEncoder(); } else { if (typeof AudioEncoder === 'undefined') { - throw new Error('AudioEncoder is not supported by this browser.'); + throw new Error(missingWebCodecsClassMessage('AudioEncoder')); } let supported: boolean; @@ -2226,7 +2227,8 @@ class AudioEncoderWrapper { throw new Error( `This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps,` + ` ${encoderConfig.numberOfChannels} channels, ${encoderConfig.sampleRate} Hz) is not` - + ` supported by this browser. Consider using another codec or changing your audio parameters.`, + + ` supported in this environment. Consider using another codec or changing your` + + ` audio parameters.`, ); } diff --git a/src/misc.ts b/src/misc.ts index 60fea06..3b0d98a 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -721,6 +721,16 @@ export const getChromiumVersion = () => { return chromiumVersionCache = Number(match[1]!); }; +export const missingWebCodecsClassMessage = (className: string) => { + if (typeof globalThis.isSecureContext !== 'undefined' && !globalThis.isSecureContext) { + // WebCodecs is not exposed in insecure contexts + return `${className} is not available in this environment; this may be because this page is running in an` + + ` insecure context. Try serving your page over HTTPS or use localhost.`; + } + + return `${className} is not available in this environment.`; +}; + /** * T or a promise that resolves to T. * @group Miscellaneous diff --git a/src/packet.ts b/src/packet.ts index 36901f4..f345b5d 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -155,7 +155,7 @@ export class EncodedPacket { throw new TypeError('Metadata-only packets cannot be converted to a video chunk.'); } if (typeof EncodedVideoChunk === 'undefined') { - throw new Error('Your browser does not support EncodedVideoChunk.'); + throw new Error('EncodedVideoChunk is not available in this environment.'); } return new EncodedVideoChunk({ @@ -179,7 +179,7 @@ export class EncodedPacket { throw new TypeError('Metadata-only packets cannot be converted to a video chunk.'); } if (typeof EncodedVideoChunk === 'undefined') { - throw new Error('Your browser does not support EncodedVideoChunk.'); + throw new Error('EncodedVideoChunk is not available in this environment.'); } return new EncodedVideoChunk({ @@ -198,7 +198,7 @@ export class EncodedPacket { throw new TypeError('Metadata-only packets cannot be converted to an audio chunk.'); } if (typeof EncodedAudioChunk === 'undefined') { - throw new Error('Your browser does not support EncodedAudioChunk.'); + throw new Error('EncodedAudioChunk is not available in this environment.'); } return new EncodedAudioChunk({ diff --git a/test/browser/quality.test.ts b/test/browser/quality.test.ts index c60474e..44f6637 100644 --- a/test/browser/quality.test.ts +++ b/test/browser/quality.test.ts @@ -76,6 +76,7 @@ const convertVideo = async (quality: Quality, codec: VideoCodec, allowMissingQua trim: { start: 0, end: 2 }, video: { codec, quality }, audio: { discard: true }, + showWarnings: false, }); if (allowMissingQuantizerSupport && !conversion.isValid) { diff --git a/test/node/read-mp4.test.ts b/test/node/read-mp4.test.ts index 9ec5970..6b09ed5 100644 --- a/test/node/read-mp4.test.ts +++ b/test/node/read-mp4.test.ts @@ -140,3 +140,23 @@ test('QuickTime nclc color information', async () => { }); expect(await track.hasHighDynamicRange()).toBe(true); }); + +// Annex B isn't supposed to exist in MP4, but some files have it anyway, with an empty avcC box +test('Annex B', async () => { + const filePath = path.join(__dirname, '..', 'public/annex-b.mp4'); + using input = new Input({ + source: new FilePathSource(filePath), + formats: ALL_FORMATS, + }); + + const track = await input.getPrimaryVideoTrack(); + if (!track) throw new Error('No video track found'); + + const decoderConfig = (await track.getDecoderConfig())!; + expect(decoderConfig.codec).toBe('avc1.424028'); + expect(decoderConfig.description).toBeUndefined(); + + const sink = new EncodedPacketSink(track); + const firstPacket = (await sink.getFirstPacket())!; + expect([...firstPacket.data.slice(0, 4)]).toEqual([0, 0, 0, 1]); +}); diff --git a/test/public/annex-b.mp4 b/test/public/annex-b.mp4 new file mode 100644 index 0000000..a53c09d Binary files /dev/null and b/test/public/annex-b.mp4 differ