Merge main into release for tag v1.24.5

This commit is contained in:
github-actions[bot]
2025-11-10 20:20:43 +00:00
11 changed files with 141 additions and 29 deletions
+29 -1
View File
@@ -14,7 +14,34 @@
source: new Mediabunny.BlobSource(file), source: new Mediabunny.BlobSource(file),
}); });
const videoTrack = await input.getPrimaryVideoTrack(); const videoTrack = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
for await (const packet of sink.packets()) {
console.log(packet.timestamp, packet.duration, packet.timestamp + packet.duration);
if (packet.timestamp > 10) break;
}
/*
const sink = new Mediabunny.VideoSampleSink(videoTrack);
for await (const sample of sink.samples()) {
console.log(sample.timestamp, sample.duration);
sample.close();
}
*/
/*
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
for await (const packet of sink.packets()) {
console.log(packet.timestamp, packet.timestamp + packet.duration);
}
*/
/*
const sink = new Mediabunny.VideoSampleSink(videoTrack); const sink = new Mediabunny.VideoSampleSink(videoTrack);
console.log(await videoTrack.getDecoderConfig()) console.log(await videoTrack.getDecoderConfig())
@@ -30,6 +57,7 @@
for await (const sample of sink.samples()) { for await (const sample of sink.samples()) {
console.log(sample); console.log(sample);
} }
*/
/* /*
let total = 0; let total = 0;
+6 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.24.4", "version": "1.24.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.24.4", "version": "1.24.5",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -7749,9 +7749,9 @@
} }
}, },
"node_modules/mediabunny": { "node_modules/mediabunny": {
"version": "1.24.3", "version": "1.24.4",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.3.tgz", "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.4.tgz",
"integrity": "sha512-uQu5lXg6c3z2YUKLimAsem1qYPdGfGziQ5CKaAMJAs0C0NNw5T/Zkn0foPML0gWqQJ5ZeC0wTgCxpiBQY8m1Tw==", "integrity": "sha512-dpWYBPTtMg152yNLXZQ7xb6hsXdYbKp9EuK8qq4npS+SZ08FVc1XHlXYhrOm31T+tUVJKgm95Yaqy69wTpZP9Q==",
"license": "MPL-2.0", "license": "MPL-2.0",
"peer": true, "peer": true,
"workspaces": [ "workspaces": [
@@ -12242,7 +12242,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.24.4", "version": "1.24.5",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.24.4", "version": "1.24.5",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.24.4", "version": "1.24.5",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+48
View File
@@ -621,6 +621,54 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioS
}; };
}; };
export const buildAacAudioSpecificConfig = (config: {
objectType: number;
sampleRate: number;
numberOfChannels: number;
}) => {
let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate);
let customSampleRate: number | null = null;
if (frequencyIndex === -1) {
frequencyIndex = 15;
customSampleRate = config.sampleRate;
}
const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels);
if (channelConfiguration === -1) {
throw new TypeError(`Unsupported number of channels: ${config.numberOfChannels}`);
}
let bitCount = 5 + 4 + 4;
if (config.objectType >= 32) {
bitCount += 6;
}
if (frequencyIndex === 15) {
bitCount += 24;
}
const byteCount = Math.ceil(bitCount / 8);
const bytes = new Uint8Array(byteCount);
const bitstream = new Bitstream(bytes);
if (config.objectType < 32) {
bitstream.writeBits(5, config.objectType);
} else {
bitstream.writeBits(5, 31);
bitstream.writeBits(6, config.objectType - 32);
}
bitstream.writeBits(4, frequencyIndex);
if (frequencyIndex === 15) {
bitstream.writeBits(24, customSampleRate!);
}
bitstream.writeBits(4, channelConfiguration);
return bytes;
};
export const OPUS_SAMPLE_RATE = 48_000; export const OPUS_SAMPLE_RATE = 48_000;
const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/; const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;
+2 -2
View File
@@ -844,13 +844,13 @@ export class MatroskaDemuxer extends Demuxer {
blocks.splice(blockIndex, 1); // Remove the original block blocks.splice(blockIndex, 1); // Remove the original block
const blockDuration = originalBlock.duration || frameCount * (track.defaultDuration ?? 0);
// Now, let's insert each frame as its own block // Now, let's insert each frame as its own block
for (let i = 0; i < frameCount; i++) { for (let i = 0; i < frameCount; i++) {
const frameSize = frameSizes[i]!; const frameSize = frameSizes[i]!;
const frameData = readBytes(slice, frameSize); const frameData = readBytes(slice, frameSize);
const blockDuration = originalBlock.duration || (frameCount * (track.defaultDuration ?? 0));
// Distribute timestamps evenly across the block duration // Distribute timestamps evenly across the block duration
const frameTimestamp = originalBlock.timestamp + (blockDuration * i / frameCount); const frameTimestamp = originalBlock.timestamp + (blockDuration * i / frameCount);
const frameDuration = blockDuration / frameCount; const frameDuration = blockDuration / frameCount;
+4 -4
View File
@@ -26,7 +26,7 @@ import {
insertSorted, insertSorted,
isFirefox, isFirefox,
isNumber, isNumber,
isSafari, isWebKit,
last, last,
mapAsyncGenerator, mapAsyncGenerator,
promiseWithResolvers, promiseWithResolvers,
@@ -918,7 +918,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
} else { } else {
assert(this.decoder); assert(this.decoder);
if (!isSafari()) { if (!isWebKit()) {
insertSorted(this.inputTimestamps, packet.timestamp, x => x); insertSorted(this.inputTimestamps, packet.timestamp, x => x);
} }
@@ -1049,7 +1049,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
/** Handler for the WebCodecs VideoDecoder for ironing out browser differences. */ /** Handler for the WebCodecs VideoDecoder for ironing out browser differences. */
sampleHandler(sample: VideoSample) { sampleHandler(sample: VideoSample) {
if (isSafari()) { if (isWebKit()) {
// For correct B-frame handling, we don't just hand over the frames directly but instead add them to // For correct B-frame handling, we don't just hand over the frames directly but instead add them to
// a queue, because we want to ensure frames are emitted in presentation order. We flush the queue // a queue, because we want to ensure frames are emitted in presentation order. We flush the queue
// each time we receive a frame with a timestamp larger than the highest we've seen so far, as we // each time we receive a frame with a timestamp larger than the highest we've seen so far, as we
@@ -1137,7 +1137,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
this.alphaRaslSkipped = false; this.alphaRaslSkipped = false;
} }
if (isSafari()) { if (isWebKit()) {
for (const sample of this.sampleQueue) { for (const sample of this.sampleQueue) {
this.finalizeAndEmitSample(sample); this.finalizeAndEmitSample(sample);
} }
+30
View File
@@ -9,6 +9,8 @@
import { import {
AUDIO_CODECS, AUDIO_CODECS,
AudioCodec, AudioCodec,
buildAacAudioSpecificConfig,
parseAacAudioSpecificConfig,
parsePcmCodec, parsePcmCodec,
PCM_AUDIO_CODECS, PCM_AUDIO_CODECS,
PcmAudioCodec, PcmAudioCodec,
@@ -24,9 +26,11 @@ import {
CallSerializer, CallSerializer,
clamp, clamp,
isFirefox, isFirefox,
last,
promiseWithResolvers, promiseWithResolvers,
setInt24, setInt24,
setUint24, setUint24,
toUint8Array,
} from './misc'; } from './misc';
import { Muxer } from './muxer'; import { Muxer } from './muxer';
import { SubtitleParser } from './subtitles'; import { SubtitleParser } from './subtitles';
@@ -1515,6 +1519,32 @@ class AudioEncoderWrapper {
this.encoder = new AudioEncoder({ this.encoder = new AudioEncoder({
output: (chunk, meta) => { output: (chunk, meta) => {
// WebKit emits an invalid description for AAC (https://bugs.webkit.org/show_bug.cgi?id=302253),
// which we try to detect here. If detected, we'll provide our own description instead, derived
// from the codec string and audio parameters.
if (this.encodingConfig.codec === 'aac' && meta?.decoderConfig) {
let needsDescriptionOverwrite = false;
if (!meta.decoderConfig.description || meta.decoderConfig.description.byteLength < 2) {
needsDescriptionOverwrite = true;
} else {
const audioSpecificConfig = parseAacAudioSpecificConfig(
toUint8Array(meta.decoderConfig.description),
);
needsDescriptionOverwrite = audioSpecificConfig.objectType === 0;
}
if (needsDescriptionOverwrite) {
const objectType = Number(last(encoderConfig.codec.split('.')));
meta.decoderConfig.description = buildAacAudioSpecificConfig({
objectType,
numberOfChannels: meta.decoderConfig.numberOfChannels,
sampleRate: meta.decoderConfig.sampleRate,
});
}
}
const packet = EncodedPacket.fromEncodedChunk(chunk); const packet = EncodedPacket.fromEncodedChunk(chunk);
this.encodingConfig.onEncodedPacket?.(packet, meta); this.encodingConfig.onEncodedPacket?.(packet, meta);
+7 -12
View File
@@ -658,21 +658,16 @@ export class CallSerializer {
} }
} }
let isSafariCache: boolean | null = null; let isWebKitCache: boolean | null = null;
export const isSafari = () => { export const isWebKit = () => {
if (isSafariCache !== null) { if (isWebKitCache !== null) {
return isSafariCache; return isWebKitCache;
} }
const result = !!( // This even returns true for WebKit-wrapping browsers such as Chrome on iOS
typeof navigator !== 'undefined' const result = !!(typeof navigator !== 'undefined' && navigator.vendor?.match(/apple/i));
&& navigator.vendor?.match(/apple/i)
&& !navigator.userAgent?.match(/crios/i)
&& !navigator.userAgent?.match(/fxios/i)
&& !navigator.userAgent?.match(/Opera|OPT\//)
);
isSafariCache = result; isWebKitCache = result;
return result; return result;
}; };
+4 -1
View File
@@ -126,7 +126,10 @@ export class EncodedPacket {
} }
} }
/** If this packet is a metadata-only packet. Metadata-only packets don't contain their packet data. */ /**
* If this packet is a metadata-only packet. Metadata-only packets don't contain their packet data. They are the
* result of retrieving packets with {@link PacketRetrievalOptions.metadataOnly} set to `true`.
*/
get isMetadataOnly() { get isMetadataOnly() {
return this.data === PLACEHOLDER_DATA; return this.data === PLACEHOLDER_DATA;
} }
+9 -1
View File
@@ -12,6 +12,7 @@ import {
binarySearchLessOrEqual, binarySearchLessOrEqual,
closedIntervalsOverlap, closedIntervalsOverlap,
isNumber, isNumber,
isWebKit,
MaybePromise, MaybePromise,
mergeRequestInit, mergeRequestInit,
promiseWithResolvers, promiseWithResolvers,
@@ -209,7 +210,14 @@ export class BlobSource extends Source {
private async _runWorker(worker: ReadWorker) { private async _runWorker(worker: ReadWorker) {
let reader = this._readers.get(worker); let reader = this._readers.get(worker);
if (reader === undefined) { if (reader === undefined) {
if ('stream' in this._blob) { // https://github.com/Vanilagy/mediabunny/issues/184
// WebKit has critical bugs with blob.stream():
// - WebKitBlobResource error 1 when streaming large files
// - Memory buildup and reload loops on iOS (network process crashes)
// - ReadableStream stalls under backpressure (especially video)
// Affects Safari and all iOS browsers (Chrome, Firefox, etc.).
// Use arrayBuffer() fallback for WebKit browsers.
if ('stream' in this._blob && !isWebKit()) {
// Get a reader of the blob starting at the required offset, and then keep it around // Get a reader of the blob starting at the required offset, and then keep it around
const slice = this._blob.slice(worker.currentPos); const slice = this._blob.slice(worker.currentPos);
reader = slice.stream().getReader(); reader = slice.stream().getReader();