mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 16:53:47 +02:00
Add new utility functions for finding best codec
This commit is contained in:
@@ -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
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user