Add more options to Video/AudioEncodingConfig, restructure code (#61)

This commit is contained in:
Vanilagy
2025-08-22 17:24:02 +02:00
parent a56dca10d9
commit 134a6f8f4e
7 changed files with 587 additions and 442 deletions
-269
View File
@@ -12,7 +12,6 @@ import {
HevcDecoderConfigurationRecord,
Vp9CodecInfo,
} from './codec-data';
import { customAudioEncoders, customVideoEncoders } from './custom-coder';
import {
Bitstream,
COLOR_PRIMARIES_MAP,
@@ -1124,271 +1123,3 @@ export const validateSubtitleMetadata = (metadata: SubtitleMetadata | undefined)
throw new TypeError('Subtitle metadata config description must be a string.');
}
};
/**
* Checks if the browser is able to encode the given codec.
* @public
*/
export const canEncode = (codec: MediaCodec) => {
if ((VIDEO_CODECS as readonly string[]).includes(codec)) {
return canEncodeVideo(codec as VideoCodec);
} else if ((AUDIO_CODECS as readonly string[]).includes(codec)) {
return canEncodeAudio(codec as AudioCodec);
} else if ((SUBTITLE_CODECS as readonly string[]).includes(codec)) {
return canEncodeSubtitles(codec as SubtitleCodec);
}
throw new TypeError(`Unknown codec '${codec}'.`);
};
/**
* Checks if the browser is able to encode the given video codec with the given parameters.
* @public
*/
export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height = 720, bitrate = 1e6 }: {
width?: number;
height?: number;
bitrate?: number | Quality;
} = {}) => {
if (!VIDEO_CODECS.includes(codec)) {
return false;
}
if (!Number.isInteger(width) || width <= 0) {
throw new TypeError('width must be a positive integer.');
}
if (!Number.isInteger(height) || height <= 0) {
throw new TypeError('height must be a positive integer.');
}
if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
throw new TypeError('bitrate must be a positive integer or a quality.');
}
const resolvedBitrate = bitrate instanceof Quality
? bitrate._toVideoBitrate(codec, width, height)
: bitrate;
if (customVideoEncoders.length > 0) {
const encoderConfig: VideoEncoderConfig = {
codec: buildVideoCodecString(
codec,
width,
height,
resolvedBitrate,
),
width,
height,
bitrate: resolvedBitrate,
...getVideoEncoderConfigExtension(codec),
};
if (customVideoEncoders.some(x => x.supports(codec, encoderConfig))) {
// There's a custom encoder
return true;
}
}
if (typeof VideoEncoder === 'undefined') {
return false;
}
const support = await VideoEncoder.isConfigSupported({
codec: buildVideoCodecString(codec, width, height, resolvedBitrate),
width,
height,
bitrate: resolvedBitrate,
...getVideoEncoderConfigExtension(codec),
});
return support.supported === true;
};
/**
* Checks if the browser is able to encode the given audio codec with the given parameters.
* @public
*/
export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2, sampleRate = 48000, bitrate = 128e3 }: {
numberOfChannels?: number;
sampleRate?: number;
bitrate?: number | Quality;
} = {}) => {
if (!AUDIO_CODECS.includes(codec)) {
return false;
}
if (!Number.isInteger(numberOfChannels) || numberOfChannels <= 0) {
throw new TypeError('numberOfChannels must be a positive integer.');
}
if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
throw new TypeError('sampleRate must be a positive integer.');
}
if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
throw new TypeError('bitrate must be a positive integer.');
}
const resolvedBitrate = bitrate instanceof Quality
? bitrate._toAudioBitrate(codec)
: bitrate;
if (customAudioEncoders.length > 0) {
const encoderConfig: AudioEncoderConfig = {
codec: buildAudioCodecString(
codec,
numberOfChannels,
sampleRate,
),
numberOfChannels,
sampleRate,
bitrate: resolvedBitrate,
...getAudioEncoderConfigExtension(codec),
};
if (customAudioEncoders.some(x => x.supports(codec, encoderConfig))) {
// There's a custom encoder
return true;
}
}
if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return true; // Because we encode these ourselves
}
if (typeof AudioEncoder === 'undefined') {
return false;
}
const support = await AudioEncoder.isConfigSupported({
codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
numberOfChannels,
sampleRate,
bitrate: resolvedBitrate,
...getAudioEncoderConfigExtension(codec),
});
return support.supported === true;
};
/**
* Checks if the browser is able to encode the given subtitle codec.
* @public
*/
export const canEncodeSubtitles = async (codec: SubtitleCodec) => {
if (!SUBTITLE_CODECS.includes(codec)) {
return false;
}
return true;
};
/**
* Returns the list of all media codecs that can be encoded by the browser.
* @public
*/
export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
getEncodableVideoCodecs(),
getEncodableAudioCodecs(),
getEncodableSubtitleCodecs(),
]);
return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
};
/**
* Returns the list of all video codecs that can be encoded by the browser.
* @public
*/
export const getEncodableVideoCodecs = async (
checkedCodecs = VIDEO_CODECS as unknown as VideoCodec[],
options?: {
width?: number;
height?: number;
bitrate?: number | Quality;
},
): Promise<VideoCodec[]> => {
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeVideo(codec, options)));
return checkedCodecs.filter((_, i) => bools[i]);
};
/**
* Returns the list of all audio codecs that can be encoded by the browser.
* @public
*/
export const getEncodableAudioCodecs = async (
checkedCodecs = AUDIO_CODECS as unknown as AudioCodec[],
options?: {
numberOfChannels?: number;
sampleRate?: number;
bitrate?: number | Quality;
},
): Promise<AudioCodec[]> => {
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeAudio(codec, options)));
return checkedCodecs.filter((_, i) => bools[i]);
};
/**
* Returns the list of all subtitle codecs that can be encoded by the browser.
* @public
*/
export const getEncodableSubtitleCodecs = async (
checkedCodecs = SUBTITLE_CODECS as unknown as SubtitleCodec[],
): Promise<SubtitleCodec[]> => {
const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles));
return checkedCodecs.filter((_, i) => bools[i]);
};
/**
* Returns the first video codec from the given list that can be encoded by the browser.
* @public
*/
export const getFirstEncodableVideoCodec = async (
checkedCodecs: VideoCodec[],
options?: {
width?: number;
height?: number;
bitrate?: number | Quality;
},
): Promise<VideoCodec | null> => {
for (const codec of checkedCodecs) {
if (await canEncodeVideo(codec, options)) {
return codec;
}
}
return null;
};
/**
* Returns the first audio codec from the given list that can be encoded by the browser.
* @public
*/
export const getFirstEncodableAudioCodec = async (
checkedCodecs: AudioCodec[],
options?: {
numberOfChannels?: number;
sampleRate?: number;
bitrate?: number | Quality;
},
): Promise<AudioCodec | null> => {
for (const codec of checkedCodecs) {
if (await canEncodeAudio(codec, options)) {
return codec;
}
}
return null;
};
/**
* Returns the first subtitle codec from the given list that can be encoded by the browser.
* @public
*/
export const getFirstEncodableSubtitleCodec = async (
checkedCodecs: SubtitleCodec[],
): Promise<SubtitleCodec | null> => {
for (const codec of checkedCodecs) {
if (await canEncodeSubtitles(codec)) {
return codec;
}
}
return null;
};