diff --git a/examples/file-compression/file-compression.ts b/examples/file-compression/file-compression.ts index d4613fa..1e4bf44 100644 --- a/examples/file-compression/file-compression.ts +++ b/examples/file-compression/file-compression.ts @@ -9,8 +9,15 @@ import { Conversion, QUALITY_VERY_LOW, } from 'mediabunny'; +import { registerAc3Decoder } from '@mediabunny/ac3'; +import { registerProresDecoder } from '@mediabunny/prores'; import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4'; + +// Enable codecs that aren't natively supported by WebCodecs. +registerAc3Decoder(); +registerProresDecoder(); + (document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl; const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement; @@ -137,7 +144,7 @@ const compressFile = async (resource: File | string) => { selectMediaButton.addEventListener('click', () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; - fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac'; + fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac'; fileInput.addEventListener('change', () => { const file = fileInput.files?.[0]; if (!file) { diff --git a/examples/hls-transcoding/hls-transcoding.ts b/examples/hls-transcoding/hls-transcoding.ts index 7444bff..c2f9d67 100644 --- a/examples/hls-transcoding/hls-transcoding.ts +++ b/examples/hls-transcoding/hls-transcoding.ts @@ -15,8 +15,15 @@ import { QUALITY_LOW, QUALITY_VERY_LOW, } from 'mediabunny'; +import { registerAc3Decoder } from '@mediabunny/ac3'; +import { registerProresDecoder } from '@mediabunny/prores'; import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4'; + +// Enable codecs that aren't natively supported by WebCodecs. +registerAc3Decoder(); +registerProresDecoder(); + (document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl; declare global { @@ -233,7 +240,7 @@ selectDirectoryButton.addEventListener('click', async () => { selectMediaButton.addEventListener('click', () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; - fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts'; + fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts'; fileInput.addEventListener('change', () => { const file = fileInput.files![0]; if (file) { diff --git a/examples/media-player/media-player.ts b/examples/media-player/media-player.ts index 30f6040..6d851e2 100644 --- a/examples/media-player/media-player.ts +++ b/examples/media-player/media-player.ts @@ -8,8 +8,15 @@ import { WrappedAudioBuffer, WrappedCanvas, } from 'mediabunny'; +import { registerAc3Decoder } from '@mediabunny/ac3'; +import { registerProresDecoder } from '@mediabunny/prores'; import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4'; + +// Enable codecs that aren't natively supported by WebCodecs. +registerAc3Decoder(); +registerProresDecoder(); + (document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl; const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement; @@ -732,7 +739,7 @@ window.addEventListener('resize', () => { selectMediaButton.addEventListener('click', () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; - fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac'; + fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac'; fileInput.addEventListener('change', () => { const file = fileInput.files?.[0]; if (!file) { diff --git a/examples/thumbnail-generation/thumbnail-generation.ts b/examples/thumbnail-generation/thumbnail-generation.ts index 5681568..69a995a 100644 --- a/examples/thumbnail-generation/thumbnail-generation.ts +++ b/examples/thumbnail-generation/thumbnail-generation.ts @@ -1,6 +1,13 @@ import { Input, ALL_FORMATS, BlobSource, UrlSource, CanvasSink } from 'mediabunny'; +import { registerAc3Decoder } from '@mediabunny/ac3'; +import { registerProresDecoder } from '@mediabunny/prores'; import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4'; + +// Enable codecs that aren't natively supported by WebCodecs. +registerAc3Decoder(); +registerProresDecoder(); + (document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl; const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement; @@ -122,7 +129,7 @@ const generateThumbnails = async (resource: File | string) => { selectMediaButton.addEventListener('click', () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; - fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac'; + fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac'; fileInput.addEventListener('change', () => { const file = fileInput.files?.[0]; if (!file) { diff --git a/package-lock.json b/package-lock.json index 5f88d24..bf05182 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.50.8", + "version": "1.50.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.50.8", + "version": "1.50.9", "license": "MPL-2.0", "workspaces": [ ".", @@ -2715,6 +2715,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.0", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", @@ -12894,7 +12954,7 @@ }, "packages/aac-encoder": { "name": "@mediabunny/aac-encoder", - "version": "1.50.8", + "version": "1.50.9", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12909,7 +12969,7 @@ }, "packages/ac3": { "name": "@mediabunny/ac3", - "version": "1.50.8", + "version": "1.50.9", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12924,7 +12984,7 @@ }, "packages/flac-encoder": { "name": "@mediabunny/flac-encoder", - "version": "1.50.8", + "version": "1.50.9", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12939,7 +12999,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.50.8", + "version": "1.50.9", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12954,7 +13014,7 @@ }, "packages/prores": { "name": "@mediabunny/prores", - "version": "1.50.8", + "version": "1.50.9", "license": "MPL-2.0", "dependencies": { "turbores": "^1.2.2" @@ -12969,10 +13029,10 @@ }, "packages/server": { "name": "@mediabunny/server", - "version": "1.50.8", + "version": "1.50.9", "license": "MPL-2.0", "dependencies": { - "@mediabunny/prores": "^1.50.8", + "@mediabunny/prores": "^1.50.9", "node-av": "^6.0.0" }, "funding": { diff --git a/package.json b/package.json index 37c5bfa..f96aa99 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.50.8", + "version": "1.50.9", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ @@ -56,10 +56,10 @@ "examples:build": "vite build", "fix-build-import-paths": "tsx scripts/add-import-extensions.ts", "append-namespace": "echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts", - "bump-patch": "npm version patch --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i", - "bump-minor": "npm version minor --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i", - "bump-major": "npm version major --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i", - "set-version": "npm version --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i" + "bump-patch": "npm version patch --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only", + "bump-minor": "npm version minor --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only", + "bump-major": "npm version major --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only", + "set-version": "npm version --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only" }, "license": "MPL-2.0", "repository": { diff --git a/packages/aac-encoder/package.json b/packages/aac-encoder/package.json index 8d2641d..4e11979 100644 --- a/packages/aac-encoder/package.json +++ b/packages/aac-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/aac-encoder", "author": "Vanilagy", - "version": "1.50.8", + "version": "1.50.9", "description": "AAC encoder extension for Mediabunny, based on FFmpeg.", "main": "./dist/bundles/mediabunny-aac-encoder.mjs", "module": "./dist/bundles/mediabunny-aac-encoder.mjs", diff --git a/packages/aac-encoder/src/encode.worker.ts b/packages/aac-encoder/src/encode.worker.ts index 76fea2f..5627280 100644 --- a/packages/aac-encoder/src/encode.worker.ts +++ b/packages/aac-encoder/src/encode.worker.ts @@ -192,3 +192,7 @@ if (parentPort) { } else { self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand })); } + +// Prevents the worker for being randomly closed by Firefox +// https://github.com/Vanilagy/mediabunny/issues/435 +setInterval(() => {}, 1000); diff --git a/packages/ac3/package.json b/packages/ac3/package.json index bf60d87..66bd294 100644 --- a/packages/ac3/package.json +++ b/packages/ac3/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/ac3", "author": "Vanilagy", - "version": "1.50.8", + "version": "1.50.9", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.", "main": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs", diff --git a/packages/ac3/src/codec.worker.ts b/packages/ac3/src/codec.worker.ts index 8bfc2fa..e474da1 100644 --- a/packages/ac3/src/codec.worker.ts +++ b/packages/ac3/src/codec.worker.ts @@ -304,3 +304,7 @@ if (parentPort) { } else { self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand })); } + +// Prevents the worker for being randomly closed by Firefox +// https://github.com/Vanilagy/mediabunny/issues/435 +setInterval(() => {}, 1000); diff --git a/packages/ac3/src/worker-client.ts b/packages/ac3/src/worker-client.ts index 74a8bab..15c6b8f 100644 --- a/packages/ac3/src/worker-client.ts +++ b/packages/ac3/src/worker-client.ts @@ -46,6 +46,7 @@ export const unrefWorker = async () => { if (worker) { if (worker.unref) { worker.unref(); // If we don't do this, then the Node process never terminates by itself + // Keep the worker around tho } else if (typeof window === 'undefined') { // Non-browser environment without unref - terminate instead worker.terminate(); diff --git a/packages/flac-encoder/package.json b/packages/flac-encoder/package.json index c97ae6e..43e3680 100644 --- a/packages/flac-encoder/package.json +++ b/packages/flac-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/flac-encoder", "author": "Vanilagy", - "version": "1.50.8", + "version": "1.50.9", "description": "FLAC encoder extension for Mediabunny, based on libFLAC.", "main": "./dist/bundles/mediabunny-flac-encoder.mjs", "module": "./dist/bundles/mediabunny-flac-encoder.mjs", diff --git a/packages/flac-encoder/src/encode.worker.ts b/packages/flac-encoder/src/encode.worker.ts index fe881d3..cfd6484 100644 --- a/packages/flac-encoder/src/encode.worker.ts +++ b/packages/flac-encoder/src/encode.worker.ts @@ -192,3 +192,7 @@ if (parentPort) { } else { self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand })); } + +// Prevents the worker for being randomly closed by Firefox +// https://github.com/Vanilagy/mediabunny/issues/435 +setInterval(() => {}, 1000); diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index cf59128..0bc5480 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.50.8", + "version": "1.50.9", "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/packages/mp3-encoder/src/encode.worker.ts b/packages/mp3-encoder/src/encode.worker.ts index ee24db8..715816d 100644 --- a/packages/mp3-encoder/src/encode.worker.ts +++ b/packages/mp3-encoder/src/encode.worker.ts @@ -219,3 +219,7 @@ if (parentPort) { } else { self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand })); } + +// Prevents the worker for being randomly closed by Firefox +// https://github.com/Vanilagy/mediabunny/issues/435 +setInterval(() => {}, 1000); diff --git a/packages/prores/package.json b/packages/prores/package.json index d28c9de..7c0477a 100644 --- a/packages/prores/package.json +++ b/packages/prores/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/prores", "author": "Vanilagy", - "version": "1.50.8", + "version": "1.50.9", "description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.", "main": "./dist/bundles/mediabunny-prores.mjs", "module": "./dist/bundles/mediabunny-prores.mjs", diff --git a/packages/server/package.json b/packages/server/package.json index 944adc1..0f33ccc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/server", "author": "Vanilagy", - "version": "1.50.8", + "version": "1.50.9", "description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.", "main": "./dist/bundles/mediabunny-server.cjs", "module": "./dist/bundles/mediabunny-server.mjs", @@ -35,7 +35,7 @@ }, "dependencies": { "node-av": "^6.0.0", - "@mediabunny/prores": "^1.50.8" + "@mediabunny/prores": "^1.50.9" }, "peerDependencies": { "mediabunny": "^1.45.0" diff --git a/src/codec-data.ts b/src/codec-data.ts index 83a040d..8319af3 100644 --- a/src/codec-data.ts +++ b/src/codec-data.ts @@ -2207,9 +2207,22 @@ const OPUS_FRAME_DURATION_TABLE = [ export const parseOpusTocByte = (packet: Uint8Array) => { const config = packet[0]! >> 3; + const code = packet[0]! & 0b11; + + // A packet may pack more than one frame, in which case its duration is the frame duration times the number of + // frames it carries. See https://datatracker.ietf.org/doc/html/rfc6716, section 3.2. + let frameCount: number; + if (code === 0) { + frameCount = 1; + } else if (code === 1 || code === 2) { + frameCount = 2; + } else { + // Code 3: the frame count sits in the six low bits of the frame count byte + frameCount = packet[1]! & 0b111111; + } return { - durationInSamples: OPUS_FRAME_DURATION_TABLE[config]!, + durationInSamples: OPUS_FRAME_DURATION_TABLE[config]! * frameCount, }; }; diff --git a/src/conversion.ts b/src/conversion.ts index bd15ba7..252e8cb 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -1321,7 +1321,7 @@ export class Conversion { await tempOutput.start(); const sink = new VideoSampleSink(track); - const firstSample = await sink.getSample(firstTimestamp); // Let's just use the first sample + using firstSample = await sink.getSample(firstTimestamp); // Let's just use the first sample if (firstSample) { try { @@ -1375,9 +1375,8 @@ export class Conversion { const sink = new VideoSampleSink(track); - for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) { + for await (using sample of sink.samples(this._startTimestamp, this._endTimestamp)) { if (this._canceled) { - sample.close(); return; } @@ -1386,14 +1385,13 @@ export class Conversion { this._reportProgress(outputTrackId, sample.timestamp + sample.duration); await source.add(sample); + sample.close(); if (lastSampleTimestamp !== null) { if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) { await this._synchronizer.wait(lastSampleTimestamp); } } - - sample.close(); } source.close(); @@ -1595,9 +1593,8 @@ export class Conversion { await this._started; const sink = new AudioSampleSink(track); - for await (let sample of sink.samples(this._startTimestamp, this._endTimestamp)) { + for await (using sample of sink.samples(this._startTimestamp, this._endTimestamp)) { if (this._canceled) { - sample.close(); return; } @@ -1612,7 +1609,7 @@ export class Conversion { data.fill(2 ** 7); // Fill it with the silent value } - const silentSample = new AudioSample({ + using silentSample = new AudioSample({ data, // Use the same format the decoder is spitting out. This avoids feeding changing sample // formats to the audio encoder. @@ -1636,22 +1633,28 @@ export class Conversion { endFrame = Math.round((this._endTimestamp - sample.timestamp) * sample.sampleRate); } + // Can't assign to "using" identifiers so we gotta do this + let finalSampleLet: AudioSample; if (startFrame > 0 || endFrame < sample.numberOfFrames) { // Trim the sample if it sticks out of the trim region on either end const trimmedSample = sample.trim(startFrame, endFrame); sample.close(); - sample = trimmedSample; + finalSampleLet = trimmedSample; - if (sample.numberOfFrames === 0) { - sample.close(); + if (trimmedSample.numberOfFrames === 0) { + trimmedSample.close(); continue; } + } else { + finalSampleLet = sample; } - // Offset the timestamp as needed - sample.setTimestamp(sample.timestamp - this._startTimestamp); + using finalSample = finalSampleLet; - await this._registerAudioSample(sample, source, outputTrackId, () => lastSampleTimestamp); + // Offset the timestamp as needed + finalSample.setTimestamp(finalSample.timestamp - this._startTimestamp); + + await this._registerAudioSample(finalSample, source, outputTrackId, () => lastSampleTimestamp); } source.close(); diff --git a/src/isobmff/isobmff-muxer.ts b/src/isobmff/isobmff-muxer.ts index c9baf86..bf3a89e 100644 --- a/src/isobmff/isobmff-muxer.ts +++ b/src/isobmff/isobmff-muxer.ts @@ -206,6 +206,8 @@ export class IsobmffMuxer extends Muxer { this.isCmaf = format instanceof CmafOutputFormat; this.minimumFragmentDuration = format._options.minimumFragmentDuration ?? (format instanceof CmafOutputFormat ? Infinity : 1); + + this.auxWriter.start(); } async start() { diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index 79e2e93..c15ec98 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -1168,7 +1168,10 @@ export class MatroskaMuxer extends Muxer { const msDuration = Math.round(1000 * chunk.duration); - if (!chunk.additions) { + // Subtitle cues need an explicit BlockDuration (a SimpleBlock has none) + const needsBlockGroup = !!chunk.additions || trackData.type === 'subtitle'; + + if (!needsBlockGroup) { // No additions, we can write out a SimpleBlock view.setUint8(3, Number(chunk.type === 'key') << 7); // Flags (keyframe flag only present for SimpleBlock) diff --git a/src/output-format.ts b/src/output-format.ts index f759896..4e64946 100644 --- a/src/output-format.ts +++ b/src/output-format.ts @@ -799,7 +799,7 @@ export class WavOutputFormat extends OutputFormat { getSupportedCodecs(): MediaCodec[] { return [ ...PCM_AUDIO_CODECS.filter(codec => - ['pcm-s16', 'pcm-s24', 'pcm-s32', 'pcm-f32', 'pcm-u8', 'ulaw', 'alaw'].includes(codec), + ['pcm-s16', 'pcm-s24', 'pcm-s32', 'pcm-f32', 'pcm-f64', 'pcm-u8', 'ulaw', 'alaw'].includes(codec), ), ]; } diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts index 1324c6f..ddf750a 100644 --- a/src/wave/wave-demuxer.ts +++ b/src/wave/wave-demuxer.ts @@ -173,6 +173,28 @@ export class WaveDemuxer extends Demuxer { bitsPerSample = 8; } + if ( + formatTag !== WaveFormat.PCM + && formatTag !== WaveFormat.IEEE_FLOAT + && formatTag !== WaveFormat.ALAW + && formatTag !== WaveFormat.MULAW + ) { + throw new Error( + `Unsupported WAVE codec (format tag ${formatTag}). Only integer/float PCM, A-law, and μ-law are` + + ` supported.`, + ); + } + if (formatTag === WaveFormat.PCM && ![8, 16, 24, 32].includes(bitsPerSample)) { + throw new Error( + `Unsupported WAVE PCM bit depth (${bitsPerSample}). Only 8, 16, 24, and 32 bits are supported.`, + ); + } + if (formatTag === WaveFormat.IEEE_FLOAT && ![32, 64].includes(bitsPerSample)) { + throw new Error( + `Unsupported WAVE float bit depth (${bitsPerSample}). Only 32 and 64 bits are supported.`, + ); + } + this.audioInfo = { format: formatTag, numberOfChannels: numChannels, @@ -322,10 +344,12 @@ export class WaveDemuxer extends Demuxer { if (this.audioInfo.format === WaveFormat.IEEE_FLOAT) { if (this.audioInfo.sampleSizeInBytes === 4) { return 'pcm-f32'; + } else if (this.audioInfo.sampleSizeInBytes === 8) { + return 'pcm-f64'; } } - return null; + assert(false); } async getMimeType() { diff --git a/test/browser/ogg-muxer.test.ts b/test/browser/ogg-muxer.test.ts index ea982e1..215dc7d 100644 --- a/test/browser/ogg-muxer.test.ts +++ b/test/browser/ogg-muxer.test.ts @@ -1,8 +1,14 @@ 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'; +import { BufferTarget, NullTarget } from '../../src/target.js'; +import { AudioBufferSource, EncodedAudioPacketSource } from '../../src/media-source.js'; +import { EncodedPacket } from '../../src/packet.js'; +import { assert } from '../../src/misc.js'; +import { Input } from '../../src/input.js'; +import { BufferSource } from '../../src/source.js'; +import { ALL_FORMATS, OggInputFormat } from '../../src/input-format.js'; +import { EncodedPacketSink } from '../../src/media-sink.js'; test('maximumPageDuration option', async () => { const sampleRate = 48000; @@ -55,3 +61,82 @@ test('maximumPageDuration option', async () => { expect(pageCountWithoutOption).toBe(3); expect(pageCountWithOption).toBe(23); // It created more pages }); + +test('Multi-frame Opus packets', async () => { + const SAMPLE_RATE = 48000; + const SAMPLES_PER_FRAME = 960; // 20 ms at 48 kHz + + const createOpusHead = () => { + const bytes = new Uint8Array(19); + const view = new DataView(bytes.buffer); + + bytes.set([0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64], 0); // 'OpusHead' + bytes[8] = 1; // Version + bytes[9] = 1; // Channel count + view.setUint16(10, 312, true); // Pre-skip + view.setUint32(12, SAMPLE_RATE, true); // Input sample rate + view.setInt16(16, 0, true); // Output gain + bytes[18] = 0; // Channel mapping family + + return bytes; + }; + + const createOpusPacket = (frameCount: number) => { + const data = new Uint8Array(2 + 3 * frameCount); + + data[0] = (31 << 3) | 0b11; // TOC byte: config 31 (CELT fullband, 20 ms), code 3 + data[1] = frameCount; // CBR, no padding, `frameCount` frames + + return data; + }; + + const framesPerPacket = 3; + const packetCount = 10; + const packetDuration = (framesPerPacket * SAMPLES_PER_FRAME) / SAMPLE_RATE; + + const output = new Output({ + format: new OggOutputFormat(), + target: new BufferTarget(), + }); + + const audioSource = new EncodedAudioPacketSource('opus'); + output.addAudioTrack(audioSource); + + await output.start(); + + for (let i = 0; i < packetCount; i++) { + await audioSource.add( + new EncodedPacket( + createOpusPacket(framesPerPacket), + 'key', + i * packetDuration, + packetDuration, + ), + { + decoderConfig: { + codec: 'opus', + numberOfChannels: 1, + sampleRate: SAMPLE_RATE, + description: createOpusHead(), + }, + }, + ); + } + + audioSource.close(); + await output.finalize(); + + assert(output.target.buffer); + + const input = new Input({ + source: new BufferSource(output.target.buffer), + formats: ALL_FORMATS, + }); + + expect(await input.getFormat()).toBeInstanceOf(OggInputFormat); + + const sink = new EncodedPacketSink((await input.getPrimaryAudioTrack())!); + const packet = await sink.getFirstPacket(); + + expect(packet?.duration).toBe(packetDuration); +}); diff --git a/test/node/webvtt.test.ts b/test/node/webvtt.test.ts new file mode 100644 index 0000000..5f3f72c --- /dev/null +++ b/test/node/webvtt.test.ts @@ -0,0 +1,57 @@ +import { test } from 'vitest'; +import { Output } from '../../src/output.js'; +import { MkvOutputFormat, Mp4OutputFormat } from '../../src/output-format.js'; +import { BufferTarget } from '../../src/target.js'; +import { TextSubtitleSource } from '../../src/media-source.js'; + +test('ISOBMFF muxing', async () => { + const output = new Output({ + format: new Mp4OutputFormat(), + target: new BufferTarget(), + }); + + const source = new TextSubtitleSource('webvtt'); + output.addSubtitleTrack(source); + + await output.start(); + + await source.add(`WEBVTT + +00:00.000 --> 00:00.900 +Hildy! + +00:01.000 --> 00:01.400 +How are you? + +00:01.500 --> 00:02.900 +Tell me, is the lord of the universe in? +`); + + await output.finalize(); +}); + +test('Matroska muxing', async () => { + const output = new Output({ + format: new MkvOutputFormat(), + target: new BufferTarget(), + }); + + const source = new TextSubtitleSource('webvtt'); + output.addSubtitleTrack(source); + + await output.start(); + + await source.add(`WEBVTT + +00:00.000 --> 00:00.900 +Hildy! + +00:01.000 --> 00:01.400 +How are you? + +00:01.500 --> 00:02.900 +Tell me, is the lord of the universe in? +`); + + await output.finalize(); +}); diff --git a/tsconfig.vite.json b/tsconfig.vite.json index b8dad8d..eb5ae43 100644 --- a/tsconfig.vite.json +++ b/tsconfig.vite.json @@ -19,6 +19,8 @@ "./examples/**/*.ts" ], "references": [ - { "path": "./src" } - ] -} \ No newline at end of file + { "path": "./src" }, + { "path": "./packages/ac3" }, + { "path": "./packages/prores" } + ] +} diff --git a/vite.config.ts b/vite.config.ts index df28f91..576ccc9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,6 +21,8 @@ export default defineConfig({ resolve: { alias: { 'mediabunny': path.resolve(__dirname, './dist/bundles/mediabunny.mjs'), + '@mediabunny/ac3': + path.resolve(__dirname, './packages/ac3/dist/bundles/mediabunny-ac3.mjs'), '@mediabunny/aac-encoder': path.resolve(__dirname, './packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.mjs'), '@mediabunny/flac-encoder':