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 { BufferSource } from '../../src/source.js';
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
import { Output } from '../../src/output.js';
|
||||
import { BufferTarget } from '../../src/target.js';
|
||||
import { canEncode } from '../../src/encode.js';
|
||||
import { canEncode, Quality } from '../../src/encode.js';
|
||||
import { AudioSampleSource } from '../../src/media-source.js';
|
||||
import { EncodedPacketSink } from '../../src/media-sink.js';
|
||||
import { Mp4OutputFormat } from '../../src/output-format.js';
|
||||
@@ -47,7 +47,7 @@ test('AAC encoding', async () => {
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioSampleSource({ codec: 'aac', bitrate: 128000 });
|
||||
const audioSource = new AudioSampleSource({ codec: 'aac', quality: new Quality({ bitrate: 128000 }) });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
@@ -98,7 +98,7 @@ test('AAC with huge timestamps', async () => {
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioSampleSource({ codec: 'aac', bitrate: 128000 });
|
||||
const audioSource = new AudioSampleSource({ codec: 'aac', quality: new Quality({ bitrate: 128000 }) });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MpegTsOutputFormat } from '../../src/output-format.js';
|
||||
import { BufferTarget } from '../../src/target.js';
|
||||
import { Conversion } from '../../src/conversion.js';
|
||||
import { AC3_REGISTRATION_DESCRIPTOR, EAC3_REGISTRATION_DESCRIPTOR } from '../../src/codec-data.js';
|
||||
import { canEncode } from '../../src/encode.js';
|
||||
import { canEncode, Quality } from '../../src/encode.js';
|
||||
import { AudioSampleSink, EncodedPacketSink } from '../../src/media-sink.js';
|
||||
import { AudioSampleSource } from '../../src/media-source.js';
|
||||
import { Mp4OutputFormat } from '../../src/output-format.js';
|
||||
@@ -248,7 +248,7 @@ test('AC-3 encoding', async () => {
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioSampleSource({ codec: 'ac3', bitrate: 192000 });
|
||||
const audioSource = new AudioSampleSource({ codec: 'ac3', quality: new Quality({ bitrate: 192000 }) });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
@@ -298,7 +298,7 @@ test('E-AC-3 encoding', async () => {
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioSampleSource({ codec: 'eac3', bitrate: 192000 });
|
||||
const audioSource = new AudioSampleSource({ codec: 'eac3', quality: new Quality({ bitrate: 192000 }) });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
@@ -350,7 +350,7 @@ test('AC-3 with huge timestamps', async () => {
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioSampleSource({ codec: 'ac3', bitrate: 192000 });
|
||||
const audioSource = new AudioSampleSource({ codec: 'ac3', quality: new Quality({ bitrate: 192000 }) });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
@@ -401,7 +401,7 @@ test('E-AC-3 with huge timestamps', async () => {
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioSampleSource({ codec: 'eac3', bitrate: 192000 });
|
||||
const audioSource = new AudioSampleSource({ codec: 'eac3', quality: new Quality({ bitrate: 192000 }) });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BufferSource } from '../../src/source.js';
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
import { Output } from '../../src/output.js';
|
||||
import { BufferTarget } from '../../src/target.js';
|
||||
import { canEncode } from '../../src/encode.js';
|
||||
import { canEncode, Quality } from '../../src/encode.js';
|
||||
import { AudioSampleSource } from '../../src/media-source.js';
|
||||
import { EncodedPacketSink } from '../../src/media-sink.js';
|
||||
import { Mp3OutputFormat, Mp4OutputFormat } from '../../src/output-format.js';
|
||||
@@ -47,7 +47,7 @@ test('MP3 encoding', async () => {
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioSampleSource({ codec: 'mp3', bitrate: 128_000 });
|
||||
const audioSource = new AudioSampleSource({ codec: 'mp3', quality: new Quality({ bitrate: 128_000 }) });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
@@ -99,7 +99,7 @@ test('MP3 with huge timestamps', async () => {
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioSampleSource({ codec: 'mp3', bitrate: 128_000 });
|
||||
const audioSource = new AudioSampleSource({ codec: 'mp3', quality: new Quality({ bitrate: 128_000 }) });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
|
||||
@@ -32,7 +32,7 @@ import { Mp4OutputFormat } from '../../src/output-format.js';
|
||||
import { BufferTarget } from '../../src/target.js';
|
||||
import { Conversion } from '../../src/conversion.js';
|
||||
import { VideoSampleSource } from '../../src/media-source.js';
|
||||
import { QUALITY_HIGH } from '../../src/encode.js';
|
||||
import { buildQuantizerEncodeOptions, Quality } from '../../src/encode.js';
|
||||
import { AvFrameVideoSampleResource } from '../../packages/server/src/video-sample.js';
|
||||
import { AvFrameAudioSampleResource } from '../../packages/server/src/audio-sample.js';
|
||||
import { toAvFrame } from '../../packages/server/src/index.js';
|
||||
@@ -474,6 +474,34 @@ describe('Video', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
const QUANTIZER_TEST_CODECS = [
|
||||
{ codec: 'avc', name: 'AVC', low: 10, high: 45 },
|
||||
{ codec: 'hevc', name: 'HEVC', low: 10, high: 45 },
|
||||
{ codec: 'vp9', name: 'VP9', low: 10, high: 55 },
|
||||
{ codec: 'av1', name: 'AV1', low: 40, high: 220 },
|
||||
] as const;
|
||||
|
||||
for (const { codec, name, low, high } of QUANTIZER_TEST_CODECS) {
|
||||
test(`${name} quantizer encode`, { timeout: 20_000 }, async () => {
|
||||
const lowQuantizerPackets = await quantizerEncodeTest(codec, () => low);
|
||||
const highQuantizerPackets = await quantizerEncodeTest(codec, () => high);
|
||||
|
||||
// A lower quantizer means higher quality, which shows in the encoded size
|
||||
expect(lowQuantizerPackets.reduce((sum, packet) => sum + packet.data.byteLength, 0))
|
||||
.toBeGreaterThan(highQuantizerPackets.reduce((sum, packet) => sum + packet.data.byteLength, 0));
|
||||
});
|
||||
|
||||
test(`${name} mid-stream quantizer change`, { timeout: 20_000 }, async () => {
|
||||
const packets = await quantizerEncodeTest(codec, i => i < 5 ? low : high);
|
||||
|
||||
// Changing the quantizer forces a fresh encoder stream, which begins with a new key frame
|
||||
expect(packets[5]!.type).toBe('key');
|
||||
|
||||
expect(packets.slice(0, 5).reduce((sum, packet) => sum + packet.data.byteLength, 0))
|
||||
.toBeGreaterThan(packets.slice(5).reduce((sum, packet) => sum + packet.data.byteLength, 0));
|
||||
});
|
||||
}
|
||||
|
||||
test('ProRes encode', async () => {
|
||||
// ProRes can't be decoded by node-av's wrapper here, so this is encode-only.
|
||||
const encoder = new NodeAvVideoEncoder();
|
||||
@@ -598,7 +626,7 @@ describe('Video', async () => {
|
||||
|
||||
const source = new VideoSampleSource({
|
||||
codec: 'prores',
|
||||
bitrate: QUALITY_HIGH,
|
||||
quality: new Quality({ quality: 0.75, preferBitrate: true }),
|
||||
alpha: 'keep',
|
||||
});
|
||||
output.addVideoTrack(source);
|
||||
@@ -781,6 +809,59 @@ describe('Video', async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const quantizerEncodeTest = async (codec: VideoCodec, getQuantizer: (frameIndex: number) => number) => {
|
||||
const width = 640;
|
||||
const height = 360;
|
||||
|
||||
const encoder = new NodeAvVideoEncoder();
|
||||
// @ts-expect-error Readonly
|
||||
encoder.codec = codec;
|
||||
// @ts-expect-error Readonly
|
||||
encoder.config = {
|
||||
codec: buildVideoCodecString(codec, width, height, 1e6, false),
|
||||
width,
|
||||
height,
|
||||
bitrateMode: 'quantizer',
|
||||
} satisfies VideoEncoderConfig;
|
||||
|
||||
const packets: EncodedPacket[] = [];
|
||||
|
||||
// @ts-expect-error Readonly
|
||||
encoder.onPacket = (packet: EncodedPacket) => {
|
||||
packets.push(packet);
|
||||
};
|
||||
|
||||
await encoder.init();
|
||||
|
||||
// Noise, so that quality differences clearly show in the encoded size
|
||||
const data = new Uint8Array(width * height * 4);
|
||||
let seed = 123456789;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
seed = (seed * 48271) % 2147483647;
|
||||
data[i] = seed & 0xff;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
using sample = new VideoSample(data, {
|
||||
format: 'RGBX',
|
||||
codedWidth: width,
|
||||
codedHeight: height,
|
||||
timestamp: i / 30,
|
||||
duration: 1 / 30,
|
||||
});
|
||||
|
||||
await encoder.encode(sample, buildQuantizerEncodeOptions(codec, getQuantizer(i)));
|
||||
}
|
||||
|
||||
await encoder.flush();
|
||||
await encoder.close();
|
||||
|
||||
expect(packets).toHaveLength(10);
|
||||
expect(packets[0]!.type).toBe('key');
|
||||
|
||||
return packets;
|
||||
};
|
||||
|
||||
test('AVC conversion roundtrip', { timeout: 20_000 }, async () => {
|
||||
await conversionRoundtrip('avc');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user