mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Add alpha option to Conversion API
This commit is contained in:
+3
-1
@@ -24,7 +24,7 @@
|
||||
chunked: true,
|
||||
chunkSize: 2**20
|
||||
});
|
||||
const outputFormat = new Mediabunny.Mp3OutputFormat({});
|
||||
const outputFormat = new Mediabunny.WebMOutputFormat({});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Cancel';
|
||||
@@ -92,6 +92,8 @@
|
||||
},
|
||||
*/
|
||||
video: () => ({
|
||||
alpha: 'keep',
|
||||
width: 320,
|
||||
//discard: true,
|
||||
//discard: true,
|
||||
//crop: {
|
||||
|
||||
@@ -114,6 +114,7 @@ type ConversionVideoOptions = {
|
||||
frameRate?: number;
|
||||
codec?: VideoCodec;
|
||||
bitrate?: number | Quality;
|
||||
alpha?: 'discard' | 'keep'; // Defaults to 'discard'
|
||||
forceTranscode?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
+20
-1
@@ -153,6 +153,12 @@ export type ConversionVideoOptions = {
|
||||
codec?: VideoCodec;
|
||||
/** The desired bitrate of the output video. */
|
||||
bitrate?: number | Quality;
|
||||
/**
|
||||
* Whether to discard or keep the transparency information of the input video. The default is `'discard'`. Note that
|
||||
* for `'keep'` to produce a transparent video, you must use an output config that supports it, such as WebM with
|
||||
* VP9.
|
||||
*/
|
||||
alpha?: 'discard' | 'keep';
|
||||
/** When `true`, video will always be re-encoded instead of directly copying over the encoded samples. */
|
||||
forceTranscode?: boolean;
|
||||
};
|
||||
@@ -212,7 +218,7 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
|
||||
throw new TypeError('options.video.height, when provided, must be a positive integer.');
|
||||
}
|
||||
if (videoOptions?.fit !== undefined && !['fill', 'contain', 'cover'].includes(videoOptions.fit)) {
|
||||
throw new TypeError('options.video.fit, when provided, must be one of "fill", "contain", or "cover".');
|
||||
throw new TypeError('options.video.fit, when provided, must be one of \'fill\', \'contain\', or \'cover\'.');
|
||||
}
|
||||
if (
|
||||
videoOptions?.width !== undefined
|
||||
@@ -236,6 +242,9 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
|
||||
) {
|
||||
throw new TypeError('options.video.frameRate, when provided, must be a finite positive number.');
|
||||
}
|
||||
if (videoOptions?.alpha !== undefined && !['discard', 'keep'].includes(videoOptions.alpha)) {
|
||||
throw new TypeError('options.video.alpha, when provided, must be either \'discard\' or \'keep\'.');
|
||||
}
|
||||
};
|
||||
|
||||
const validateAudioOptions = (audioOptions: ConversionAudioOptions | undefined) => {
|
||||
@@ -672,6 +681,8 @@ export class Conversion {
|
||||
|| (totalRotation !== 0 && !outputSupportsRotation)
|
||||
|| !!crop;
|
||||
|
||||
const alpha = trackOptions.alpha ?? 'discard';
|
||||
|
||||
let videoCodecs = this.output.format.getSupportedVideoCodecs();
|
||||
if (
|
||||
!needsTranscode
|
||||
@@ -704,6 +715,12 @@ export class Conversion {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alpha === 'discard') {
|
||||
// Feels hacky given that the rest of the packet is readonly. But, works for now.
|
||||
delete packet.sideData.alpha;
|
||||
delete packet.sideData.alphaByteLength;
|
||||
}
|
||||
|
||||
await source.add(packet, meta);
|
||||
this._reportProgress(track.id, packet.timestamp + packet.duration);
|
||||
}
|
||||
@@ -742,6 +759,7 @@ export class Conversion {
|
||||
codec: encodableCodec,
|
||||
bitrate,
|
||||
sizeChangeBehavior: trackOptions.fit ?? 'passThrough',
|
||||
alpha,
|
||||
onEncodedPacket: sample => this._reportProgress(track.id, sample.timestamp + sample.duration),
|
||||
};
|
||||
|
||||
@@ -796,6 +814,7 @@ export class Conversion {
|
||||
rotation: totalRotation, // Bake the rotation into the output
|
||||
crop: trackOptions.crop,
|
||||
poolSize: 1,
|
||||
alpha: alpha === 'keep',
|
||||
});
|
||||
const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
|
||||
const frameRate = trackOptions.frameRate;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BufferTarget } from '../../src/target.js';
|
||||
import { CanvasSource, VideoSampleSource } from '../../src/media-source.js';
|
||||
import { canEncodeVideo, QUALITY_HIGH } from '../../src/encode.js';
|
||||
import { VideoSample } from '../../src/sample.js';
|
||||
import { Conversion } from '../../src/conversion.js';
|
||||
|
||||
test('Can decode transparent video', async () => {
|
||||
using input = new Input({
|
||||
@@ -269,3 +270,104 @@ test('Positive encodability check with alpha', async () => {
|
||||
const result = await canEncodeVideo('vp9', { alpha: 'keep' });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('Can transmux transparent video, discards alpha by default', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
const output = new Output({
|
||||
format: new WebMOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
});
|
||||
await conversion.execute();
|
||||
|
||||
using outputInput = new Input({
|
||||
source: new BufferSource(output.target.buffer!),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await outputInput.getPrimaryVideoTrack())!;
|
||||
expect(await videoTrack.canBeTransparent()).toBe(false);
|
||||
|
||||
const sink = new VideoSampleSink(videoTrack);
|
||||
const sample = (await sink.getSample(await videoTrack.getFirstTimestamp()))!;
|
||||
expect(sample.hasAlpha).toBe(false);
|
||||
});
|
||||
|
||||
test('Can transmux transparent video, can keep alpha', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
const output = new Output({
|
||||
format: new WebMOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: {
|
||||
alpha: 'keep',
|
||||
},
|
||||
});
|
||||
await conversion.execute();
|
||||
|
||||
using outputInput = new Input({
|
||||
source: new BufferSource(output.target.buffer!),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await outputInput.getPrimaryVideoTrack())!;
|
||||
expect(await videoTrack.canBeTransparent()).toBe(true);
|
||||
|
||||
const sink = new VideoSampleSink(videoTrack);
|
||||
const sample = (await sink.getSample(await videoTrack.getFirstTimestamp()))!;
|
||||
expect(sample.format).toContain('A');
|
||||
expect(sample.hasAlpha).toBe(true);
|
||||
});
|
||||
|
||||
test('Can reencode transparent video, keeping alpha', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/transparency.webm'),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
const output = new Output({
|
||||
format: new WebMOutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: {
|
||||
width: 320,
|
||||
alpha: 'keep',
|
||||
},
|
||||
trim: {
|
||||
start: 0,
|
||||
end: 0.5,
|
||||
},
|
||||
});
|
||||
await conversion.execute();
|
||||
|
||||
using outputInput = new Input({
|
||||
source: new BufferSource(output.target.buffer!),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await outputInput.getPrimaryVideoTrack())!;
|
||||
expect(await videoTrack.canBeTransparent()).toBe(true);
|
||||
expect(videoTrack.displayWidth).toBe(320);
|
||||
|
||||
const sink = new VideoSampleSink(videoTrack);
|
||||
const sample = (await sink.getSample(await videoTrack.getFirstTimestamp()))!;
|
||||
expect(sample.format).toContain('A');
|
||||
expect(sample.hasAlpha).toBe(true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user