Compare commits

...
8 Commits
8 changed files with 169 additions and 86 deletions
+4
View File
@@ -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 = {
+2
View File
@@ -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' },
+6 -6
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
}
}
};
+7
View File
@@ -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' });
*/
});