mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Add "force" transform option, formalize that transformations bake in rotation
This commit is contained in:
@@ -112,6 +112,11 @@ export type VideoEncodingConfig = {
|
|||||||
process?: (sample: VideoSample) => MaybePromise<
|
process?: (sample: VideoSample) => MaybePromise<
|
||||||
CanvasImageSource | VideoSample | (CanvasImageSource | VideoSample)[] | null
|
CanvasImageSource | VideoSample | (CanvasImageSource | VideoSample)[] | null
|
||||||
>;
|
>;
|
||||||
|
/**
|
||||||
|
* Forces every video frame through the transformation step even if no transformation properties are defined.
|
||||||
|
* This can be used, for example, to bake rotation into the encoded video frames.
|
||||||
|
*/
|
||||||
|
force?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */
|
/** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */
|
||||||
@@ -202,6 +207,9 @@ export const validateVideoEncodingConfig = (config: VideoEncodingConfig) => {
|
|||||||
) {
|
) {
|
||||||
throw new TypeError('config.transform.frameRate, when provided, must be a finite positive number.');
|
throw new TypeError('config.transform.frameRate, when provided, must be a finite positive number.');
|
||||||
}
|
}
|
||||||
|
if (config.transform.force !== undefined && typeof config.transform.force !== 'boolean') {
|
||||||
|
throw new TypeError('config.transform.force, when provided, must be a boolean.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
|
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
|
||||||
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
|
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
|
||||||
|
|||||||
+2
-1
@@ -309,7 +309,8 @@ class VideoEncoderWrapper {
|
|||||||
const hasTransformConfig = config.transform?.width !== undefined
|
const hasTransformConfig = config.transform?.width !== undefined
|
||||||
|| config.transform?.height !== undefined
|
|| config.transform?.height !== undefined
|
||||||
|| config.transform?.rotate !== undefined
|
|| config.transform?.rotate !== undefined
|
||||||
|| config.transform?.crop !== undefined;
|
|| config.transform?.crop !== undefined
|
||||||
|
|| config.transform?.force === true;
|
||||||
const needsTransform = hasTransformConfig || (isSizeChange && sizeChangeBehavior !== 'passThrough');
|
const needsTransform = hasTransformConfig || (isSizeChange && sizeChangeBehavior !== 'passThrough');
|
||||||
|
|
||||||
if (needsTransform) {
|
if (needsTransform) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Input } from '../../src/input.js';
|
|||||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||||
import { BufferSource } from '../../src/source.js';
|
import { BufferSource } from '../../src/source.js';
|
||||||
import { VideoSampleSink } from '../../src/media-sink.js';
|
import { VideoSampleSink } from '../../src/media-sink.js';
|
||||||
import { assert } from '../../src/misc.js';
|
import { assert, Rotation } from '../../src/misc.js';
|
||||||
import { InputVideoTrack } from '../../src/input-track.js';
|
import { InputVideoTrack } from '../../src/input-track.js';
|
||||||
|
|
||||||
test('VideoSampleSource.close() should be idempotent after finalize()', async () => {
|
test('VideoSampleSource.close() should be idempotent after finalize()', async () => {
|
||||||
@@ -169,6 +169,36 @@ test('Changing dimensions with passThrough, width and height set', async () => {
|
|||||||
expect(samples[1]).toMatchObject({ codedWidth: 50, codedHeight: 80 });
|
expect(samples[1]).toMatchObject({ codedWidth: 50, codedHeight: 80 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Encoding rotated video frames', async () => {
|
||||||
|
const buffer = await encodeFrames(
|
||||||
|
{ codec: 'vp8', bitrate: QUALITY_MEDIUM },
|
||||||
|
[{ width: 200, height: 100, rotation: 90 }, { width: 200, height: 100, rotation: 90 }],
|
||||||
|
);
|
||||||
|
|
||||||
|
const samples = await readBackSamples(buffer);
|
||||||
|
|
||||||
|
// They were encoded with no regard for the rotation metadata
|
||||||
|
for (const sample of samples) {
|
||||||
|
expect(sample.codedWidth).toBe(200);
|
||||||
|
expect(sample.codedHeight).toBe(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Encoding rotated video frames with forced transform', async () => {
|
||||||
|
const buffer = await encodeFrames(
|
||||||
|
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { force: true } },
|
||||||
|
[{ width: 200, height: 100, rotation: 90 }, { width: 200, height: 100, rotation: 90 }],
|
||||||
|
);
|
||||||
|
|
||||||
|
const samples = await readBackSamples(buffer);
|
||||||
|
|
||||||
|
// The rotation has been baked into the samples
|
||||||
|
for (const sample of samples) {
|
||||||
|
expect(sample.codedWidth).toBe(100);
|
||||||
|
expect(sample.codedHeight).toBe(200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('transform.process identity function', async () => {
|
test('transform.process identity function', async () => {
|
||||||
const buffer = await encodeFrames(
|
const buffer = await encodeFrames(
|
||||||
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { process: sample => sample } },
|
{ codec: 'vp8', bitrate: QUALITY_MEDIUM, transform: { process: sample => sample } },
|
||||||
@@ -460,7 +490,13 @@ const makeCanvas = (width: number, height: number) => {
|
|||||||
|
|
||||||
const encodeFrames = async (
|
const encodeFrames = async (
|
||||||
encodingConfig: ConstructorParameters<typeof VideoSampleSource>[0],
|
encodingConfig: ConstructorParameters<typeof VideoSampleSource>[0],
|
||||||
frames: { width: number; height: number; timestamp?: number; duration?: number }[],
|
frames: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
timestamp?: number;
|
||||||
|
duration?: number;
|
||||||
|
rotation?: Rotation;
|
||||||
|
}[],
|
||||||
) => {
|
) => {
|
||||||
const output = new Output({
|
const output = new Output({
|
||||||
format: new Mp4OutputFormat(),
|
format: new Mp4OutputFormat(),
|
||||||
@@ -477,6 +513,7 @@ const encodeFrames = async (
|
|||||||
const sample = new VideoSample(canvas, {
|
const sample = new VideoSample(canvas, {
|
||||||
timestamp: f.timestamp ?? i / 30,
|
timestamp: f.timestamp ?? i / 30,
|
||||||
duration: f.duration ?? 1 / 30,
|
duration: f.duration ?? 1 / 30,
|
||||||
|
rotation: f.rotation,
|
||||||
});
|
});
|
||||||
await videoSource.add(sample);
|
await videoSource.add(sample);
|
||||||
sample.close();
|
sample.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user