Add toAvFrame, NodeAv* -> Av*, add server README, fix track synchronizer in conversion, increase default cache size for ReadableStreamSource

This commit is contained in:
Vanilagy
2026-05-13 15:26:13 +02:00
parent d2d485196f
commit 292189e3d4
15 changed files with 4597 additions and 88 deletions
+83 -4
View File
@@ -24,8 +24,10 @@ import { Output } from '../../src/output.js';
import { Mp4OutputFormat } from '../../src/output-format.js';
import { BufferTarget } from '../../src/target.js';
import { Conversion } from '../../src/conversion.js';
import { NodeAvFrameVideoSampleResource } from '../../packages/server/src/video-sample.js';
import { NodeAvFrameAudioSampleResource } from '../../packages/server/src/audio-sample.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';
import * as NodeAv from 'node-av';
beforeAll(() => {
registerMediabunnyServer();
@@ -687,7 +689,7 @@ describe('Video', async () => {
using sample = await sink.getSample(0);
assert(sample);
expect(sample._data).toBeInstanceOf(NodeAvFrameVideoSampleResource);
expect(sample._data).toBeInstanceOf(AvFrameVideoSampleResource);
expect(sample.format).toBe('I420');
expect(sample.codedWidth).toBe(1920);
expect(sample.codedHeight).toBe(1080);
@@ -822,6 +824,47 @@ describe('Video', async () => {
]);
});
test('toAvFrame with RGBA VideoSample', async () => {
const width = 16;
const height = 8;
const data = new Uint8Array(width * height * 4);
for (let i = 0; i < width * height; i++) {
data[i * 4] = 0x12;
data[i * 4 + 1] = 0x34;
data[i * 4 + 2] = 0x56;
data[i * 4 + 3] = 0xff;
}
using sample = new VideoSample(data, {
format: 'RGBA',
codedWidth: width,
codedHeight: height,
timestamp: 0.5,
duration: 1 / 30,
});
using frame = new NodeAv.Frame();
frame.alloc();
await toAvFrame(sample, frame);
expect(frame.getMediaType()).toBe(NodeAv.AVMEDIA_TYPE_VIDEO);
expect(frame.format).toBe(NodeAv.AV_PIX_FMT_RGBA);
expect(frame.width).toBe(width);
expect(frame.height).toBe(height);
expect(Number(frame.pts)).toBe(Math.round(0.5 * 1e6));
expect(Number(frame.duration)).toBe(Math.round((1 / 30) * 1e6));
expect(frame.timeBase.num).toBe(1);
expect(frame.timeBase.den).toBe(1e6);
assert(frame.data);
const plane = toUint8Array(frame.data[0]!);
expect(plane[0]).toBe(0x12);
expect(plane[1]).toBe(0x34);
expect(plane[2]).toBe(0x56);
expect(plane[3]).toBe(0xff);
});
describe('VideoSample transformation', () => {
// 400x400 image: red everywhere, with a 200x200 blue square filling the bottom-left quadrant.
const TEST_IMAGE = (() => {
@@ -1558,7 +1601,7 @@ describe('Audio', async () => {
using sample = await sink.getSample(await audioTrack.getFirstTimestamp());
assert(sample);
expect(sample._data).toBeInstanceOf(NodeAvFrameAudioSampleResource);
expect(sample._data).toBeInstanceOf(AvFrameAudioSampleResource);
expect(sample.format).toBe('f32-planar');
expect(sample.numberOfChannels).toBe(6);
expect(sample.sampleRate).toBe(48000);
@@ -1579,4 +1622,40 @@ describe('Audio', async () => {
sample.copyTo(buf, { planeIndex: 5 });
sample.copyTo(buf, { format: 'f32', planeIndex: 0 });
});
test('toAvFrame with f32 AudioSample', async () => {
const sampleRate = 48000;
const numberOfChannels = 2;
const numberOfFrames = 1024;
const data = createF32SineWave(sampleRate, numberOfChannels, numberOfFrames / sampleRate);
using sample = new AudioSample({
data,
format: 'f32',
timestamp: 0.25,
numberOfChannels,
sampleRate,
});
using frame = new NodeAv.Frame();
frame.alloc();
await toAvFrame(sample, frame);
expect(frame.getMediaType()).toBe(NodeAv.AVMEDIA_TYPE_AUDIO);
expect(frame.format).toBe(NodeAv.AV_SAMPLE_FMT_FLT);
expect(frame.sampleRate).toBe(sampleRate);
expect(frame.channels).toBe(numberOfChannels);
expect(frame.nbSamples).toBe(numberOfFrames);
expect(Number(frame.pts)).toBe(Math.round(0.25 * sampleRate));
expect(Number(frame.duration)).toBe(numberOfFrames);
expect(frame.timeBase.num).toBe(1);
expect(frame.timeBase.den).toBe(sampleRate);
assert(frame.data);
const u8 = toUint8Array(frame.data[0]!);
const plane = new Float32Array(u8.buffer, u8.byteOffset, numberOfFrames * numberOfChannels);
expect(plane[0]).toBe(data[0]);
expect(plane[1]).toBe(data[1]);
});
});