mediabunny
Version:
Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.
196 lines (195 loc) • 8.23 kB
JavaScript
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { AUDIO_CODECS, buildAudioCodecString, buildVideoCodecString, guessDescriptionForAudio, guessDescriptionForVideo, inferCodecFromCodecString, PCM_AUDIO_CODECS, VIDEO_CODECS, } from './codec.js';
import { customAudioDecoders, customVideoDecoders } from './custom-coder.js';
import { isAllowSharedBufferSource } from './misc.js';
export const canDecodeVideoMemo = new Map();
export const canDecodeAudioMemo = new Map();
const validateVideoDecodingConfig = (codec, options) => {
if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.');
}
if (options.codec !== undefined && typeof options.codec !== 'string') {
throw new TypeError('options.codec, when provided, must be a string.');
}
if (options.codec !== undefined && inferCodecFromCodecString(options.codec) !== codec) {
throw new TypeError(`options.codec, when provided, must match the specified codec (${codec}).`);
}
if (options.codedWidth !== undefined
&& (!Number.isInteger(options.codedWidth) || options.codedWidth <= 0)) {
throw new TypeError('options.codedWidth, when provided, must be a positive integer.');
}
if (options.codedHeight !== undefined
&& (!Number.isInteger(options.codedHeight) || options.codedHeight <= 0)) {
throw new TypeError('options.codedHeight, when provided, must be a positive integer.');
}
if (options.displayAspectWidth !== undefined
&& (!Number.isInteger(options.displayAspectWidth) || options.displayAspectWidth <= 0)) {
throw new TypeError('options.displayAspectWidth, when provided, must be a positive integer.');
}
if (options.displayAspectHeight !== undefined
&& (!Number.isInteger(options.displayAspectHeight) || options.displayAspectHeight <= 0)) {
throw new TypeError('options.displayAspectHeight, when provided, must be a positive integer.');
}
if (options.description !== undefined && !isAllowSharedBufferSource(options.description)) {
throw new TypeError('options.description, when provided, must be a buffer source.');
}
if (options.hardwareAcceleration !== undefined
&& !['no-preference', 'prefer-hardware', 'prefer-software'].includes(options.hardwareAcceleration)) {
throw new TypeError('options.hardwareAcceleration, when provided, must be \'no-preference\', \'prefer-hardware\' or'
+ ' \'prefer-software\'.');
}
if (options.optimizeForLatency !== undefined && typeof options.optimizeForLatency !== 'boolean') {
throw new TypeError('options.optimizeForLatency, when provided, must be a boolean.');
}
};
const validateAudioDecodingConfig = (codec, options) => {
if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.');
}
if (options.codec !== undefined && typeof options.codec !== 'string') {
throw new TypeError('options.codec, when provided, must be a string.');
}
if (options.codec !== undefined && inferCodecFromCodecString(options.codec) !== codec) {
throw new TypeError(`options.codec, when provided, must match the specified codec (${codec}).`);
}
if (options.numberOfChannels !== undefined
&& (!Number.isInteger(options.numberOfChannels) || options.numberOfChannels <= 0)) {
throw new TypeError('options.numberOfChannels, when provided, must be a positive integer.');
}
if (options.sampleRate !== undefined
&& (!Number.isInteger(options.sampleRate) || options.sampleRate <= 0)) {
throw new TypeError('options.sampleRate, when provided, must be a positive integer.');
}
if (options.description !== undefined && !isAllowSharedBufferSource(options.description)) {
throw new TypeError('options.description, when provided, must be a buffer source.');
}
};
/**
* Checks if the browser is able to decode the given codec.
* @group Decoding
* @public
*/
export const canDecode = (codec) => {
if (VIDEO_CODECS.includes(codec)) {
return canDecodeVideo(codec);
}
else if (AUDIO_CODECS.includes(codec)) {
return canDecodeAudio(codec);
}
return false;
};
/**
* Checks if the browser is able to decode the given video codec with the given parameters.
* @group Decoding
* @public
*/
export const canDecodeVideo = async (codec, options = {}) => {
if (!VIDEO_CODECS.includes(codec)) {
return false;
}
validateVideoDecodingConfig(codec, options);
const resolvedOptions = {
...options,
codedWidth: options.codedWidth ?? 1280,
codedHeight: options.codedHeight ?? 720,
codec: options.codec ?? buildVideoCodecString(codec, 1280, 720, 1e6),
};
resolvedOptions.description ??= guessDescriptionForVideo(resolvedOptions);
const key = JSON.stringify(resolvedOptions);
const memoized = canDecodeVideoMemo.get(key);
if (memoized) {
return memoized;
}
const promise = (async () => {
if (customVideoDecoders.some(x => x.supports(codec, resolvedOptions))) {
return true;
}
if (typeof VideoDecoder === 'undefined') {
return false;
}
const support = await VideoDecoder.isConfigSupported(resolvedOptions);
return support.supported === true;
})();
canDecodeVideoMemo.set(key, promise);
return promise;
};
/**
* Checks if the browser is able to decode the given audio codec with the given parameters.
* @group Decoding
* @public
*/
export const canDecodeAudio = async (codec, options = {}) => {
if (!AUDIO_CODECS.includes(codec)) {
return false;
}
validateAudioDecodingConfig(codec, options);
const resolvedOptions = {
...options,
numberOfChannels: options.numberOfChannels ?? 2,
sampleRate: options.sampleRate ?? 48000,
codec: options.codec ?? buildAudioCodecString(codec, 2, 48000),
};
if (resolvedOptions.description === undefined) {
const generatedDescription = guessDescriptionForAudio(resolvedOptions);
if (generatedDescription === false) {
return false;
}
resolvedOptions.description = generatedDescription;
}
const key = JSON.stringify(resolvedOptions);
const memoized = canDecodeAudioMemo.get(key);
if (memoized) {
return memoized;
}
const promise = (async () => {
if (customAudioDecoders.some(x => x.supports(codec, resolvedOptions))) {
return true;
}
if (PCM_AUDIO_CODECS.includes(codec)) {
return true;
}
if (typeof AudioDecoder === 'undefined') {
return false;
}
const support = await AudioDecoder.isConfigSupported(resolvedOptions);
return support.supported === true;
})();
canDecodeAudioMemo.set(key, promise);
return promise;
};
/**
* Returns the list of all media codecs that can be decoded by the browser.
* @group Decoding
* @public
*/
export const getDecodableCodecs = async () => {
const [videoCodecs, audioCodecs] = await Promise.all([
getDecodableVideoCodecs(),
getDecodableAudioCodecs(),
]);
return [...videoCodecs, ...audioCodecs];
};
/**
* Returns the list of all video codecs that can be decoded by the browser.
* @group Decoding
* @public
*/
export const getDecodableVideoCodecs = async (checkedCodecs = VIDEO_CODECS, options) => {
const bools = await Promise.all(checkedCodecs.map(codec => canDecodeVideo(codec, options)));
return checkedCodecs.filter((_, i) => bools[i]);
};
/**
* Returns the list of all audio codecs that can be decoded by the browser.
* @group Decoding
* @public
*/
export const getDecodableAudioCodecs = async (checkedCodecs = AUDIO_CODECS, options) => {
const bools = await Promise.all(checkedCodecs.map(codec => canDecodeAudio(codec, options)));
return checkedCodecs.filter((_, i) => bools[i]);
};