mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-29 03:43:50 +02:00
Merge main into release for tag v1.28.0
This commit is contained in:
@@ -194,9 +194,12 @@ This format ensures [append-only writing](#append-only-writing).
|
||||
The following options are available:
|
||||
```ts
|
||||
type OggOutputFormatOptions = {
|
||||
maximumPageDuration?: number;
|
||||
onPage?: (data: Uint8Array, position: number, source: MediaSource) => unknown;
|
||||
};
|
||||
```
|
||||
- `maximumPageDuration`\
|
||||
The maximum duration in seconds of each Ogg page. Pages will be flushed early if adding another packet would cause the page to exceed this duration. This is useful for streaming contexts where more frequent page output is desired. By default, pages are only flushed when they exceed a certain size.
|
||||
- `onPage`\
|
||||
Will be called for each finalized Ogg page of the output file. The [media source](./media-sources) backing the page's track (logical bitstream) is also passed.
|
||||
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.27.6",
|
||||
"version": "1.28.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.27.6",
|
||||
"version": "1.28.0",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7739,9 +7739,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.27.5",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.27.5.tgz",
|
||||
"integrity": "sha512-VGiZILC3S7muo7bhnq5/PvorqB4A72qSFOzI5tg2Ed79ecco9VFY4kvzbU32lLuuxSLxb9TWpOe1cGGtQuP0vA==",
|
||||
"version": "1.27.6",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.27.6.tgz",
|
||||
"integrity": "sha512-Y6QLjH5lAea9swaLcfXzcK/Xw1cFW4KDLvkm+0fUFcwfp8SEnAiMh8RSBtMBVADoE7hTCA9yn7JoYJv3VBd/yQ==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12065,7 +12065,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.27.6",
|
||||
"version": "1.28.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.27.6",
|
||||
"version": "1.28.0",
|
||||
"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.6",
|
||||
"version": "1.28.0",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -322,6 +322,10 @@ export class OggDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
const totalPacketSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
if (totalPacketSize === 0) {
|
||||
return null; // Invalid packet, treat it as end of stream
|
||||
}
|
||||
|
||||
const packetData = new Uint8Array(totalPacketSize);
|
||||
|
||||
let offset = 0;
|
||||
|
||||
+34
-17
@@ -47,12 +47,13 @@ type OggTrackData = {
|
||||
currentPageData: Uint8Array[];
|
||||
currentPageSize: number;
|
||||
currentPageStartsWithFreshPacket: boolean;
|
||||
currentPageStartTimestampInSamples: number;
|
||||
};
|
||||
|
||||
type Packet = {
|
||||
data: Uint8Array;
|
||||
endGranulePosition: number;
|
||||
timestamp: number;
|
||||
timestampInSamples: number;
|
||||
durationInSamples: number;
|
||||
forcePageFlush: boolean;
|
||||
};
|
||||
|
||||
@@ -132,6 +133,7 @@ export class OggMuxer extends Muxer {
|
||||
currentPageData: [],
|
||||
currentPageSize: 27,
|
||||
currentPageStartsWithFreshPacket: true,
|
||||
currentPageStartTimestampInSamples: 0,
|
||||
};
|
||||
|
||||
this.queueHeaderPackets(newTrackData, meta);
|
||||
@@ -199,18 +201,18 @@ export class OggMuxer extends Muxer {
|
||||
|
||||
trackData.packetQueue.push({
|
||||
data: identificationHeader,
|
||||
endGranulePosition: 0,
|
||||
timestamp: 0,
|
||||
timestampInSamples: 0,
|
||||
durationInSamples: 0,
|
||||
forcePageFlush: true,
|
||||
}, {
|
||||
data: commentHeader,
|
||||
endGranulePosition: 0,
|
||||
timestamp: 0,
|
||||
timestampInSamples: 0,
|
||||
durationInSamples: 0,
|
||||
forcePageFlush: false,
|
||||
}, {
|
||||
data: setupHeader,
|
||||
endGranulePosition: 0,
|
||||
timestamp: 0,
|
||||
timestampInSamples: 0,
|
||||
durationInSamples: 0,
|
||||
forcePageFlush: true, // The last header packet must flush the page
|
||||
});
|
||||
|
||||
@@ -239,13 +241,13 @@ export class OggMuxer extends Muxer {
|
||||
|
||||
trackData.packetQueue.push({
|
||||
data: identificationHeader,
|
||||
endGranulePosition: 0,
|
||||
timestamp: 0,
|
||||
timestampInSamples: 0,
|
||||
durationInSamples: 0,
|
||||
forcePageFlush: true,
|
||||
}, {
|
||||
data: commentHeader,
|
||||
endGranulePosition: 0,
|
||||
timestamp: 0,
|
||||
timestampInSamples: 0,
|
||||
durationInSamples: 0,
|
||||
forcePageFlush: true, // The last header packet must flush the page
|
||||
});
|
||||
|
||||
@@ -275,8 +277,8 @@ export class OggMuxer extends Muxer {
|
||||
|
||||
trackData.packetQueue.push({
|
||||
data: packet.data,
|
||||
endGranulePosition: trackData.currentTimestampInSamples,
|
||||
timestamp: currentTimestampInSamples / trackData.internalSampleRate,
|
||||
timestampInSamples: currentTimestampInSamples,
|
||||
durationInSamples,
|
||||
forcePageFlush: false,
|
||||
});
|
||||
|
||||
@@ -338,10 +340,10 @@ export class OggMuxer extends Muxer {
|
||||
|
||||
if (
|
||||
trackData.packetQueue.length > 0
|
||||
&& trackData.packetQueue[0]!.timestamp < minTimestamp
|
||||
&& trackData.packetQueue[0]!.timestampInSamples < minTimestamp
|
||||
) {
|
||||
trackWithMinTimestamp = trackData;
|
||||
minTimestamp = trackData.packetQueue[0]!.timestamp;
|
||||
minTimestamp = trackData.packetQueue[0]!.timestampInSamples;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +363,20 @@ export class OggMuxer extends Muxer {
|
||||
}
|
||||
|
||||
writePacket(trackData: OggTrackData, packet: Packet, isFinalPacket: boolean) {
|
||||
const packetEndTimestampInSamples = packet.timestampInSamples + packet.durationInSamples;
|
||||
|
||||
if (this.format._options.maximumPageDuration !== undefined) {
|
||||
const maxDurationInSamples = this.format._options.maximumPageDuration * trackData.internalSampleRate;
|
||||
|
||||
if (
|
||||
trackData.currentLacingValues.length > 0
|
||||
&& packetEndTimestampInSamples - trackData.currentPageStartTimestampInSamples > maxDurationInSamples
|
||||
) {
|
||||
// Flush the current page early to avoid exceeding the maximum page duration
|
||||
this.writePage(trackData, false);
|
||||
}
|
||||
}
|
||||
|
||||
let remainingLength = packet.data.length;
|
||||
let dataStartOffset = 0;
|
||||
let dataOffset = 0;
|
||||
@@ -401,7 +417,7 @@ export class OggMuxer extends Muxer {
|
||||
const slice = packet.data.subarray(dataStartOffset);
|
||||
trackData.currentPageData.push(slice);
|
||||
trackData.currentPageSize += slice.length;
|
||||
trackData.currentGranulePosition = packet.endGranulePosition;
|
||||
trackData.currentGranulePosition = packetEndTimestampInSamples;
|
||||
|
||||
if (trackData.currentPageSize >= PAGE_SIZE_TARGET || packet.forcePageFlush) {
|
||||
this.writePage(trackData, isFinalPacket);
|
||||
@@ -452,6 +468,7 @@ export class OggMuxer extends Muxer {
|
||||
trackData.currentPageData.length = 0;
|
||||
trackData.currentPageSize = 27;
|
||||
trackData.currentPageStartsWithFreshPacket = true;
|
||||
trackData.currentPageStartTimestampInSamples = trackData.currentGranulePosition;
|
||||
|
||||
if (this.format._options.onPage) {
|
||||
this.writer.startTrackingWrites();
|
||||
|
||||
@@ -725,6 +725,12 @@ export class WavOutputFormat extends OutputFormat {
|
||||
* @public
|
||||
*/
|
||||
export type OggOutputFormatOptions = {
|
||||
/**
|
||||
* The maximum duration of each Ogg page, in seconds. This is useful for streaming contexts where more frequent page
|
||||
* output is desired. By default, pages are only flushed when they exceed a certain size.
|
||||
*/
|
||||
maximumPageDuration?: number;
|
||||
|
||||
/**
|
||||
* Will be called for each Ogg page that is written.
|
||||
*
|
||||
@@ -749,6 +755,12 @@ export class OggOutputFormat extends OutputFormat {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new TypeError('options must be an object.');
|
||||
}
|
||||
if (
|
||||
options.maximumPageDuration !== undefined
|
||||
&& (!Number.isFinite(options.maximumPageDuration) || options.maximumPageDuration <= 0)
|
||||
) {
|
||||
throw new TypeError('options.maximumPageDuration, when provided, must be a positive number.');
|
||||
}
|
||||
if (options.onPage !== undefined && typeof options.onPage !== 'function') {
|
||||
throw new TypeError('options.onPage, when provided, must be a function.');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { UrlSource } from '../../src/source.js';
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
import { AudioBufferSink } from '../../src/media-sink.js';
|
||||
import { assert } from '../../src/misc.js';
|
||||
|
||||
// VLC creates OGG files with an empty EOS page, which previously caused decoding errors
|
||||
test('can decode OGG Vorbis file with empty EOS page', async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('/vorbis-eos.ogg'),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
assert(track);
|
||||
|
||||
const sink = new AudioBufferSink(track);
|
||||
const buffers: AudioBuffer[] = [];
|
||||
|
||||
for await (const { buffer } of sink.buffers(4, 10)) {
|
||||
buffers.push(buffer);
|
||||
}
|
||||
|
||||
expect(buffers.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { Output } from '../../src/output.js';
|
||||
import { OggOutputFormat } from '../../src/output-format.js';
|
||||
import { NullTarget } from '../../src/target.js';
|
||||
import { AudioBufferSource } from '../../src/media-source.js';
|
||||
|
||||
test('maximumPageDuration option', async () => {
|
||||
const sampleRate = 48000;
|
||||
const durationSeconds = 2;
|
||||
const audioBuffer = new AudioBuffer({ numberOfChannels: 1, length: sampleRate * durationSeconds, sampleRate });
|
||||
|
||||
// First, create an Ogg file without the maximumPageDuration option
|
||||
let pageCountWithoutOption = 0;
|
||||
{
|
||||
const output = new Output({
|
||||
format: new OggOutputFormat({
|
||||
onPage: () => {
|
||||
pageCountWithoutOption++;
|
||||
},
|
||||
}),
|
||||
target: new NullTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioBufferSource({ codec: 'opus', bitrate: 64000 });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
await audioSource.add(audioBuffer);
|
||||
audioSource.close();
|
||||
await output.finalize();
|
||||
}
|
||||
|
||||
// Then, create an Ogg file with maximumPageDuration set to 0.1 seconds
|
||||
let pageCountWithOption = 0;
|
||||
{
|
||||
const output = new Output({
|
||||
format: new OggOutputFormat({
|
||||
maximumPageDuration: 0.1,
|
||||
onPage: () => {
|
||||
pageCountWithOption++;
|
||||
},
|
||||
}),
|
||||
target: new NullTarget(),
|
||||
});
|
||||
|
||||
const audioSource = new AudioBufferSource({ codec: 'opus', bitrate: 64000 });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
await output.start();
|
||||
await audioSource.add(audioBuffer);
|
||||
audioSource.close();
|
||||
await output.finalize();
|
||||
}
|
||||
|
||||
expect(pageCountWithoutOption).toBe(3);
|
||||
expect(pageCountWithOption).toBe(23); // It created more pages
|
||||
});
|
||||
Binary file not shown.
Reference in New Issue
Block a user