mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-06 07:13:47 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7580935639 | ||
|
|
53824536fa | ||
|
|
8f43086e3d | ||
|
|
9343607fe1 | ||
|
|
f877fabc60 | ||
|
|
a2115b1de7 | ||
|
|
8d46d50c39 | ||
|
|
19219fc1ce |
@@ -272,6 +272,10 @@ const output = new Output({
|
||||
});
|
||||
```
|
||||
|
||||
::: info
|
||||
This format ensures [append-only writing](#append-only-writing).
|
||||
:::
|
||||
|
||||
The following options are available:
|
||||
```ts
|
||||
type AdtsOutputFormatOptions = {
|
||||
|
||||
@@ -106,6 +106,7 @@ const sponsors = {
|
||||
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
|
||||
],
|
||||
individual: [
|
||||
{ image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/197597', name: 'Christopher Chedeau', url: 'https://github.com/vjeux' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' },
|
||||
@@ -115,6 +116,7 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/139718', name: 'Anton Kosiakin', url: 'https://github.com/deil' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/56988069', name: 'SyhabouthAlex', url: 'https://github.com/SyhabouthAlex' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/38181164', name: 'wcw', url: 'https://github.com/asd55667' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/1836701', name: 'Bean Deng', url: 'https://github.com/HADB' },
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.27.2",
|
||||
"version": "1.27.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.27.2",
|
||||
"version": "1.27.4",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7739,9 +7739,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.27.1.tgz",
|
||||
"integrity": "sha512-XdgXNox1hx2ynQy3jcCbzX1YsJt9en9VnZVidNXTNnQUdl6q2twSoV1GjgTb7Wlw9evCYXcKkgjE8KNU7K5trg==",
|
||||
"version": "1.27.3",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.27.3.tgz",
|
||||
"integrity": "sha512-hlzmgzMznp9DhA5fMJKS5yEAyfCUMxAc+DbSPxD4J1J2cYVl1L+pZLndkt5xLlD5aB5eHEnphHMW14ammMlUXg==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12065,7 +12065,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.27.2",
|
||||
"version": "1.27.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.27.2",
|
||||
"version": "1.27.4",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.27.2",
|
||||
"version": "1.27.4",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
+7
-17
@@ -480,23 +480,13 @@ export abstract class BaseMediaSampleSink<
|
||||
|
||||
let currentPacket: EncodedPacket | null = keyPacket;
|
||||
|
||||
let endPacket: EncodedPacket | undefined = undefined;
|
||||
if (endTimestamp < Infinity) {
|
||||
// When an end timestamp is set, we cannot simply use that for the packet iterator due to out-of-order
|
||||
// frames (B-frames). Instead, we'll need to keep decoding packets until we get a frame that exceeds
|
||||
// this end time. However, we can still put a bound on it: Since key frames are by definition never
|
||||
// out of order, we can stop at the first key frame after the end timestamp.
|
||||
const packet = await packetSink.getPacket(endTimestamp);
|
||||
const keyPacket = !packet
|
||||
? null
|
||||
: packet.type === 'key' && packet.timestamp === endTimestamp
|
||||
? packet
|
||||
: await packetSink.getNextKeyPacket(packet, { verifyKeyPackets: true });
|
||||
|
||||
if (keyPacket) {
|
||||
endPacket = keyPacket;
|
||||
}
|
||||
}
|
||||
// B-frames make it exceedingly difficult to properly define an upper bound for packet iteration if an end
|
||||
// timestamp is set, so we just don't do it. The case that makes it especially tricky is when the frames
|
||||
// following a key frame have a lower timestamp than the keyframe; something that quite frequently happens
|
||||
// in HEVC streams. The price to pay for not upper-bounding the packet iterator is a slight increase in
|
||||
// decoder work at the end of the range, but the added correctness and reliability makes this tradeoff worth
|
||||
// it.
|
||||
const endPacket = undefined;
|
||||
|
||||
const packets = packetSink.packets(keyPacket ?? undefined, endPacket);
|
||||
await packets.next(); // Skip the start packet as we already have it
|
||||
|
||||
+141
-61
@@ -18,6 +18,7 @@ import {
|
||||
isFirefox,
|
||||
polyfillSymbolDispose,
|
||||
assertNever,
|
||||
isWebKit,
|
||||
} from './misc';
|
||||
|
||||
polyfillSymbolDispose();
|
||||
@@ -445,8 +446,7 @@ export class VideoSample implements Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes required to hold this video sample's pixel data. Throws if `format` is `null`;
|
||||
* specify an explicit RGB format in the options in this case.
|
||||
* Returns the number of bytes required to hold this video sample's pixel data. Throws if `format` is `null`.
|
||||
*/
|
||||
allocationSize(options: VideoFrameCopyToOptions = {}): number {
|
||||
validateVideoFrameCopyToOptions(options);
|
||||
@@ -454,11 +454,10 @@ export class VideoSample implements Disposable {
|
||||
if (this._closed) {
|
||||
throw new Error('VideoSample is closed.');
|
||||
}
|
||||
if ((options.format ?? this.format) === null) {
|
||||
throw new Error(
|
||||
'Cannot get allocation size when format is null. Please manually provide an RGB pixel format in the'
|
||||
+ ' options instead.',
|
||||
);
|
||||
if (this.format === null) {
|
||||
// https://github.com/Vanilagy/mediabunny/issues/267
|
||||
// https://github.com/w3c/webcodecs/issues/920
|
||||
throw new Error('Cannot get allocation size when format is null. Sorry!');
|
||||
}
|
||||
|
||||
assert(this._data !== null);
|
||||
@@ -471,6 +470,7 @@ export class VideoSample implements Disposable {
|
||||
|| options.rect
|
||||
) {
|
||||
// Temporarily convert to VideoFrame to get it done
|
||||
// TODO: Compute this directly without needing to go through VideoFrame
|
||||
const videoFrame = this.toVideoFrame();
|
||||
const size = videoFrame.allocationSize(options);
|
||||
videoFrame.close();
|
||||
@@ -489,8 +489,7 @@ export class VideoSample implements Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies this video sample's pixel data to an ArrayBuffer or ArrayBufferView. Throws if `format` is `null`;
|
||||
* specify an explicit RGB format in the options in this case.
|
||||
* Copies this video sample's pixel data to an ArrayBuffer or ArrayBufferView. Throws if `format` is `null`.
|
||||
* @returns The byte layout of the planes of the copied data.
|
||||
*/
|
||||
async copyTo(destination: AllowSharedBufferSource, options: VideoFrameCopyToOptions = {}): Promise<PlaneLayout[]> {
|
||||
@@ -502,11 +501,8 @@ export class VideoSample implements Disposable {
|
||||
if (this._closed) {
|
||||
throw new Error('VideoSample is closed.');
|
||||
}
|
||||
if ((options.format ?? this.format) === null) {
|
||||
throw new Error(
|
||||
'Cannot copy video sample data when format is null. Please manually provide an RGB pixel format in the'
|
||||
+ ' options instead.',
|
||||
);
|
||||
if (this.format === null) {
|
||||
throw new Error('Cannot copy video sample data when format is null. Sorry!');
|
||||
}
|
||||
|
||||
assert(this._data !== null);
|
||||
@@ -519,6 +515,7 @@ export class VideoSample implements Disposable {
|
||||
|| options.rect
|
||||
) {
|
||||
// Temporarily convert to VideoFrame to get it done
|
||||
// TODO: Do this directly without needing to go through VideoFrame
|
||||
const videoFrame = this.toVideoFrame();
|
||||
const layout = await videoFrame.copyTo(destination, options);
|
||||
videoFrame.close();
|
||||
@@ -1416,6 +1413,7 @@ export class AudioSample implements Disposable {
|
||||
|
||||
const { planeIndex, format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options;
|
||||
|
||||
const srcFormat = this.format;
|
||||
const destFormat = format ?? this.format;
|
||||
if (!destFormat) throw new Error('Destination format not determined');
|
||||
|
||||
@@ -1450,58 +1448,31 @@ export class AudioSample implements Disposable {
|
||||
const writeFn = getWriteFunction(destFormat);
|
||||
|
||||
if (isAudioData(this._data)) {
|
||||
if (destIsPlanar) {
|
||||
if (destFormat === 'f32-planar') {
|
||||
// Simple, since the browser must support f32-planar, we can just delegate here
|
||||
this._data.copyTo(destination, {
|
||||
planeIndex,
|
||||
frameOffset,
|
||||
frameCount: copyFrameCount,
|
||||
format: 'f32-planar',
|
||||
});
|
||||
} else {
|
||||
// Allocate temporary buffer for f32-planar data
|
||||
const tempBuffer = new ArrayBuffer(copyFrameCount * 4);
|
||||
const tempArray = new Float32Array(tempBuffer);
|
||||
this._data.copyTo(tempArray, {
|
||||
planeIndex,
|
||||
frameOffset,
|
||||
frameCount: copyFrameCount,
|
||||
format: 'f32-planar',
|
||||
});
|
||||
|
||||
// Convert each f32 sample to destination format
|
||||
const tempView = new DataView(tempBuffer);
|
||||
for (let i = 0; i < copyFrameCount; i++) {
|
||||
const destOffset = i * destBytesPerSample;
|
||||
const sample = tempView.getFloat32(i * 4, true);
|
||||
writeFn(destView, destOffset, sample);
|
||||
}
|
||||
}
|
||||
if (isWebKit() && numChannels > 2 && destFormat !== srcFormat) {
|
||||
// WebKit bug workaround
|
||||
doAudioDataCopyToWebKitWorkaround(
|
||||
this._data,
|
||||
destView,
|
||||
srcFormat,
|
||||
destFormat,
|
||||
numChannels,
|
||||
planeIndex,
|
||||
frameOffset,
|
||||
copyFrameCount,
|
||||
);
|
||||
} else {
|
||||
// Destination is interleaved.
|
||||
// Allocate a temporary Float32Array to hold one channel's worth of data.
|
||||
const numCh = numChannels;
|
||||
const temp = new Float32Array(copyFrameCount);
|
||||
for (let ch = 0; ch < numCh; ch++) {
|
||||
this._data.copyTo(temp, {
|
||||
planeIndex: ch,
|
||||
frameOffset,
|
||||
frameCount: copyFrameCount,
|
||||
format: 'f32-planar',
|
||||
});
|
||||
for (let i = 0; i < copyFrameCount; i++) {
|
||||
const destIndex = i * numCh + ch;
|
||||
const destOffset = destIndex * destBytesPerSample;
|
||||
writeFn(destView, destOffset, temp[i]!);
|
||||
}
|
||||
}
|
||||
// Per spec, only f32-planar conversion must be supported, but in practice, all browsers support all
|
||||
// destination formats, so let's just delegate here:
|
||||
this._data.copyTo(destination, {
|
||||
planeIndex,
|
||||
frameOffset,
|
||||
frameCount: copyFrameCount,
|
||||
format: destFormat,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const uint8Data = this._data;
|
||||
const srcView = toDataView(uint8Data);
|
||||
|
||||
const srcFormat = this.format;
|
||||
const readFn = getReadFunction(srcFormat);
|
||||
const srcBytesPerSample = getBytesPerSample(srcFormat);
|
||||
const srcIsPlanar = formatIsPlanar(srcFormat);
|
||||
@@ -1844,3 +1815,112 @@ const getWriteFunction = (format: AudioSampleFormat): (view: DataView, offset: n
|
||||
const isAudioData = (x: unknown): x is AudioData => {
|
||||
return typeof AudioData !== 'undefined' && x instanceof AudioData;
|
||||
};
|
||||
|
||||
/**
|
||||
* WebKit has a bug where calling AudioData.copyTo with a format different from the source format
|
||||
* crashes the tab when there are more than 2 channels. This function works around that by always
|
||||
* copying with the source format and then manually converting to the destination format.
|
||||
*
|
||||
* See https://bugs.webkit.org/show_bug.cgi?id=302521.
|
||||
*/
|
||||
const doAudioDataCopyToWebKitWorkaround = (
|
||||
audioData: AudioData,
|
||||
destView: DataView,
|
||||
srcFormat: AudioSampleFormat,
|
||||
destFormat: AudioSampleFormat,
|
||||
numChannels: number,
|
||||
planeIndex: number,
|
||||
frameOffset: number,
|
||||
copyFrameCount: number,
|
||||
) => {
|
||||
const readFn = getReadFunction(srcFormat);
|
||||
const writeFn = getWriteFunction(destFormat);
|
||||
const srcBytesPerSample = getBytesPerSample(srcFormat);
|
||||
const destBytesPerSample = getBytesPerSample(destFormat);
|
||||
const srcIsPlanar = formatIsPlanar(srcFormat);
|
||||
const destIsPlanar = formatIsPlanar(destFormat);
|
||||
|
||||
if (destIsPlanar) {
|
||||
if (srcIsPlanar) {
|
||||
// src planar -> dest planar: copy single plane and convert
|
||||
const data = new ArrayBuffer(copyFrameCount * srcBytesPerSample);
|
||||
const dataView = toDataView(data);
|
||||
|
||||
audioData.copyTo(data, {
|
||||
planeIndex,
|
||||
frameOffset,
|
||||
frameCount: copyFrameCount,
|
||||
format: srcFormat,
|
||||
});
|
||||
|
||||
for (let i = 0; i < copyFrameCount; i++) {
|
||||
const srcOffset = i * srcBytesPerSample;
|
||||
const destOffset = i * destBytesPerSample;
|
||||
const sample = readFn(dataView, srcOffset);
|
||||
writeFn(destView, destOffset, sample);
|
||||
}
|
||||
} else {
|
||||
// src interleaved -> dest planar: copy all interleaved data, extract one channel
|
||||
const data = new ArrayBuffer(copyFrameCount * numChannels * srcBytesPerSample);
|
||||
const dataView = toDataView(data);
|
||||
|
||||
audioData.copyTo(data, {
|
||||
planeIndex: 0,
|
||||
frameOffset,
|
||||
frameCount: copyFrameCount,
|
||||
format: srcFormat,
|
||||
});
|
||||
|
||||
for (let i = 0; i < copyFrameCount; i++) {
|
||||
const srcOffset = (i * numChannels + planeIndex) * srcBytesPerSample;
|
||||
const destOffset = i * destBytesPerSample;
|
||||
const sample = readFn(dataView, srcOffset);
|
||||
writeFn(destView, destOffset, sample);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (srcIsPlanar) {
|
||||
// src planar -> dest interleaved: copy each plane and interleave
|
||||
const planeSize = copyFrameCount * srcBytesPerSample;
|
||||
const data = new ArrayBuffer(planeSize);
|
||||
const dataView = toDataView(data);
|
||||
|
||||
for (let ch = 0; ch < numChannels; ch++) {
|
||||
audioData.copyTo(data, {
|
||||
planeIndex: ch,
|
||||
frameOffset,
|
||||
frameCount: copyFrameCount,
|
||||
format: srcFormat,
|
||||
});
|
||||
|
||||
for (let i = 0; i < copyFrameCount; i++) {
|
||||
const srcOffset = i * srcBytesPerSample;
|
||||
const destOffset = (i * numChannels + ch) * destBytesPerSample;
|
||||
const sample = readFn(dataView, srcOffset);
|
||||
writeFn(destView, destOffset, sample);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// src interleaved -> dest interleaved: copy all and convert
|
||||
const data = new ArrayBuffer(copyFrameCount * numChannels * srcBytesPerSample);
|
||||
const dataView = toDataView(data);
|
||||
|
||||
audioData.copyTo(data, {
|
||||
planeIndex: 0,
|
||||
frameOffset,
|
||||
frameCount: copyFrameCount,
|
||||
format: srcFormat,
|
||||
});
|
||||
|
||||
for (let i = 0; i < copyFrameCount; i++) {
|
||||
for (let ch = 0; ch < numChannels; ch++) {
|
||||
const idx = i * numChannels + ch;
|
||||
const srcOffset = idx * srcBytesPerSample;
|
||||
const destOffset = idx * destBytesPerSample;
|
||||
const sample = readFn(dataView, srcOffset);
|
||||
writeFn(destView, destOffset, sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,6 +201,12 @@ test('null format', async () => {
|
||||
expect(() => sample.allocationSize()).toThrow('when format is null');
|
||||
await expect(async () => sample.copyTo(new ArrayBuffer())).rejects.toThrow('when format is null');
|
||||
|
||||
// Even this throws :(
|
||||
// See https://github.com/Vanilagy/mediabunny/issues/267
|
||||
expect(() => sample.allocationSize({ format: 'RGBA' })).toThrow('when format is null');
|
||||
|
||||
// Uncomment this if the RGBA conversion works again:
|
||||
/*
|
||||
const size = sample.allocationSize({ format: 'RGBA' });
|
||||
expect(size).toBe(1280 * 720 * 4);
|
||||
const buffer = new ArrayBuffer(size);
|
||||
@@ -217,4 +223,5 @@ test('null format', async () => {
|
||||
await sample.copyTo(buffer, { format: 'RGBX' });
|
||||
await sample.copyTo(buffer, { format: 'BGRA' });
|
||||
await sample.copyTo(buffer, { format: 'BGRX' });
|
||||
*/
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user