Add custom quality factors and quantizer bitrate mode (#448)

* Add custom quality factors and quantizer bitrate mode (closes #327)

* Fix quantizer mode fallback and per-frame quantizer edge cases

* Clean up quantizer mode tests

* Quantizer implementation

* Add quality tests

* Improve conversion codec error message, make test more lenient

* Add quantizer support blog post

---------

Co-authored-by: Vanilagy <[email protected]>
This commit is contained in:
Don Carignan
2026-07-30 17:24:59 +02:00
committed by GitHub
co-authored by Vanilagy
parent 1a99b0371d
commit c3df2a24e5
48 changed files with 1372 additions and 371 deletions
@@ -1,13 +1,13 @@
import {
Input,
ALL_FORMATS,
BlobSource,
UrlSource,
Output,
BufferTarget,
Mp4OutputFormat,
Conversion,
QUALITY_VERY_LOW,
Input,
Mp4OutputFormat,
Output,
Quality,
UrlSource,
} from 'mediabunny';
import { registerAc3Decoder } from '@mediabunny/ac3';
import { registerProresDecoder } from '@mediabunny/prores';
@@ -72,11 +72,11 @@ const compressFile = async (resource: File | string) => {
tracks: 'primary', // Keep only one track per type
video: {
width: 320, // Height will be deduced automatically to retain aspect ratio
bitrate: QUALITY_VERY_LOW,
quality: new Quality('very-low'),
},
audio: {
codec: 'opus',
bitrate: QUALITY_VERY_LOW,
quality: new Quality('very-low'),
},
});
+13 -17
View File
@@ -1,19 +1,15 @@
import {
Input,
ALL_FORMATS,
BlobSource,
UrlSource,
BufferTarget,
Conversion,
HlsOutputFormat,
Input,
MpegTsOutputFormat,
Output,
PathedTarget,
BufferTarget,
HlsOutputFormat,
MpegTsOutputFormat,
Conversion,
QUALITY_VERY_HIGH,
QUALITY_HIGH,
QUALITY_MEDIUM,
QUALITY_LOW,
QUALITY_VERY_LOW,
Quality,
UrlSource,
} from 'mediabunny';
import { registerAc3Decoder } from '@mediabunny/ac3';
import { registerProresDecoder } from '@mediabunny/prores';
@@ -101,14 +97,14 @@ const convertToHls = async (resource: File | string) => {
output,
tracks: 'primary', // Use only the primary video and audio tracks of the input
video: [
{ codec: 'avc', height: 1080, bitrate: QUALITY_VERY_HIGH },
{ codec: 'avc', height: 720, bitrate: QUALITY_HIGH },
{ codec: 'avc', height: 480, bitrate: QUALITY_MEDIUM },
{ codec: 'avc', height: 360, bitrate: QUALITY_LOW },
{ codec: 'avc', height: 240, bitrate: QUALITY_VERY_LOW },
{ codec: 'avc', height: 1080, quality: new Quality('very-high') },
{ codec: 'avc', height: 720, quality: new Quality('high') },
{ codec: 'avc', height: 480, quality: new Quality('medium') },
{ codec: 'avc', height: 360, quality: new Quality('low') },
{ codec: 'avc', height: 240, quality: new Quality('very-low') },
],
audio: [
{ codec: 'aac', bitrate: QUALITY_HIGH },
{ codec: 'aac', quality: new Quality('high') },
],
});
+4 -4
View File
@@ -4,7 +4,7 @@ import {
MediaStreamAudioTrackSource,
Mp4OutputFormat,
Output,
QUALITY_MEDIUM,
Quality,
StreamTarget,
} from 'mediabunny';
@@ -48,7 +48,7 @@ const startRecording = async () => {
context.fillRect(0, 0, canvas.width, canvas.height);
const audioIsEncodable = await canEncodeAudio('opus', {
bitrate: QUALITY_MEDIUM,
quality: new Quality('medium'),
});
let audioTrack: MediaStreamAudioTrack | null = null;
@@ -99,7 +99,7 @@ const startRecording = async () => {
// Add the video track, with the canvas as the source
videoSource = new CanvasSource(canvas, {
codec: 'avc',
bitrate: QUALITY_MEDIUM,
quality: new Quality('medium'),
keyFrameInterval: 0.5,
latencyMode: 'realtime', // Allow the encoder to skip frames to keep up with real-time constraints
});
@@ -109,7 +109,7 @@ const startRecording = async () => {
// Add the audio track, with the media stream track as the source
const audioSource = new MediaStreamAudioTrackSource(audioTrack, {
codec: 'opus',
bitrate: QUALITY_MEDIUM,
quality: new Quality('medium'),
});
audioSource.errorPromise.catch(cancelRecording); // Make sure errors are bubbled up
@@ -1,13 +1,13 @@
import {
Output,
BufferTarget,
Mp4OutputFormat,
CanvasSource,
AudioBufferSource,
QUALITY_HIGH,
BufferTarget,
CanvasSource,
getFirstEncodableAudioCodec,
getFirstEncodableVideoCodec,
Mp4OutputFormat,
Output,
OutputFormat,
Quality,
} from 'mediabunny';
const durationSlider = document.querySelector('#duration-slider') as HTMLInputElement;
@@ -102,7 +102,7 @@ const generateVideo = async () => {
// For video, we use a CanvasSource for convenience, as we're rendering to a canvas
const canvasSource = new CanvasSource(renderCanvas, {
codec: videoCodec,
bitrate: QUALITY_HIGH,
quality: new Quality('high'),
});
output.addVideoTrack(canvasSource, { frameRate });
@@ -117,7 +117,7 @@ const generateVideo = async () => {
if (audioCodec) {
audioBufferSource = new AudioBufferSource({
codec: audioCodec,
bitrate: QUALITY_HIGH,
quality: new Quality('high'),
});
output.addAudioTrack(audioBufferSource);
} else {