Add new utility functions for finding best codec

This commit is contained in:
Vanilagy
2025-06-05 14:01:39 +02:00
parent b514aa9999
commit d0d847edf8
4 changed files with 148 additions and 37 deletions
+43 -13
View File
@@ -116,8 +116,9 @@ canEncodeAudio('aac', {
}); // => Promise<boolean> }); // => Promise<boolean>
``` ```
In addition, you can use the following functions which check encodability for multiple codecs at once and return a list of ---
supported codecs:
In addition, you can use the following functions to check encodability for multiple codecs at once, getting back a list of supported codecs:
```ts ```ts
import { import {
getEncodableCodecs, getEncodableCodecs,
@@ -126,25 +127,54 @@ import {
getEncodableSubtitleCodecs, getEncodableSubtitleCodecs,
} from 'mediakit'; } from 'mediakit';
getEncodableCodecs(); // Promise<MediaCodec[]> getEncodableCodecs(); // => Promise<MediaCodec[]>
getEncodableVideoCodecs(); // Promise<VideoCodec[]> getEncodableVideoCodecs(); // => Promise<VideoCodec[]>
getEncodableAudioCodecs(); // Promise<AudioCodec[]> getEncodableAudioCodecs(); // => Promise<AudioCodec[]>
getEncodableSubtitleCodecs(); // Promise<SubtitleCodec[]> getEncodableSubtitleCodecs(); // => Promise<SubtitleCodec[]>
```
These functions also accept optional configuration options: // These functions also accept optional configuration options.
```ts // Here, we check which of AVC, HEVC and VP8 can be encoded at 1920x1080 @10Mbps:
import { getEncodableVideoCodecs } from 'mediakit';
// Checks only which of AVC, HEVC and VP8 can be encoded at 1920x1080 @10Mbps:
getEncodableVideoCodecs( getEncodableVideoCodecs(
['avc', 'hevc', 'vp8'], ['avc', 'hevc', 'vp8'],
{ width: 1920, height: 1080, bitrate: 1e7 }, { width: 1920, height: 1080, bitrate: 1e7 },
); // => Promise<VideoCodec[]> ); // => Promise<VideoCodec[]>
``` ```
---
If you simply want to find the best codec that the browser can encode, you can use these functions, which return the first codec supported by the browser:
```ts
import {
getFirstEncodableVideoCodec,
getFirstEncodableAudioCodec,
getFirstEncodableSubtitleCodec,
} from 'mediakit';
getFirstEncodableVideoCodec(['avc', 'vp9', 'av1']); // => Promise<VideoCodec | null>
getFirstEncodableAudioCodec(['opus', 'aac']); // => Promise<AudioCodec | null>
getEncodableVideoCodecs(
['avc', 'hevc', 'vp8'],
{ width: 1920, height: 1080, bitrate: 1e7 },
); // => Promise<VideoCodec | null>
```
If none of the listed codecs is supported, `null` is returned.
These functions are especially useful in conjunction with an [output format](./output-formats) to retrieve the best codec that is supported both by the encoder as well as the container format:
```ts
import {
Mp4OutputFormat,
getFirstEncodableVideoCodec,
} from 'mediakit';
const outputFormat = new Mp4OutputFormat();
const containableVideoCodecs = outputFormat.getSupportedVideoCodecs();
const bestVideoCodec = await getFirstEncodableVideoCodec(containableVideoCodecs);
```
::: info ::: info
These checks also take [custom encoders](#custom-encoders) into account. Codec encodability checks take [custom encoders](#custom-encoders) into account.
::: :::
## Querying codec decodability ## Querying codec decodability
@@ -5,6 +5,8 @@ import {
CanvasSource, CanvasSource,
AudioBufferSource, AudioBufferSource,
QUALITY_HIGH, QUALITY_HIGH,
getFirstEncodableAudioCodec,
getFirstEncodableVideoCodec,
} from 'mediakit'; } from 'mediakit';
const durationSlider = document.querySelector('#duration-slider') as HTMLInputElement; const durationSlider = document.querySelector('#duration-slider') as HTMLInputElement;
@@ -47,6 +49,7 @@ const scaleHues = [
const wallWidth = 10; const wallWidth = 10;
const frameRate = 60; const frameRate = 60;
const numberOfChannels = 2;
const sampleRate = 48000; const sampleRate = 48000;
let balls: Ball[] = []; let balls: Ball[] = [];
@@ -84,19 +87,39 @@ const generateVideo = async () => {
format: new Mp4OutputFormat(), format: new Mp4OutputFormat(),
}); });
// Retrieve the first video codec supported by this browser that can be contained in the output format
const videoCodec = await getFirstEncodableVideoCodec(output.format.getSupportedVideoCodecs(), {
width: renderCanvas.width,
height: renderCanvas.height,
});
if (!videoCodec) {
throw new Error('Your browser doesn\'t support video encoding.');
}
// For video, we use a CanvasSource for convenience, as we're rendering to a canvas // For video, we use a CanvasSource for convenience, as we're rendering to a canvas
const canvasSource = new CanvasSource(renderCanvas, { const canvasSource = new CanvasSource(renderCanvas, {
codec: 'avc', codec: videoCodec,
bitrate: QUALITY_HIGH, bitrate: QUALITY_HIGH,
}); });
output.addVideoTrack(canvasSource); output.addVideoTrack(canvasSource);
// For audio, we use ArrayBufferSource, because we'll be creating an ArrayBuffer with OfflineAudioContext // For audio, we use ArrayBufferSource, because we'll be creating an ArrayBuffer with OfflineAudioContext
const audioBufferSource = new AudioBufferSource({ let audioBufferSource: AudioBufferSource | null = null;
codec: 'aac',
bitrate: QUALITY_HIGH, // Retrieve the first audio codec supported by this browser that can be contained in the output format
const audioCodec = await getFirstEncodableAudioCodec(output.format.getSupportedAudioCodecs(), {
numberOfChannels,
sampleRate,
}); });
output.addAudioTrack(audioBufferSource); if (audioCodec) {
audioBufferSource = new AudioBufferSource({
codec: audioCodec,
bitrate: QUALITY_HIGH,
});
output.addAudioTrack(audioBufferSource);
} else {
alert('Your browser doesn\'t support audio encoding, so we won\'t include audio in the output file.');
}
await output.start(); await output.start();
@@ -105,10 +128,10 @@ const generateVideo = async () => {
// Start an interval that updates the progress bar // Start an interval that updates the progress bar
progressInterval = window.setInterval(() => { progressInterval = window.setInterval(() => {
const videoProgress = currentFrame / totalFrames; const videoProgress = currentFrame / totalFrames;
const overallProgress = videoProgress * 0.9; // 90% when 100% of the video has been rendered const overallProgress = videoProgress * (audioBufferSource ? 0.9 : 0.95);
progressBar.style.width = `${overallProgress * 100}%`; progressBar.style.width = `${overallProgress * 100}%`;
if (currentFrame === totalFrames) { if (currentFrame === totalFrames && audioBufferSource) {
progressText.textContent = 'Rendering audio...'; progressText.textContent = 'Rendering audio...';
} else { } else {
progressText.textContent = `Rendering frame ${currentFrame}/${totalFrames}`; progressText.textContent = `Rendering frame ${currentFrame}/${totalFrames}`;
@@ -130,11 +153,13 @@ const generateVideo = async () => {
// Signal to the output that no more video frames are coming (not necessary, but recommended) // Signal to the output that no more video frames are coming (not necessary, but recommended)
canvasSource.close(); canvasSource.close();
// Let's render the audio. Ideally, the audio is rendered before the video (or concurrently to it), but for if (audioBufferSource) {
// simplicity, we're rendering it after we've cranked through all frames. // Let's render the audio. Ideally, the audio is rendered before the video (or concurrently to it), but for
const audioBuffer = await offlineAudioContext.startRendering(); // simplicity, we're rendering it after we've cranked through all frames.
await audioBufferSource.add(audioBuffer); const audioBuffer = await offlineAudioContext.startRendering();
audioBufferSource.close(); await audioBufferSource.add(audioBuffer);
audioBufferSource.close();
}
clearInterval(progressInterval); clearInterval(progressInterval);
@@ -172,7 +197,7 @@ const generateVideo = async () => {
/** === SCENE SIMULATION LOGIC === */ /** === SCENE SIMULATION LOGIC === */
const initScene = (duration: number) => { const initScene = (duration: number) => {
offlineAudioContext = new OfflineAudioContext(2, duration * sampleRate, sampleRate); offlineAudioContext = new OfflineAudioContext(numberOfChannels, duration * sampleRate, sampleRate);
// Create reverb effect // Create reverb effect
offlineReverbConvolver = offlineAudioContext.createConvolver(); offlineReverbConvolver = offlineAudioContext.createConvolver();
@@ -467,7 +492,7 @@ const getFrequencyFromScaleIndex = (scaleIndex: number) => {
const createReverbImpulse = (duration: number) => { const createReverbImpulse = (duration: number) => {
const length = sampleRate * duration; const length = sampleRate * duration;
const impulse = offlineAudioContext.createBuffer(2, length, sampleRate); const impulse = offlineAudioContext.createBuffer(numberOfChannels, length, sampleRate);
for (let channel = 0; channel < 2; channel++) { for (let channel = 0; channel < 2; channel++) {
const channelData = impulse.getChannelData(channel); const channelData = impulse.getChannelData(channel);
+63 -10
View File
@@ -742,16 +742,11 @@ export class Quality {
vp8: 1.2, // Slightly less efficient than AVC vp8: 1.2, // Slightly less efficient than AVC
}; };
const getBaseBitrateForPixels = (pixelCount: number): number => { const referencePixels = 1920 * 1080;
const referencePixels = 1920 * 1080; const referenceBitrate = 3000000;
const referenceBitrate = 2000000; const scaleFactor = Math.pow(pixels / referencePixels, 0.95); // Slight non-linear scaling
const baseBitrate = referenceBitrate * scaleFactor;
// Non-linear scaling
const scaleFactor = Math.pow(pixelCount / referencePixels, 0.75);
return referenceBitrate * scaleFactor;
};
const baseBitrate = getBaseBitrateForPixels(pixels);
const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec]; const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec];
const finalBitrate = codecAdjustedBitrate * this._factor; const finalBitrate = codecAdjustedBitrate * this._factor;
@@ -804,7 +799,7 @@ export class Quality {
* Represents a very low media quality. * Represents a very low media quality.
* @public * @public
*/ */
export const QUALITY_VERY_LOW = new Quality(0.4); export const QUALITY_VERY_LOW = new Quality(0.3);
/** /**
* Represents a low media quality. * Represents a low media quality.
* @public * @public
@@ -1329,3 +1324,61 @@ export const getEncodableSubtitleCodecs = async (
const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles)); const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles));
return checkedCodecs.filter((_, i) => bools[i]); 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;
};
+3
View File
@@ -68,6 +68,9 @@ export {
getEncodableVideoCodecs, getEncodableVideoCodecs,
getEncodableAudioCodecs, getEncodableAudioCodecs,
getEncodableSubtitleCodecs, getEncodableSubtitleCodecs,
getFirstEncodableVideoCodec,
getFirstEncodableAudioCodec,
getFirstEncodableSubtitleCodec,
} from './codec'; } from './codec';
export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target'; export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target';
export { Rotation, AnyIterable, SetRequired } from './misc'; export { Rotation, AnyIterable, SetRequired } from './misc';