From d0d847edf88c7c818ed2b3f160cc91f1d45d10ba Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:01:39 +0200 Subject: [PATCH] Add new utility functions for finding best codec --- docs/guide/supported-formats-and-codecs.md | 56 ++++++++++---- .../procedural-generation.ts | 53 ++++++++++---- src/codec.ts | 73 ++++++++++++++++--- src/index.ts | 3 + 4 files changed, 148 insertions(+), 37 deletions(-) diff --git a/docs/guide/supported-formats-and-codecs.md b/docs/guide/supported-formats-and-codecs.md index cf58b4f..e7ba13c 100644 --- a/docs/guide/supported-formats-and-codecs.md +++ b/docs/guide/supported-formats-and-codecs.md @@ -116,8 +116,9 @@ canEncodeAudio('aac', { }); // => Promise ``` -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 import { getEncodableCodecs, @@ -126,25 +127,54 @@ import { getEncodableSubtitleCodecs, } from 'mediakit'; -getEncodableCodecs(); // Promise -getEncodableVideoCodecs(); // Promise -getEncodableAudioCodecs(); // Promise -getEncodableSubtitleCodecs(); // Promise -``` +getEncodableCodecs(); // => Promise +getEncodableVideoCodecs(); // => Promise +getEncodableAudioCodecs(); // => Promise +getEncodableSubtitleCodecs(); // => Promise -These functions also accept optional configuration options: -```ts -import { getEncodableVideoCodecs } from 'mediakit'; - -// Checks only which of AVC, HEVC and VP8 can be encoded at 1920x1080 @10Mbps: +// These functions also accept optional configuration options. +// Here, we check which of AVC, HEVC and VP8 can be encoded at 1920x1080 @10Mbps: getEncodableVideoCodecs( ['avc', 'hevc', 'vp8'], { width: 1920, height: 1080, bitrate: 1e7 }, ); // => Promise ``` +--- + +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 +getFirstEncodableAudioCodec(['opus', 'aac']); // => Promise + +getEncodableVideoCodecs( + ['avc', 'hevc', 'vp8'], + { width: 1920, height: 1080, bitrate: 1e7 }, +); // => Promise +``` + +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 -These checks also take [custom encoders](#custom-encoders) into account. +Codec encodability checks take [custom encoders](#custom-encoders) into account. ::: ## Querying codec decodability diff --git a/examples/procedural-generation/procedural-generation.ts b/examples/procedural-generation/procedural-generation.ts index 2df63f8..c0e73e1 100644 --- a/examples/procedural-generation/procedural-generation.ts +++ b/examples/procedural-generation/procedural-generation.ts @@ -5,6 +5,8 @@ import { CanvasSource, AudioBufferSource, QUALITY_HIGH, + getFirstEncodableAudioCodec, + getFirstEncodableVideoCodec, } from 'mediakit'; const durationSlider = document.querySelector('#duration-slider') as HTMLInputElement; @@ -47,6 +49,7 @@ const scaleHues = [ const wallWidth = 10; const frameRate = 60; +const numberOfChannels = 2; const sampleRate = 48000; let balls: Ball[] = []; @@ -84,19 +87,39 @@ const generateVideo = async () => { 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 const canvasSource = new CanvasSource(renderCanvas, { - codec: 'avc', + codec: videoCodec, bitrate: QUALITY_HIGH, }); output.addVideoTrack(canvasSource); // For audio, we use ArrayBufferSource, because we'll be creating an ArrayBuffer with OfflineAudioContext - const audioBufferSource = new AudioBufferSource({ - codec: 'aac', - bitrate: QUALITY_HIGH, + let audioBufferSource: AudioBufferSource | null = null; + + // 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(); @@ -105,10 +128,10 @@ const generateVideo = async () => { // Start an interval that updates the progress bar progressInterval = window.setInterval(() => { 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}%`; - if (currentFrame === totalFrames) { + if (currentFrame === totalFrames && audioBufferSource) { progressText.textContent = 'Rendering audio...'; } else { 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) canvasSource.close(); - // Let's render the audio. Ideally, the audio is rendered before the video (or concurrently to it), but for - // simplicity, we're rendering it after we've cranked through all frames. - const audioBuffer = await offlineAudioContext.startRendering(); - await audioBufferSource.add(audioBuffer); - audioBufferSource.close(); + if (audioBufferSource) { + // Let's render the audio. Ideally, the audio is rendered before the video (or concurrently to it), but for + // simplicity, we're rendering it after we've cranked through all frames. + const audioBuffer = await offlineAudioContext.startRendering(); + await audioBufferSource.add(audioBuffer); + audioBufferSource.close(); + } clearInterval(progressInterval); @@ -172,7 +197,7 @@ const generateVideo = async () => { /** === SCENE SIMULATION LOGIC === */ const initScene = (duration: number) => { - offlineAudioContext = new OfflineAudioContext(2, duration * sampleRate, sampleRate); + offlineAudioContext = new OfflineAudioContext(numberOfChannels, duration * sampleRate, sampleRate); // Create reverb effect offlineReverbConvolver = offlineAudioContext.createConvolver(); @@ -467,7 +492,7 @@ const getFrequencyFromScaleIndex = (scaleIndex: number) => { const createReverbImpulse = (duration: number) => { 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++) { const channelData = impulse.getChannelData(channel); diff --git a/src/codec.ts b/src/codec.ts index b772b17..b73a1b5 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -742,16 +742,11 @@ export class Quality { vp8: 1.2, // Slightly less efficient than AVC }; - const getBaseBitrateForPixels = (pixelCount: number): number => { - const referencePixels = 1920 * 1080; - const referenceBitrate = 2000000; + const referencePixels = 1920 * 1080; + const referenceBitrate = 3000000; + 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 finalBitrate = codecAdjustedBitrate * this._factor; @@ -804,7 +799,7 @@ export class Quality { * Represents a very low media quality. * @public */ -export const QUALITY_VERY_LOW = new Quality(0.4); +export const QUALITY_VERY_LOW = new Quality(0.3); /** * Represents a low media quality. * @public @@ -1329,3 +1324,61 @@ export const getEncodableSubtitleCodecs = async ( 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 => { + 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 => { + 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 => { + for (const codec of checkedCodecs) { + if (await canEncodeSubtitles(codec)) { + return codec; + } + } + + return null; +}; diff --git a/src/index.ts b/src/index.ts index 1022169..b3a05b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,6 +68,9 @@ export { getEncodableVideoCodecs, getEncodableAudioCodecs, getEncodableSubtitleCodecs, + getFirstEncodableVideoCodec, + getFirstEncodableAudioCodec, + getFirstEncodableSubtitleCodec, } from './codec'; export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target'; export { Rotation, AnyIterable, SetRequired } from './misc';