mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
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:
@@ -4,7 +4,7 @@ import { Mp4OutputFormat, WebMOutputFormat } from '../../src/output-format.js';
|
||||
import { BufferTarget } from '../../src/target.js';
|
||||
import { AudioSampleSource, VideoSampleSource } from '../../src/media-source.js';
|
||||
import { AudioSample, VideoSample } from '../../src/sample.js';
|
||||
import { QUALITY_MEDIUM } from '../../src/encode.js';
|
||||
import { Quality } from '../../src/encode.js';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
import { BufferSource } from '../../src/source.js';
|
||||
@@ -14,7 +14,7 @@ import { InputAudioTrack, InputVideoTrack } from '../../src/input-track.js';
|
||||
|
||||
test('VideoSampleSource, normal usage', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM },
|
||||
{ codec: 'vp8', quality: new Quality('medium') },
|
||||
[{ width: 100, height: 100 }, { width: 100, height: 100 }, { width: 100, height: 100 }],
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ test('VideoSampleSource, .close() should be idempotent after finalize()', async
|
||||
|
||||
const videoSource = new VideoSampleSource({
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
});
|
||||
|
||||
output.addVideoTrack(videoSource);
|
||||
@@ -63,7 +63,7 @@ test('VideoSampleSource, changing input dimensions throws with deny (default)',
|
||||
|
||||
const videoSource = new VideoSampleSource({
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
});
|
||||
|
||||
output.addVideoTrack(videoSource);
|
||||
@@ -82,7 +82,7 @@ test('VideoSampleSource, changing input dimensions throws with deny (default)',
|
||||
|
||||
test('VideoSampleSource, changing input dimensions with passThrough preserves per-frame dimensions', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, sizeChangeBehavior: 'passThrough' },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), sizeChangeBehavior: 'passThrough' },
|
||||
[{ width: 100, height: 100 }, { width: 200, height: 150 }],
|
||||
);
|
||||
|
||||
@@ -101,7 +101,7 @@ test(
|
||||
async () => {
|
||||
for (const behavior of ['fill', 'contain', 'cover'] as const) {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, sizeChangeBehavior: behavior },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), sizeChangeBehavior: behavior },
|
||||
[{ width: 100, height: 100 }, { width: 200, height: 150 }],
|
||||
);
|
||||
|
||||
@@ -115,7 +115,7 @@ test(
|
||||
|
||||
test('VideoSampleSource, same-sized frames with width and height set', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { width: 50, height: 80, fit: 'fill' } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { width: 50, height: 80, fit: 'fill' } },
|
||||
[{ width: 100, height: 100 }, { width: 100, height: 100 }],
|
||||
);
|
||||
|
||||
@@ -127,7 +127,7 @@ test('VideoSampleSource, same-sized frames with width and height set', async ()
|
||||
|
||||
test('VideoSampleSource, same-sized frames with rotation set to 90', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { rotate: 90 } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { rotate: 90 } },
|
||||
[{ width: 200, height: 100 }, { width: 200, height: 100 }],
|
||||
);
|
||||
|
||||
@@ -139,7 +139,11 @@ test('VideoSampleSource, same-sized frames with rotation set to 90', async () =>
|
||||
|
||||
test('VideoSampleSource, same-sized frames with rotation, width and height', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { rotate: 90, width: 50, height: 80, fit: 'contain' } },
|
||||
{
|
||||
codec: 'vp8',
|
||||
quality: new Quality('medium'),
|
||||
transform: { rotate: 90, width: 50, height: 80, fit: 'contain' },
|
||||
},
|
||||
[{ width: 200, height: 100 }, { width: 200, height: 100 }],
|
||||
);
|
||||
|
||||
@@ -151,7 +155,7 @@ test('VideoSampleSource, same-sized frames with rotation, width and height', asy
|
||||
|
||||
test('VideoSampleSource, changing dimensions with passThrough and rotation 90', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, sizeChangeBehavior: 'passThrough', transform: { rotate: 90 } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), sizeChangeBehavior: 'passThrough', transform: { rotate: 90 } },
|
||||
[{ width: 200, height: 100 }, { width: 300, height: 150 }],
|
||||
);
|
||||
|
||||
@@ -169,7 +173,7 @@ test('VideoSampleSource, changing dimensions with passThrough, width and height
|
||||
const buffer = await encodeFrames(
|
||||
{
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
sizeChangeBehavior: 'passThrough',
|
||||
transform: {
|
||||
width: 50,
|
||||
@@ -189,7 +193,7 @@ test('VideoSampleSource, changing dimensions with passThrough, width and height
|
||||
|
||||
test('VideoSampleSource, encoding rotated video frames', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM },
|
||||
{ codec: 'vp8', quality: new Quality('medium') },
|
||||
[{ width: 200, height: 100, rotation: 90 }, { width: 200, height: 100, rotation: 90 }],
|
||||
);
|
||||
|
||||
@@ -204,7 +208,7 @@ test('VideoSampleSource, encoding rotated video frames', async () => {
|
||||
|
||||
test('VideoSampleSource, encoding rotated video frames with forced transform', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { force: true } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { force: true } },
|
||||
[{ width: 200, height: 100, rotation: 90 }, { width: 200, height: 100, rotation: 90 }],
|
||||
);
|
||||
|
||||
@@ -219,7 +223,7 @@ test('VideoSampleSource, encoding rotated video frames with forced transform', a
|
||||
|
||||
test('VideoSampleSource, transform.process identity function', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { process: sample => sample } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { process: sample => sample } },
|
||||
[{ width: 100, height: 100 }, { width: 100, height: 100 }, { width: 100, height: 100 }],
|
||||
);
|
||||
|
||||
@@ -235,7 +239,7 @@ test('VideoSampleSource, transform.process manual resize', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
transform: {
|
||||
process: (sample) => {
|
||||
const canvas = new OffscreenCanvas(60, 40);
|
||||
@@ -262,7 +266,7 @@ test('VideoSampleSource, transform.process receives pre-transformed frames', asy
|
||||
const buffer = await encodeFrames(
|
||||
{
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
transform: {
|
||||
width: 50,
|
||||
height: 80,
|
||||
@@ -297,7 +301,7 @@ test('VideoSampleSource, transform.process drops all frames after the first', as
|
||||
const buffer = await encodeFrames(
|
||||
{
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
transform: {
|
||||
process: (sample) => {
|
||||
if (frameIndex++ > 0) {
|
||||
@@ -318,7 +322,7 @@ test('VideoSampleSource, transform.process expands every frame into two', async
|
||||
const buffer = await encodeFrames(
|
||||
{
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
transform: {
|
||||
process: (sample) => {
|
||||
const t = sample.timestamp;
|
||||
@@ -348,7 +352,7 @@ test('VideoSampleSource, transform.process expands every frame into two', async
|
||||
|
||||
test('VideoSampleSource, transform.frameRate normalizes variable-rate input to fixed rate', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { frameRate: 10 } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { frameRate: 10 } },
|
||||
[
|
||||
{ width: 100, height: 100, timestamp: 0, duration: 0.15 },
|
||||
{ width: 100, height: 100, timestamp: 0.15, duration: 0.1 },
|
||||
@@ -366,7 +370,7 @@ test('VideoSampleSource, transform.frameRate normalizes variable-rate input to f
|
||||
|
||||
test('VideoSampleSource, transform.frameRate pads gaps by repeating last frame', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { frameRate: 10 } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { frameRate: 10 } },
|
||||
[
|
||||
{ width: 100, height: 100, timestamp: 0, duration: 0.1 },
|
||||
{ width: 100, height: 100, timestamp: 0.3, duration: 0.1 },
|
||||
@@ -383,7 +387,7 @@ test('VideoSampleSource, transform.frameRate pads gaps by repeating last frame',
|
||||
|
||||
test('VideoSampleSource, transform.frameRate deduplicates frames in the same slot', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { frameRate: 10 } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { frameRate: 10 } },
|
||||
[
|
||||
{ width: 100, height: 100, timestamp: 0, duration: 0.03 },
|
||||
{ width: 100, height: 100, timestamp: 0.03, duration: 0.03 },
|
||||
@@ -400,7 +404,7 @@ test('VideoSampleSource, transform.frameRate deduplicates frames in the same slo
|
||||
|
||||
test('VideoSampleSource, transform.frameRate final padding fills remaining duration', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { frameRate: 10 } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { frameRate: 10 } },
|
||||
[
|
||||
{ width: 100, height: 100, timestamp: 0, duration: 0.5 },
|
||||
],
|
||||
@@ -415,7 +419,7 @@ test('VideoSampleSource, transform.frameRate final padding fills remaining durat
|
||||
|
||||
test('VideoSampleSource, transform.frameRate skipping and padding combined', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { frameRate: 10 } },
|
||||
{ codec: 'vp8', quality: new Quality('medium'), transform: { frameRate: 10 } },
|
||||
[
|
||||
{ width: 100, height: 100, timestamp: 0, duration: 0.02 },
|
||||
{ width: 100, height: 100, timestamp: 0.02, duration: 0.02 },
|
||||
@@ -437,7 +441,7 @@ test('VideoSampleSource, transform.frameRate works with transform', async () =>
|
||||
const buffer = await encodeFrames(
|
||||
{
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
transform: { width: 50, height: 50, fit: 'fill', frameRate: 10 },
|
||||
},
|
||||
[
|
||||
@@ -461,7 +465,7 @@ test('VideoSampleSource, transform.frameRate works with process', async () => {
|
||||
const buffer = await encodeFrames(
|
||||
{
|
||||
codec: 'vp8',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
quality: new Quality('medium'),
|
||||
transform: {
|
||||
frameRate: 10,
|
||||
process: (sample) => {
|
||||
|
||||
Reference in New Issue
Block a user