From c3df2a24e5b6598029458aa607d71f648708dc8a Mon Sep 17 00:00:00 2001 From: Don Carignan Date: Thu, 30 Jul 2026 11:24:59 -0400 Subject: [PATCH] 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 <1696106+Vanilagy@users.noreply.github.com> --- dev/convert.html | 24 +- dev/live.html | 4 +- dev/mux.html | 2 +- docs/.vitepress/config.mts | 9 +- docs/blog.md | 2 +- docs/blog/mediabunny-now-supports-hls.md | 6 +- docs/blog/quantizer-support.md | 125 ++++ docs/codec-registry/av1.md | 4 + docs/codec-registry/avc.md | 6 +- docs/codec-registry/hevc.md | 4 + docs/codec-registry/vp8.md | 4 + docs/codec-registry/vp9.md | 4 + docs/guide/converting-media-files.md | 19 +- docs/guide/extensions/server.md | 4 +- docs/guide/media-sources.md | 95 ++- docs/guide/quick-start.md | 42 +- docs/guide/supported-formats-and-codecs.md | 12 +- docs/guide/writing-hls.md | 16 +- docs/guide/writing-media-files.md | 6 +- docs/index.md | 4 +- docs/public/crf.png | Bin 0 -> 200510 bytes examples/file-compression/file-compression.ts | 14 +- examples/hls-transcoding/hls-transcoding.ts | 30 +- examples/live-recording/live-recording.ts | 8 +- .../procedural-generation.ts | 14 +- packages/server/README.md | 4 +- packages/server/src/audio-encoder.ts | 6 +- packages/server/src/video-encoder.ts | 104 ++- scripts/generate-api-docs.ts | 6 +- src/conversion.ts | 84 ++- src/encode.ts | 615 +++++++++++++++--- src/index.ts | 9 + src/media-source.ts | 145 +++-- src/misc.ts | 4 + test/browser/cmaf.test.ts | 8 +- test/browser/conversion.test.ts | 8 +- test/browser/encode-dimensions.test.ts | 8 +- test/browser/hls-output.test.ts | 4 +- test/browser/media-sources.test.ts | 54 +- test/browser/mpeg-ts-muxing.test.ts | 10 +- test/browser/ogg-muxer.test.ts | 5 +- test/browser/quality.test.ts | 92 +++ test/browser/transparency.test.ts | 8 +- test/browser/video-samples.test.ts | 4 +- test/node/aac-encoder-extension.test.ts | 6 +- test/node/ac3.test.ts | 10 +- test/node/mp3-encoder-extension.test.ts | 6 +- test/node/server-extension.test.ts | 85 ++- 48 files changed, 1372 insertions(+), 371 deletions(-) create mode 100644 docs/blog/quantizer-support.md create mode 100644 docs/public/crf.png create mode 100644 test/browser/quality.test.ts diff --git a/dev/convert.html b/dev/convert.html index 9b5117e..13dce45 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -107,15 +107,15 @@ /* video: { discard: true, - bitrate: Mediabunny.QUALITY_VERY_HIGH, - //bitrate: Mediabunny.QUALITY_VERY_HIGH + quality: new Mediabunny.Quality('very-high'), + //quality: new Mediabunny.Quality('very-high') codec: 'av1', //width: 800, //fit: 'fill', //rotate: 90, }, audio: { - bitrate: Mediabunny.QUALITY_HIGH, + quality: new Mediabunny.Quality('high'), codec: 'aac', }, */ @@ -129,10 +129,15 @@ }, */ video: { - process: (sample) => { - sample.draw(ctx, 0, 0, 1280, 720); - return new Mediabunny.VideoSample(canvas, { timestamp: sample.timestamp, duration: sample.duration }); - }, + codec: 'avc', + bitrate: new Mediabunny.Quality({ + quality: Infinity, + }), + forceTranscode: true, + //process: (sample) => { + // sample.draw(ctx, 0, 0, 1280, 720); + // return new Mediabunny.VideoSample(canvas, { timestamp: sample.timestamp, duration: sample.duration }); + //}, }, tags: {} ?? { title: 'Bigggy', @@ -152,8 +157,9 @@ } }, trim: { - start: -2, - end: 10, + end: 20, + //start: -2, + //end: 10, //start: 300.14984567374756 - 100, //end: 310.1548298151939 - 100, //end: 10, diff --git a/dev/live.html b/dev/live.html index 62e587c..23ee6ff 100644 --- a/dev/live.html +++ b/dev/live.html @@ -30,7 +30,7 @@ if (videoTrack) { videoSource = new Mediabunny.MediaStreamVideoTrackSource(videoTrack, { codec: 'vp9', - bitrate: Mediabunny.QUALITY_MEDIUM, + quality: new Mediabunny.Quality('medium'), sizeChangeBehavior: 'passThrough', }, { timestampBase: 'unix' }); @@ -41,7 +41,7 @@ if (audioTrack) { audioSource = new Mediabunny.MediaStreamAudioTrackSource(audioTrack, { codec: 'opus', - bitrate: Mediabunny.QUALITY_MEDIUM + quality: new Mediabunny.Quality('medium') }); audioSource.errorPromise.catch((d) => console.log("Hello!!???", d)); diff --git a/dev/mux.html b/dev/mux.html index d643874..e619bb9 100644 --- a/dev/mux.html +++ b/dev/mux.html @@ -65,7 +65,7 @@ /* let sourcy = new Mediabunny.AudioBufferSource({ codec: 'pcm-s16', - bitrate: Mediabunny.QUALITY_HIGH + quality: new Mediabunny.Quality('high') }); output.addAudioTrack(sourcy); diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c8164f0..9f6a58e 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -48,9 +48,7 @@ export default withMermaid({ ['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }], ['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }], ['meta', { property: 'og:site_name', content: 'Mediabunny' }], - ['meta', { property: 'og:image', content: `${ORIGIN}/mediabunny-og-image.png` }], ['meta', { property: 'og:locale', content: 'en-US' }], - ['meta', { name: 'twitter:image', content: `${ORIGIN}/mediabunny-og-image.png` }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:site', content: '@vanilagy' }], ], @@ -268,13 +266,20 @@ export default withMermaid({ }); } + // Blog posts use their header image for link previews, everything else uses the generic one + const imageUrl = isBlogPost && pageData.frontmatter['headerImage'] + ? `${ORIGIN}${pageData.frontmatter['headerImage']}` + : `${ORIGIN}/mediabunny-og-image.png`; + ((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push( ['meta', { property: 'og:type', content: isBlogPost ? 'article' : 'website' }], ['meta', { property: 'og:title', content: title }], ['meta', { property: 'og:description', content: pageData.description || DESCRIPTION }], ['meta', { property: 'og:url', content: canonicalUrl }], + ['meta', { property: 'og:image', content: imageUrl }], ['meta', { name: 'twitter:title', content: title }], ['meta', { name: 'twitter:description', content: pageData.description || DESCRIPTION }], + ['meta', { name: 'twitter:image', content: imageUrl }], ['link', { rel: 'canonical', href: canonicalUrl }], ); diff --git a/docs/blog.md b/docs/blog.md index 1ff4f3b..c385c61 100644 --- a/docs/blog.md +++ b/docs/blog.md @@ -11,7 +11,7 @@ import BlogAuthor from './components/BlogAuthor.vue';

Blog posts

-