mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Add alpha option to Conversion API
This commit is contained in:
+3
-1
@@ -24,7 +24,7 @@
|
|||||||
chunked: true,
|
chunked: true,
|
||||||
chunkSize: 2**20
|
chunkSize: 2**20
|
||||||
});
|
});
|
||||||
const outputFormat = new Mediabunny.Mp3OutputFormat({});
|
const outputFormat = new Mediabunny.WebMOutputFormat({});
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.textContent = 'Cancel';
|
button.textContent = 'Cancel';
|
||||||
@@ -92,6 +92,8 @@
|
|||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
video: () => ({
|
video: () => ({
|
||||||
|
alpha: 'keep',
|
||||||
|
width: 320,
|
||||||
//discard: true,
|
//discard: true,
|
||||||
//discard: true,
|
//discard: true,
|
||||||
//crop: {
|
//crop: {
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ type ConversionVideoOptions = {
|
|||||||
frameRate?: number;
|
frameRate?: number;
|
||||||
codec?: VideoCodec;
|
codec?: VideoCodec;
|
||||||
bitrate?: number | Quality;
|
bitrate?: number | Quality;
|
||||||
|
alpha?: 'discard' | 'keep'; // Defaults to 'discard'
|
||||||
forceTranscode?: boolean;
|
forceTranscode?: boolean;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|||||||
+20
-1
@@ -153,6 +153,12 @@ export type ConversionVideoOptions = {
|
|||||||
codec?: VideoCodec;
|
codec?: VideoCodec;
|
||||||
/** The desired bitrate of the output video. */
|
/** The desired bitrate of the output video. */
|
||||||
bitrate?: number | Quality;
|
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. */
|
/** When `true`, video will always be re-encoded instead of directly copying over the encoded samples. */
|
||||||
forceTranscode?: boolean;
|
forceTranscode?: boolean;
|
||||||
};
|
};
|
||||||
@@ -212,7 +218,7 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
|
|||||||
throw new TypeError('options.video.height, when provided, must be a positive integer.');
|
throw new TypeError('options.video.height, when provided, must be a positive integer.');
|
||||||
}
|
}
|
||||||
if (videoOptions?.fit !== undefined && !['fill', 'contain', 'cover'].includes(videoOptions.fit)) {
|
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 (
|
if (
|
||||||
videoOptions?.width !== undefined
|
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.');
|
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) => {
|
const validateAudioOptions = (audioOptions: ConversionAudioOptions | undefined) => {
|
||||||
@@ -672,6 +681,8 @@ export class Conversion {
|
|||||||
|| (totalRotation !== 0 && !outputSupportsRotation)
|
|| (totalRotation !== 0 && !outputSupportsRotation)
|
||||||
|| !!crop;
|
|| !!crop;
|
||||||
|
|
||||||
|
const alpha = trackOptions.alpha ?? 'discard';
|
||||||
|
|
||||||
let videoCodecs = this.output.format.getSupportedVideoCodecs();
|
let videoCodecs = this.output.format.getSupportedVideoCodecs();
|
||||||
if (
|
if (
|
||||||
!needsTranscode
|
!needsTranscode
|
||||||
@@ -704,6 +715,12 @@ export class Conversion {
|
|||||||
return;
|
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);
|
await source.add(packet, meta);
|
||||||
this._reportProgress(track.id, packet.timestamp + packet.duration);
|
this._reportProgress(track.id, packet.timestamp + packet.duration);
|
||||||
}
|
}
|
||||||
@@ -742,6 +759,7 @@ export class Conversion {
|
|||||||
codec: encodableCodec,
|
codec: encodableCodec,
|
||||||
bitrate,
|
bitrate,
|
||||||
sizeChangeBehavior: trackOptions.fit ?? 'passThrough',
|
sizeChangeBehavior: trackOptions.fit ?? 'passThrough',
|
||||||
|
alpha,
|
||||||
onEncodedPacket: sample => this._reportProgress(track.id, sample.timestamp + sample.duration),
|
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
|
rotation: totalRotation, // Bake the rotation into the output
|
||||||
crop: trackOptions.crop,
|
crop: trackOptions.crop,
|
||||||
poolSize: 1,
|
poolSize: 1,
|
||||||
|
alpha: alpha === 'keep',
|
||||||
});
|
});
|
||||||
const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
|
const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
|
||||||
const frameRate = trackOptions.frameRate;
|
const frameRate = trackOptions.frameRate;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { BufferTarget } from '../../src/target.js';
|
|||||||
import { CanvasSource, VideoSampleSource } from '../../src/media-source.js';
|
import { CanvasSource, VideoSampleSource } from '../../src/media-source.js';
|
||||||
import { canEncodeVideo, QUALITY_HIGH } from '../../src/encode.js';
|
import { canEncodeVideo, QUALITY_HIGH } from '../../src/encode.js';
|
||||||
import { VideoSample } from '../../src/sample.js';
|
import { VideoSample } from '../../src/sample.js';
|
||||||
|
import { Conversion } from '../../src/conversion.js';
|
||||||
|
|
||||||
test('Can decode transparent video', async () => {
|
test('Can decode transparent video', async () => {
|
||||||
using input = new Input({
|
using input = new Input({
|
||||||
@@ -269,3 +270,104 @@ test('Positive encodability check with alpha', async () => {
|
|||||||
const result = await canEncodeVideo('vp9', { alpha: 'keep' });
|
const result = await canEncodeVideo('vp9', { alpha: 'keep' });
|
||||||
expect(result).toBe(true);
|
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