Compare commits

...
8 Commits
19 changed files with 95 additions and 33 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ node_modules
/dist /dist
/dist-docs /dist-docs
.DS_Store .DS_Store
/docs/.vitepress/cache docs/.vitepress/cache
/docs/api /docs/api
*.tsbuildinfo *.tsbuildinfo
+1 -1
View File
@@ -18,7 +18,7 @@
}); });
const track = await input.getPrimaryVideoTrack(); const track = await input.getPrimaryVideoTrack();
console.log(await track.computeFrameRateMetrics()); console.log(await track.getDecoderConfig());
/* /*
+10 -10
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.55.0", "version": "1.55.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.55.0", "version": "1.55.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
".", ".",
@@ -12958,7 +12958,7 @@
}, },
"packages/aac-encoder": { "packages/aac-encoder": {
"name": "@mediabunny/aac-encoder", "name": "@mediabunny/aac-encoder",
"version": "1.55.0", "version": "1.55.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12973,7 +12973,7 @@
}, },
"packages/ac3": { "packages/ac3": {
"name": "@mediabunny/ac3", "name": "@mediabunny/ac3",
"version": "1.55.0", "version": "1.55.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12988,7 +12988,7 @@
}, },
"packages/dts": { "packages/dts": {
"name": "@mediabunny/dts", "name": "@mediabunny/dts",
"version": "1.55.0", "version": "1.55.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -13003,7 +13003,7 @@
}, },
"packages/flac-encoder": { "packages/flac-encoder": {
"name": "@mediabunny/flac-encoder", "name": "@mediabunny/flac-encoder",
"version": "1.55.0", "version": "1.55.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -13018,7 +13018,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.55.0", "version": "1.55.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -13033,7 +13033,7 @@
}, },
"packages/prores": { "packages/prores": {
"name": "@mediabunny/prores", "name": "@mediabunny/prores",
"version": "1.55.0", "version": "1.55.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"turbores": "^1.2.2" "turbores": "^1.2.2"
@@ -13048,10 +13048,10 @@
}, },
"packages/server": { "packages/server": {
"name": "@mediabunny/server", "name": "@mediabunny/server",
"version": "1.55.0", "version": "1.55.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@mediabunny/prores": "^1.55.0", "@mediabunny/prores": "^1.55.2",
"node-av": "^6.0.0" "node-av": "^6.0.0"
}, },
"funding": { "funding": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.55.0", "version": "1.55.2",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/aac-encoder", "name": "@mediabunny/aac-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.55.0", "version": "1.55.2",
"description": "AAC encoder extension for Mediabunny, based on libavcodec.", "description": "AAC encoder extension for Mediabunny, based on libavcodec.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs", "main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs", "module": "./dist/bundles/mediabunny-aac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/ac3", "name": "@mediabunny/ac3",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.55.0", "version": "1.55.2",
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on libavcodec.", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on libavcodec.",
"main": "./dist/bundles/mediabunny-ac3.mjs", "main": "./dist/bundles/mediabunny-ac3.mjs",
"module": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/dts", "name": "@mediabunny/dts",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.55.0", "version": "1.55.2",
"description": "DTS decoder and encoder extension for Mediabunny, based on libavcodec.", "description": "DTS decoder and encoder extension for Mediabunny, based on libavcodec.",
"main": "./dist/bundles/mediabunny-dts.mjs", "main": "./dist/bundles/mediabunny-dts.mjs",
"module": "./dist/bundles/mediabunny-dts.mjs", "module": "./dist/bundles/mediabunny-dts.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/flac-encoder", "name": "@mediabunny/flac-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.55.0", "version": "1.55.2",
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.", "description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
"main": "./dist/bundles/mediabunny-flac-encoder.mjs", "main": "./dist/bundles/mediabunny-flac-encoder.mjs",
"module": "./dist/bundles/mediabunny-flac-encoder.mjs", "module": "./dist/bundles/mediabunny-flac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.55.0", "version": "1.55.2",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/prores", "name": "@mediabunny/prores",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.55.0", "version": "1.55.2",
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.", "description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
"main": "./dist/bundles/mediabunny-prores.mjs", "main": "./dist/bundles/mediabunny-prores.mjs",
"module": "./dist/bundles/mediabunny-prores.mjs", "module": "./dist/bundles/mediabunny-prores.mjs",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/server", "name": "@mediabunny/server",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.55.0", "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.", "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", "main": "./dist/bundles/mediabunny-server.cjs",
"module": "./dist/bundles/mediabunny-server.mjs", "module": "./dist/bundles/mediabunny-server.mjs",
@@ -35,7 +35,7 @@
}, },
"dependencies": { "dependencies": {
"node-av": "^6.0.0", "node-av": "^6.0.0",
"@mediabunny/prores": "^1.55.0" "@mediabunny/prores": "^1.55.2"
}, },
"peerDependencies": { "peerDependencies": {
"mediabunny": "^1.45.0" "mediabunny": "^1.45.0"
+21 -1
View File
@@ -39,6 +39,8 @@ import {
parseDtsSpecificBox, parseDtsSpecificBox,
DTS_SPECIFIC_BOX_SIZE, DTS_SPECIFIC_BOX_SIZE,
AC3_ACMOD_CHANNEL_COUNTS, AC3_ACMOD_CHANNEL_COUNTS,
extractAvcDecoderConfigurationRecord,
extractHevcDecoderConfigurationRecord,
} from '../codec-data'; } from '../codec-data';
import { Demuxer } from '../demuxer'; import { Demuxer } from '../demuxer';
import { Input } from '../input'; import { Input } from '../input';
@@ -1390,6 +1392,11 @@ export class IsobmffDemuxer extends Demuxer {
} }
assert(track.info); 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); track.info.codecDescription = readBytes(slice, boxInfo.contentSize);
}; break; }; break;
@@ -1400,6 +1407,11 @@ export class IsobmffDemuxer extends Demuxer {
} }
assert(track.info); 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); track.info.codecDescription = readBytes(slice, boxInfo.contentSize);
}; break; }; break;
@@ -3407,7 +3419,15 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
} }
return this.decoderConfigPromise ??= (async (): Promise<VideoDecoderConfig> => { return this.decoderConfigPromise ??= (async (): Promise<VideoDecoderConfig> => {
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({}); const firstPacket = await this.getFirstPacket({});
this.internalTrack.info.vp9CodecInfo = firstPacket && extractVp9CodecInfoFromPacket(firstPacket.data); this.internalTrack.info.vp9CodecInfo = firstPacket && extractVp9CodecInfoFromPacket(firstPacket.data);
} else if (this.internalTrack.info.codec === 'av1' && !this.internalTrack.info.av1CodecInfo) { } else if (this.internalTrack.info.codec === 'av1' && !this.internalTrack.info.av1CodecInfo) {
+11 -2
View File
@@ -38,6 +38,7 @@ import {
isWebKit, isWebKit,
last, last,
mapAsyncGenerator, mapAsyncGenerator,
missingWebCodecsClassMessage,
promiseWithResolvers, promiseWithResolvers,
removeItem, removeItem,
Rotation, Rotation,
@@ -1691,8 +1692,12 @@ export class VideoSampleSink extends BaseMediaSampleSink<VideoSample> {
onError: (error: unknown) => unknown, onError: (error: unknown) => unknown,
) { ) {
if (!(await this._track.canDecode())) { if (!(await this._track.canDecode())) {
if (typeof VideoDecoder === 'undefined') {
throw new Error(missingWebCodecsClassMessage('VideoDecoder'));
}
throw new Error( 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.', + ' a track.',
); );
} }
@@ -2426,8 +2431,12 @@ export class AudioSampleSink extends BaseMediaSampleSink<AudioSample> {
onError: (error: unknown) => unknown, onError: (error: unknown) => unknown,
) { ) {
if (!(await this._track.canDecode())) { if (!(await this._track.canDecode())) {
if (typeof AudioDecoder === 'undefined') {
throw new Error(missingWebCodecsClassMessage('AudioDecoder'));
}
throw new Error( 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.', + ' a track.',
); );
} }
+8 -6
View File
@@ -29,6 +29,7 @@ import {
clearIntervalUnthrottled, clearIntervalUnthrottled,
floorToDivisor, floorToDivisor,
last, last,
missingWebCodecsClassMessage,
promiseWithResolvers, promiseWithResolvers,
roundToDivisor, roundToDivisor,
setInt24, setInt24,
@@ -718,7 +719,7 @@ class VideoEncoderWrapper {
if (!selected) { if (!selected) {
if (typeof VideoEncoder === 'undefined') { 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 // 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( throw new Error(
`This specific encoder configuration (${firstConfig.codec}, ${rateControls.join(' / ')},` `This specific encoder configuration (${firstConfig.codec}, ${rateControls.join(' / ')},`
+ ` ${firstConfig.width}x${firstConfig.height}, hardware acceleration:` + ` ${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.`, + ` Consider using another codec or changing your video parameters.`,
); );
} }
@@ -1694,8 +1695,8 @@ export class MediaStreamVideoTrackSource extends VideoSource {
}); });
} else { } else {
throw new Error( throw new Error(
'When no explicit frame rate is set, MediaStreamTrackProcessor is required; but it\'s not supported' 'When no explicit frame rate is set, MediaStreamTrackProcessor is required; but it\'s not available'
+ ' by this browser.', + ' in this environment.',
); );
} }
} }
@@ -2210,7 +2211,7 @@ class AudioEncoderWrapper {
this.initPcmEncoder(); this.initPcmEncoder();
} else { } else {
if (typeof AudioEncoder === 'undefined') { if (typeof AudioEncoder === 'undefined') {
throw new Error('AudioEncoder is not supported by this browser.'); throw new Error(missingWebCodecsClassMessage('AudioEncoder'));
} }
let supported: boolean; let supported: boolean;
@@ -2226,7 +2227,8 @@ class AudioEncoderWrapper {
throw new Error( throw new Error(
`This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps,` `This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps,`
+ ` ${encoderConfig.numberOfChannels} channels, ${encoderConfig.sampleRate} Hz) is not` + ` ${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.`,
); );
} }
+10
View File
@@ -721,6 +721,16 @@ export const getChromiumVersion = () => {
return chromiumVersionCache = Number(match[1]!); 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. * T or a promise that resolves to T.
* @group Miscellaneous * @group Miscellaneous
+3 -3
View File
@@ -155,7 +155,7 @@ export class EncodedPacket {
throw new TypeError('Metadata-only packets cannot be converted to a video chunk.'); throw new TypeError('Metadata-only packets cannot be converted to a video chunk.');
} }
if (typeof EncodedVideoChunk === 'undefined') { 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({ return new EncodedVideoChunk({
@@ -179,7 +179,7 @@ export class EncodedPacket {
throw new TypeError('Metadata-only packets cannot be converted to a video chunk.'); throw new TypeError('Metadata-only packets cannot be converted to a video chunk.');
} }
if (typeof EncodedVideoChunk === 'undefined') { 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({ return new EncodedVideoChunk({
@@ -198,7 +198,7 @@ export class EncodedPacket {
throw new TypeError('Metadata-only packets cannot be converted to an audio chunk.'); throw new TypeError('Metadata-only packets cannot be converted to an audio chunk.');
} }
if (typeof EncodedAudioChunk === 'undefined') { 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({ return new EncodedAudioChunk({
+1
View File
@@ -76,6 +76,7 @@ const convertVideo = async (quality: Quality, codec: VideoCodec, allowMissingQua
trim: { start: 0, end: 2 }, trim: { start: 0, end: 2 },
video: { codec, quality }, video: { codec, quality },
audio: { discard: true }, audio: { discard: true },
showWarnings: false,
}); });
if (allowMissingQuantizerSupport && !conversion.isValid) { if (allowMissingQuantizerSupport && !conversion.isValid) {
+20
View File
@@ -140,3 +140,23 @@ test('QuickTime nclc color information', async () => {
}); });
expect(await track.hasHighDynamicRange()).toBe(true); 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]);
});
Binary file not shown.