Make Conversion API copy input pairability graph by default, fix HLS muxer rotation bug, remove onfinalized callback

This commit is contained in:
Vanilagy
2026-04-14 21:14:58 +02:00
parent 53f05afbd8
commit 6410ea9f42
4 changed files with 317 additions and 29 deletions
+263 -4
View File
@@ -1,13 +1,15 @@
import { ALL_FORMATS } from '../../src/input-format.js';
import { Input } from '../../src/input.js';
import { AdtsOutputFormat, Mp4OutputFormat } from '../../src/output-format.js';
import { Output } from '../../src/output.js';
import { BufferSource, UrlSource } from '../../src/source.js';
import { AdtsOutputFormat, HlsOutputFormat, Mp4OutputFormat, MpegTsOutputFormat } from '../../src/output-format.js';
import { Output, OutputTrackGroup } from '../../src/output.js';
import { BufferSource, PathedSource, UrlSource } from '../../src/source.js';
import { expect, test } from 'vitest';
import { BufferTarget } from '../../src/target.js';
import { BufferTarget, PathedTarget } from '../../src/target.js';
import { Conversion } from '../../src/conversion.js';
import { assert } from '../../src/misc.js';
import { InputVideoTrack } from '../../src/input-track.js';
import { AudioBufferSource, CanvasSource } from '../../src/media-source.js';
import { QUALITY_HIGH } from '../../src/encode.js';
test('Rotation is baked-in when rerendering', async () => {
using input = new Input({
@@ -100,3 +102,260 @@ test('Fan-out', async () => {
expect(await tracks[0]!.getDisplayHeight()).toBe(480);
expect(await tracks[1]!.getDisplayHeight()).toBe(360);
});
const createSineWave = (sampleRate: number, channels: number, durationSeconds: number) => {
const buffer = new AudioBuffer({
sampleRate,
numberOfChannels: channels,
length: sampleRate * durationSeconds,
});
for (let ch = 0; ch < channels; ch++) {
const data = buffer.getChannelData(ch);
for (let i = 0; i < data.length; i++) {
data[i] = Math.sin(2 * Math.PI * 440 * i / sampleRate);
}
}
return buffer;
};
test('HLS track assignability is kept #1', async () => {
const files = new Map<string, ArrayBuffer>();
const output = new Output({
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
}),
target: new PathedTarget(
'master.m3u8',
({ path }) => {
const target = new BufferTarget();
target.on('finalized', () => {
files.set(path, target.buffer!);
});
return target;
},
),
});
const canvas = new OffscreenCanvas(1280, 720);
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = 'red';
ctx.fillRect(100, 100, 200, 200);
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
output.addVideoTrack(videoSource);
const audioSource = new AudioBufferSource({ codec: 'aac', bitrate: QUALITY_HIGH });
output.addAudioTrack(audioSource);
await output.start();
for (let i = 0; i < 4; i++) {
await videoSource.add(i / 2, 1 / 2);
}
await audioSource.add(createSineWave(48000, 2, 2));
await output.finalize();
const masterPlayist = new TextDecoder().decode(files.get('master.m3u8'));
expect(masterPlayist.match(/\.m3u8/g)?.length).toBe(1);
using input = new Input({
formats: ALL_FORMATS,
source: new PathedSource(
'master.m3u8',
({ path }) => new BufferSource(files.get(path)!),
),
});
const newOutput = new Output({
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
}),
target: new PathedTarget(
'new/master.m3u8',
({ path }) => {
const target = new BufferTarget();
target.on('finalized', () => {
files.set(path, target.buffer!);
});
return target;
},
),
});
const conversion = await Conversion.init({ input, output: newOutput });
await conversion.execute();
const newMasterPlayist = new TextDecoder().decode(files.get('new/master.m3u8'));
expect(newMasterPlayist).toBe(masterPlayist);
});
test('HLS track assignability is kept #2', async () => {
const files = new Map<string, ArrayBuffer>();
const output = new Output({
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
}),
target: new PathedTarget(
'master.m3u8',
({ path }) => {
const target = new BufferTarget();
target.on('finalized', () => {
files.set(path, target.buffer!);
});
return target;
},
),
});
const canvas = new OffscreenCanvas(1280, 720);
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = 'red';
ctx.fillRect(100, 100, 200, 200);
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
output.addVideoTrack(videoSource, { group: a });
const audioSource = new AudioBufferSource({ codec: 'aac', bitrate: QUALITY_HIGH });
output.addAudioTrack(audioSource, { group: b });
await output.start();
for (let i = 0; i < 4; i++) {
await videoSource.add(i / 2, 1 / 2);
}
await audioSource.add(createSineWave(48000, 2, 2));
await output.finalize();
const masterPlayist = new TextDecoder().decode(files.get('master.m3u8'));
expect(masterPlayist.match(/\.m3u8/g)?.length).toBe(2);
using input = new Input({
formats: ALL_FORMATS,
source: new PathedSource(
'master.m3u8',
({ path }) => new BufferSource(files.get(path)!),
),
});
const newOutput = new Output({
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
}),
target: new PathedTarget(
'new/master.m3u8',
({ path }) => {
const target = new BufferTarget();
target.on('finalized', () => {
files.set(path, target.buffer!);
});
return target;
},
),
});
const conversion = await Conversion.init({ input, output: newOutput });
await conversion.execute();
const newMasterPlayist = new TextDecoder().decode(files.get('new/master.m3u8'));
expect(newMasterPlayist).toBe(masterPlayist);
});
test('HLS track assignability can be overridden', async () => {
const files = new Map<string, ArrayBuffer>();
const output = new Output({
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
}),
target: new PathedTarget(
'master.m3u8',
({ path }) => {
const target = new BufferTarget();
target.on('finalized', () => {
files.set(path, target.buffer!);
});
return target;
},
),
});
const canvas = new OffscreenCanvas(1280, 720);
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = 'red';
ctx.fillRect(100, 100, 200, 200);
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
output.addVideoTrack(videoSource, { group: a });
const audioSource = new AudioBufferSource({ codec: 'aac', bitrate: QUALITY_HIGH });
output.addAudioTrack(audioSource, { group: b });
await output.start();
for (let i = 0; i < 4; i++) {
await videoSource.add(i / 2, 1 / 2);
}
await audioSource.add(createSineWave(48000, 2, 2));
await output.finalize();
const masterPlayist = new TextDecoder().decode(files.get('master.m3u8'));
expect(masterPlayist.match(/\.m3u8/g)?.length).toBe(2);
using input = new Input({
formats: ALL_FORMATS,
source: new PathedSource(
'master.m3u8',
({ path }) => new BufferSource(files.get(path)!),
),
});
const newOutput = new Output({
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
}),
target: new PathedTarget(
'new/master.m3u8',
({ path }) => {
const target = new BufferTarget();
target.on('finalized', () => {
files.set(path, target.buffer!);
});
return target;
},
),
});
const conversion = await Conversion.init({
input,
output: newOutput,
video: { group: newOutput.defaultTrackGroup },
audio: { group: newOutput.defaultTrackGroup },
});
await conversion.execute();
const newMasterPlayist = new TextDecoder().decode(files.get('new/master.m3u8'));
expect(newMasterPlayist).not.toBe(masterPlayist);
expect(newMasterPlayist.match(/\.m3u8/g)?.length).toBe(1);
});