diff --git a/dev/demux.html b/dev/demux.html index 2bca413..1a864a3 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -14,7 +14,34 @@ 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); console.log(await videoTrack.getDecoderConfig()) @@ -30,6 +57,7 @@ for await (const sample of sink.samples()) { console.log(sample); } + */ /* let total = 0; diff --git a/package-lock.json b/package-lock.json index a00879d..271da7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.24.4", + "version": "1.24.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.24.4", + "version": "1.24.5", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.24.3", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.3.tgz", - "integrity": "sha512-uQu5lXg6c3z2YUKLimAsem1qYPdGfGziQ5CKaAMJAs0C0NNw5T/Zkn0foPML0gWqQJ5ZeC0wTgCxpiBQY8m1Tw==", + "version": "1.24.4", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.4.tgz", + "integrity": "sha512-dpWYBPTtMg152yNLXZQ7xb6hsXdYbKp9EuK8qq4npS+SZ08FVc1XHlXYhrOm31T+tUVJKgm95Yaqy69wTpZP9Q==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.24.4", + "version": "1.24.5", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index f8fa949..3b11949 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "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.", "type": "module", "workspaces": [ diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index f4840ff..b7d133b 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.24.4", + "version": "1.24.5", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", diff --git a/src/codec.ts b/src/codec.ts index 2e90cfd..a396e70 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -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; const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/; diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index c9655d5..e964bef 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -844,13 +844,13 @@ export class MatroskaDemuxer extends Demuxer { 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 for (let i = 0; i < frameCount; i++) { const frameSize = frameSizes[i]!; const frameData = readBytes(slice, frameSize); - const blockDuration = originalBlock.duration || (frameCount * (track.defaultDuration ?? 0)); - // Distribute timestamps evenly across the block duration const frameTimestamp = originalBlock.timestamp + (blockDuration * i / frameCount); const frameDuration = blockDuration / frameCount; diff --git a/src/media-sink.ts b/src/media-sink.ts index 1d0665a..195ff2a 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -26,7 +26,7 @@ import { insertSorted, isFirefox, isNumber, - isSafari, + isWebKit, last, mapAsyncGenerator, promiseWithResolvers, @@ -918,7 +918,7 @@ class VideoDecoderWrapper extends DecoderWrapper { } else { assert(this.decoder); - if (!isSafari()) { + if (!isWebKit()) { insertSorted(this.inputTimestamps, packet.timestamp, x => x); } @@ -1049,7 +1049,7 @@ class VideoDecoderWrapper extends DecoderWrapper { /** Handler for the WebCodecs VideoDecoder for ironing out browser differences. */ 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 // 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 @@ -1137,7 +1137,7 @@ class VideoDecoderWrapper extends DecoderWrapper { this.alphaRaslSkipped = false; } - if (isSafari()) { + if (isWebKit()) { for (const sample of this.sampleQueue) { this.finalizeAndEmitSample(sample); } diff --git a/src/media-source.ts b/src/media-source.ts index c89b24a..86989a2 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -9,6 +9,8 @@ import { AUDIO_CODECS, AudioCodec, + buildAacAudioSpecificConfig, + parseAacAudioSpecificConfig, parsePcmCodec, PCM_AUDIO_CODECS, PcmAudioCodec, @@ -24,9 +26,11 @@ import { CallSerializer, clamp, isFirefox, + last, promiseWithResolvers, setInt24, setUint24, + toUint8Array, } from './misc'; import { Muxer } from './muxer'; import { SubtitleParser } from './subtitles'; @@ -1515,6 +1519,32 @@ class AudioEncoderWrapper { this.encoder = new AudioEncoder({ 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); this.encodingConfig.onEncodedPacket?.(packet, meta); diff --git a/src/misc.ts b/src/misc.ts index 6bdb52b..c6f5119 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -658,21 +658,16 @@ export class CallSerializer { } } -let isSafariCache: boolean | null = null; -export const isSafari = () => { - if (isSafariCache !== null) { - return isSafariCache; +let isWebKitCache: boolean | null = null; +export const isWebKit = () => { + if (isWebKitCache !== null) { + return isWebKitCache; } - const result = !!( - typeof navigator !== 'undefined' - && navigator.vendor?.match(/apple/i) - && !navigator.userAgent?.match(/crios/i) - && !navigator.userAgent?.match(/fxios/i) - && !navigator.userAgent?.match(/Opera|OPT\//) - ); + // This even returns true for WebKit-wrapping browsers such as Chrome on iOS + const result = !!(typeof navigator !== 'undefined' && navigator.vendor?.match(/apple/i)); - isSafariCache = result; + isWebKitCache = result; return result; }; diff --git a/src/packet.ts b/src/packet.ts index c71890c..f5de60e 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -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() { return this.data === PLACEHOLDER_DATA; } diff --git a/src/source.ts b/src/source.ts index adafa2e..68ebac1 100644 --- a/src/source.ts +++ b/src/source.ts @@ -12,6 +12,7 @@ import { binarySearchLessOrEqual, closedIntervalsOverlap, isNumber, + isWebKit, MaybePromise, mergeRequestInit, promiseWithResolvers, @@ -209,7 +210,14 @@ export class BlobSource extends Source { private async _runWorker(worker: ReadWorker) { let reader = this._readers.get(worker); 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 const slice = this._blob.slice(worker.currentPos); reader = slice.stream().getReader();