mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Merge main into release for tag v1.50.9
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Generated
+69
-9
@@ -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": {
|
||||
|
||||
+5
-5
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
+14
-1
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+17
-14
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
+5
-3
@@ -19,6 +19,8 @@
|
||||
"./examples/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "./src" }
|
||||
]
|
||||
}
|
||||
{ "path": "./src" },
|
||||
{ "path": "./packages/ac3" },
|
||||
{ "path": "./packages/prores" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user