mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 22:03:49 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a4e86698e | ||
|
|
317193452c | ||
|
|
4928779ba1 | ||
|
|
703858fc4e | ||
|
|
df5efa0442 | ||
|
|
ddc826e6e3 | ||
|
|
2876197631 | ||
|
|
e3906a3e36 |
@@ -34,15 +34,15 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
|
||||
<a href="https://kino.ai/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/kino.jpg" width="60" height="60" alt="Kino">
|
||||
</a>
|
||||
|
||||
<a href="https://screen.studio/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/screen-studio.webp" width="60" height="60" alt="Screen Studio">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Silver sponsors
|
||||
|
||||
<div align="center">
|
||||
<a href="https://pqina.nl/pintura/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/pintura-labs.png" width="50" height="50" alt="Pintura Labs">
|
||||
</a>
|
||||
|
||||
<a href="https://ponder.ai/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/ponder.png" width="50" height="50" alt="Ponder">
|
||||
</a>
|
||||
|
||||
+6
-4
@@ -26,7 +26,7 @@
|
||||
chunked: true,
|
||||
chunkSize: 2**20
|
||||
});
|
||||
const outputFormat = new Mediabunny.Mp4OutputFormat({});
|
||||
const outputFormat = new Mediabunny.MpegTsOutputFormat({});
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Cancel';
|
||||
@@ -58,7 +58,8 @@
|
||||
}),
|
||||
output,
|
||||
audio: (_, n) => ({
|
||||
codec: 'eac3',
|
||||
//codec: 'eac3',
|
||||
//discard: true,
|
||||
//discard: n > 1,
|
||||
//codec: 'opus',
|
||||
//codec: 'opus',
|
||||
@@ -103,7 +104,8 @@
|
||||
},
|
||||
*/
|
||||
video: () => ({
|
||||
width: 320,
|
||||
forceTranscode: true,
|
||||
width: 1280,
|
||||
//forceTranscode: true,
|
||||
//allowRotationMetadata: false,
|
||||
//width: 720,
|
||||
@@ -186,7 +188,7 @@
|
||||
},
|
||||
trim: {
|
||||
//start: 0,
|
||||
end: 10
|
||||
//end: 10
|
||||
},
|
||||
});
|
||||
console.log(conversion);
|
||||
|
||||
+2
-1
@@ -95,9 +95,9 @@ const sponsors = {
|
||||
{ image: '/sponsors/gling.svg', name: 'Gling AI', url: 'https://www.gling.ai/' },
|
||||
{ image: '/sponsors/diffusionstudio.png', name: 'Diffusion Studio', url: 'https://diffusion.studio/' },
|
||||
{ image: '/sponsors/kino.jpg', name: 'Kino', url: 'https://kino.ai/' },
|
||||
{ image: '/sponsors/screen-studio.webp', name: 'Screen Studio', url: 'https://screen.studio/' },
|
||||
],
|
||||
silver: [
|
||||
{ image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' },
|
||||
{ image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' },
|
||||
],
|
||||
bronze: [
|
||||
@@ -107,6 +107,7 @@ const sponsors = {
|
||||
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
|
||||
],
|
||||
individual: [
|
||||
{ image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/197597', name: 'Christopher Chedeau', url: 'https://github.com/vjeux' },
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -143,7 +143,7 @@ loadUrlButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
@@ -680,7 +680,7 @@ loadUrlButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
@@ -221,7 +221,7 @@ loadUrlButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
@@ -137,7 +137,7 @@ loadUrlButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
Generated
+7
-7
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.34.1",
|
||||
"version": "1.34.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.34.1",
|
||||
"version": "1.34.4",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -7743,9 +7743,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.34.0",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.34.0.tgz",
|
||||
"integrity": "sha512-mjn/7QVEPbPEakuNKD8OIPDhaoe2NlgWVyyqftuLVWvsgLol7g2MQzXWCjx523CP8Y9jE2H09hbYCuTwSI7pSQ==",
|
||||
"version": "1.34.3",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.34.3.tgz",
|
||||
"integrity": "sha512-Jb0x3jyvadGVpo8aZSn53NZduPbPdbx9foQsAGOmKBz7Hai9B38Kf+7eXbxXFcXoXxcTyNaZeDhoA0agoBQldA==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12069,7 +12069,7 @@
|
||||
},
|
||||
"packages/ac3": {
|
||||
"name": "@mediabunny/ac3",
|
||||
"version": "0.1.1",
|
||||
"version": "1.34.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12084,7 +12084,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.34.1",
|
||||
"version": "1.34.4",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.34.1",
|
||||
"version": "1.34.4",
|
||||
"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/ac3",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.34.1",
|
||||
"version": "1.34.4",
|
||||
"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",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
EncodedPacket,
|
||||
registerDecoder,
|
||||
} from 'mediabunny';
|
||||
import { sendCommand } from './worker-client';
|
||||
import { sendCommand, refWorker, unrefWorker } from './worker-client';
|
||||
|
||||
class Ac3Decoder extends CustomAudioDecoder {
|
||||
private ctx = 0;
|
||||
@@ -23,6 +23,8 @@ class Ac3Decoder extends CustomAudioDecoder {
|
||||
}
|
||||
|
||||
async init() {
|
||||
await refWorker();
|
||||
|
||||
const result = await sendCommand({
|
||||
type: 'init-decoder',
|
||||
data: { codec: this.codec },
|
||||
@@ -53,8 +55,9 @@ class Ac3Decoder extends CustomAudioDecoder {
|
||||
await sendCommand({ type: 'flush-decoder', data: { ctx: this.ctx } });
|
||||
}
|
||||
|
||||
close() {
|
||||
async close() {
|
||||
void sendCommand({ type: 'close-decoder', data: { ctx: this.ctx } });
|
||||
await unrefWorker();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
EncodedPacket,
|
||||
registerEncoder,
|
||||
} from 'mediabunny';
|
||||
import { sendCommand } from './worker-client';
|
||||
import { sendCommand, refWorker, unrefWorker } from './worker-client';
|
||||
import { assert } from './shared';
|
||||
import { AC3_SAMPLE_RATES, EAC3_REDUCED_SAMPLE_RATES } from '../../../shared/ac3-misc';
|
||||
|
||||
@@ -38,11 +38,14 @@ class Ac3Encoder extends CustomAudioEncoder {
|
||||
return (codec === 'ac3' || codec === 'eac3')
|
||||
&& config.numberOfChannels >= 1
|
||||
&& config.numberOfChannels <= 8
|
||||
&& sampleRates.includes(config.sampleRate);
|
||||
&& sampleRates.includes(config.sampleRate)
|
||||
&& config.bitrate !== undefined;
|
||||
}
|
||||
|
||||
async init() {
|
||||
assert(this.config.bitrate);
|
||||
await refWorker();
|
||||
|
||||
assert(this.config.bitrate !== undefined);
|
||||
this.sampleRate = this.config.sampleRate;
|
||||
this.numberOfChannels = this.config.numberOfChannels;
|
||||
|
||||
@@ -130,6 +133,7 @@ class Ac3Encoder extends CustomAudioEncoder {
|
||||
|
||||
close() {
|
||||
void sendCommand({ type: 'close-encoder', data: { ctx: this.ctx } });
|
||||
void unrefWorker();
|
||||
}
|
||||
|
||||
private async encodeOneFrame() {
|
||||
|
||||
@@ -10,13 +10,51 @@ import { assert, type WorkerCommand, type WorkerResponse, type WorkerResponseDat
|
||||
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
|
||||
import createWorker from './codec.worker';
|
||||
|
||||
let workerPromise: Promise<Worker> | null;
|
||||
type ExtendedWorker = Worker & {
|
||||
ref?: () => void;
|
||||
unref?: () => void;
|
||||
};
|
||||
|
||||
let workerPromise: Promise<ExtendedWorker> | null;
|
||||
let nextMessageId = 0;
|
||||
const pendingMessages = new Map<number, {
|
||||
resolve: (value: WorkerResponseData) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}>();
|
||||
|
||||
let refCount = 0;
|
||||
let keepAliveInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
export const refWorker = async () => {
|
||||
refCount++;
|
||||
if (refCount === 1) {
|
||||
keepAliveInterval = setInterval(() => {}, 2 ** 31 - 1);
|
||||
const worker = await ensureWorker();
|
||||
worker.ref?.();
|
||||
}
|
||||
};
|
||||
|
||||
export const unrefWorker = async () => {
|
||||
refCount--;
|
||||
if (refCount === 0) {
|
||||
if (keepAliveInterval !== null) {
|
||||
clearInterval(keepAliveInterval);
|
||||
keepAliveInterval = null;
|
||||
}
|
||||
|
||||
const worker = await workerPromise;
|
||||
if (worker) {
|
||||
if (worker.unref) {
|
||||
worker.unref(); // If we don't do this, then the Node process never terminates by itself
|
||||
} else if (typeof window === 'undefined') {
|
||||
// Non-browser environment without unref - terminate instead
|
||||
worker.terminate();
|
||||
workerPromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const sendCommand = async <T extends string>(
|
||||
command: WorkerCommand & { type: T },
|
||||
transferables?: Transferable[],
|
||||
@@ -41,7 +79,8 @@ export const sendCommand = async <T extends string>(
|
||||
const ensureWorker = () => {
|
||||
return workerPromise ??= (async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const worker = (await createWorker()) as Worker;
|
||||
const worker = (await createWorker()) as ExtendedWorker;
|
||||
worker.unref?.(); // Start unreffed
|
||||
|
||||
const onMessage = (data: WorkerResponse) => {
|
||||
const pending = pendingMessages.get(data.id);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.34.1",
|
||||
"version": "1.34.4",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Worker() {
|
||||
const inlineWorkerFunctionCode = `
|
||||
export default async function inlineWorker(scriptText) {
|
||||
if (typeof Worker !== 'undefined' && typeof Bun === 'undefined') {
|
||||
// Browser, Deno
|
||||
// Browser, Deno (Deno can't do dynamic import of worker_threads)
|
||||
|
||||
const blob = new Blob([scriptText], { type: "text/javascript" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -39,10 +39,10 @@ export default async function inlineWorker(scriptText) {
|
||||
// Node, Bun (Bun's Worker is flaky, worker_threads works much better)
|
||||
|
||||
let Worker;
|
||||
const workerModule = 'node:worker_threads';
|
||||
try {
|
||||
Worker = (await import('worker_threads')).Worker;
|
||||
Worker = (await import(workerModule)).Worker;
|
||||
} catch {
|
||||
const workerModule = 'worker_threads';
|
||||
Worker = require(workerModule).Worker;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ import { FileSlice, readBytes, Reader, readU16Be, readU32Be, readU8 } from '../r
|
||||
import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc';
|
||||
import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc';
|
||||
|
||||
// Resources:
|
||||
// ISO/IEC 13818-1
|
||||
|
||||
type ElementaryStream = {
|
||||
demuxer: MpegTsDemuxer;
|
||||
pid: number;
|
||||
|
||||
@@ -25,12 +25,15 @@ import {
|
||||
} from '../codec-data';
|
||||
import { assert, Bitstream, promiseWithResolvers, setUint24, toDataView, toUint8Array } from '../misc';
|
||||
import { Muxer } from '../muxer';
|
||||
import { Output, OutputAudioTrack, OutputVideoTrack } from '../output';
|
||||
import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from '../output';
|
||||
import { MpegTsOutputFormat } from '../output-format';
|
||||
import { EncodedPacket } from '../packet';
|
||||
import { Writer } from '../writer';
|
||||
import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc';
|
||||
|
||||
// Resources:
|
||||
// ISO/IEC 13818-1
|
||||
|
||||
const PAT_PID = 0x0000;
|
||||
const PMT_PID = 0x1000;
|
||||
const FIRST_TRACK_PID = 0x0100;
|
||||
@@ -47,6 +50,7 @@ type MpegTsTrackData = {
|
||||
streamType: MpegTsStreamType;
|
||||
streamId: number;
|
||||
codecString: string;
|
||||
timestampProcessingQueue: QueuedPacket[];
|
||||
packetQueue: QueuedPacket[];
|
||||
inputIsAnnexB: boolean | null;
|
||||
inputIsAdts: boolean | null;
|
||||
@@ -59,7 +63,8 @@ type MpegTsTrackData = {
|
||||
|
||||
type QueuedPacket = {
|
||||
data: Uint8Array;
|
||||
timestamp: number;
|
||||
presentationTimestamp: number;
|
||||
decodeTimestamp: number | null;
|
||||
isKeyframe: boolean;
|
||||
};
|
||||
|
||||
@@ -77,9 +82,6 @@ export class MpegTsMuxer extends Muxer {
|
||||
private videoTrackIndex = 0;
|
||||
private audioTrackIndex = 0;
|
||||
|
||||
private pesHeaderBuffer = new Uint8Array(14);
|
||||
private pesHeaderView = toDataView(this.pesHeaderBuffer);
|
||||
private ptsBitstream = new Bitstream(this.pesHeaderBuffer.subarray(9, 14));
|
||||
private adaptationFieldBuffer = new Uint8Array(184);
|
||||
private payloadBuffer = new Uint8Array(184);
|
||||
|
||||
@@ -124,6 +126,7 @@ export class MpegTsMuxer extends Muxer {
|
||||
streamType,
|
||||
streamId,
|
||||
codecString: meta.decoderConfig.codec,
|
||||
timestampProcessingQueue: [],
|
||||
packetQueue: [],
|
||||
inputIsAnnexB: null,
|
||||
inputIsAdts: null,
|
||||
@@ -188,6 +191,7 @@ export class MpegTsMuxer extends Muxer {
|
||||
streamType,
|
||||
streamId,
|
||||
codecString: meta.decoderConfig.codec,
|
||||
timestampProcessingQueue: [],
|
||||
packetQueue: [],
|
||||
inputIsAnnexB: null,
|
||||
inputIsAdts: null,
|
||||
@@ -225,13 +229,16 @@ export class MpegTsMuxer extends Muxer {
|
||||
|
||||
const preparedData = this.prepareVideoPacket(trackData, packet, meta);
|
||||
|
||||
trackData.packetQueue.push({
|
||||
if (packet.type === 'key') {
|
||||
await this.flushTimestampQueue(trackData);
|
||||
}
|
||||
|
||||
trackData.timestampProcessingQueue.push({
|
||||
data: preparedData,
|
||||
timestamp,
|
||||
presentationTimestamp: timestamp,
|
||||
decodeTimestamp: null,
|
||||
isKeyframe: packet.type === 'key',
|
||||
});
|
||||
|
||||
await this.interleavePackets();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
@@ -255,13 +262,16 @@ export class MpegTsMuxer extends Muxer {
|
||||
|
||||
const preparedData = this.prepareAudioPacket(trackData, packet, meta);
|
||||
|
||||
trackData.packetQueue.push({
|
||||
if (packet.type === 'key') {
|
||||
await this.flushTimestampQueue(trackData);
|
||||
}
|
||||
|
||||
trackData.timestampProcessingQueue.push({
|
||||
data: preparedData,
|
||||
timestamp,
|
||||
presentationTimestamp: timestamp,
|
||||
decodeTimestamp: null,
|
||||
isKeyframe: packet.type === 'key',
|
||||
});
|
||||
|
||||
await this.interleavePackets();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
@@ -444,6 +454,28 @@ export class MpegTsMuxer extends Muxer {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async flushTimestampQueue(trackData: MpegTsTrackData, alsoInterleave = true) {
|
||||
if (trackData.timestampProcessingQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedTimestamps = trackData.timestampProcessingQueue
|
||||
.map(packet => packet.presentationTimestamp)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) {
|
||||
const queuedPacket = trackData.timestampProcessingQueue[i]!;
|
||||
queuedPacket.decodeTimestamp = sortedTimestamps[i]!;
|
||||
trackData.packetQueue.push(queuedPacket);
|
||||
}
|
||||
|
||||
trackData.timestampProcessingQueue.length = 0;
|
||||
|
||||
if (alsoInterleave) {
|
||||
await this.interleavePackets();
|
||||
}
|
||||
}
|
||||
|
||||
private async interleavePackets(isFinalCall = false) {
|
||||
if (!this.tablesWritten) {
|
||||
if (!this.allTracksAreKnown() && !isFinalCall) {
|
||||
@@ -469,10 +501,10 @@ export class MpegTsMuxer extends Muxer {
|
||||
|
||||
if (
|
||||
trackData.packetQueue.length > 0
|
||||
&& trackData.packetQueue[0]!.timestamp < minTimestamp
|
||||
&& trackData.packetQueue[0]!.presentationTimestamp < minTimestamp
|
||||
) {
|
||||
trackWithMinTimestamp = trackData;
|
||||
minTimestamp = trackData.packetQueue[0]!.timestamp;
|
||||
minTimestamp = trackData.packetQueue[0]!.presentationTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,10 +558,14 @@ export class MpegTsMuxer extends Muxer {
|
||||
}
|
||||
|
||||
private writePesPacket(trackData: MpegTsTrackData, queuedPacket: QueuedPacket) {
|
||||
const pesView = this.pesHeaderView;
|
||||
const includeDts = trackData.track.type === 'video';
|
||||
const headerDataLength = includeDts ? 10 : 5;
|
||||
const pesHeaderBuffer = new Uint8Array(9 + headerDataLength);
|
||||
const pesView = toDataView(pesHeaderBuffer);
|
||||
const ptsDtsBitstream = new Bitstream(pesHeaderBuffer.subarray(9));
|
||||
|
||||
setUint24(pesView, 0, 0x000001, false); // packet_start_code_prefix
|
||||
this.pesHeaderBuffer[3] = trackData.streamId; // stream_id
|
||||
pesHeaderBuffer[3] = trackData.streamId; // stream_id
|
||||
|
||||
const pesPacketLength = trackData.track.type === 'video'
|
||||
? 0 // Unbounded
|
||||
@@ -539,20 +575,33 @@ export class MpegTsMuxer extends Muxer {
|
||||
// '10' marker, PES_scrambling_control=0, PES_priority=0,
|
||||
// data_alignment_indicator=1, copyright=0, original_or_copy=0
|
||||
pesView.setUint8(6, 0x84);
|
||||
pesView.setUint8(7, 0x80); // PTS_DTS_flags=10 (PTS only), other flags=0
|
||||
pesView.setUint8(8, 5); // PES_header_data_length (5 bytes for PTS)
|
||||
pesView.setUint8(7, includeDts ? 0xC0 : 0x80); // PTS_DTS_flags, other flags=0
|
||||
pesView.setUint8(8, headerDataLength); // PES_header_data_length
|
||||
|
||||
const pts = Math.round(queuedPacket.timestamp * TIMESCALE);
|
||||
this.ptsBitstream.pos = 0;
|
||||
this.ptsBitstream.writeBits(4, 0b0010); // marker
|
||||
this.ptsBitstream.writeBits(3, (pts >>> 30) & 0x7); // PTS[32:30]
|
||||
this.ptsBitstream.writeBits(1, 1); // marker_bit
|
||||
this.ptsBitstream.writeBits(15, (pts >>> 15) & 0x7FFF); // PTS[29:15]
|
||||
this.ptsBitstream.writeBits(1, 1); // marker_bit
|
||||
this.ptsBitstream.writeBits(15, pts & 0x7FFF); // PTS[14:0]
|
||||
this.ptsBitstream.writeBits(1, 1); // marker_bit
|
||||
const pts = Math.round(queuedPacket.presentationTimestamp * TIMESCALE);
|
||||
ptsDtsBitstream.pos = 0;
|
||||
ptsDtsBitstream.writeBits(4, includeDts ? 0b0011 : 0b0010); // marker
|
||||
ptsDtsBitstream.writeBits(3, (pts >>> 30) & 0x7); // PTS[32:30]
|
||||
ptsDtsBitstream.writeBits(1, 1); // marker_bit
|
||||
ptsDtsBitstream.writeBits(15, (pts >>> 15) & 0x7FFF); // PTS[29:15]
|
||||
ptsDtsBitstream.writeBits(1, 1); // marker_bit
|
||||
ptsDtsBitstream.writeBits(15, pts & 0x7FFF); // PTS[14:0]
|
||||
ptsDtsBitstream.writeBits(1, 1); // marker_bit
|
||||
|
||||
const totalLength = this.pesHeaderBuffer.length + queuedPacket.data.length;
|
||||
if (includeDts) {
|
||||
assert(queuedPacket.decodeTimestamp !== null);
|
||||
|
||||
const dts = Math.round(queuedPacket.decodeTimestamp * TIMESCALE);
|
||||
ptsDtsBitstream.writeBits(4, 0b0001);
|
||||
ptsDtsBitstream.writeBits(3, (dts >>> 30) & 0x7); // DTS[32:30]
|
||||
ptsDtsBitstream.writeBits(1, 1); // marker_bit
|
||||
ptsDtsBitstream.writeBits(15, (dts >>> 15) & 0x7FFF); // DTS[29:15]
|
||||
ptsDtsBitstream.writeBits(1, 1); // marker_bit
|
||||
ptsDtsBitstream.writeBits(15, dts & 0x7FFF); // DTS[14:0]
|
||||
ptsDtsBitstream.writeBits(1, 1); // marker_bit
|
||||
}
|
||||
|
||||
const totalLength = pesHeaderBuffer.length + queuedPacket.data.length;
|
||||
let offset = 0;
|
||||
let isFirstTsPacket = true;
|
||||
|
||||
@@ -593,13 +642,13 @@ export class MpegTsMuxer extends Muxer {
|
||||
const payload = this.payloadBuffer.subarray(0, payloadSize);
|
||||
|
||||
let payloadOffset = 0;
|
||||
if (offset < this.pesHeaderBuffer.length) {
|
||||
const headerBytes = Math.min(this.pesHeaderBuffer.length - offset, payloadSize);
|
||||
payload.set(this.pesHeaderBuffer.subarray(offset, offset + headerBytes), 0);
|
||||
if (offset < pesHeaderBuffer.length) {
|
||||
const headerBytes = Math.min(pesHeaderBuffer.length - offset, payloadSize);
|
||||
payload.set(pesHeaderBuffer.subarray(offset, offset + headerBytes), 0);
|
||||
payloadOffset = headerBytes;
|
||||
}
|
||||
|
||||
const dataStart = Math.max(0, offset - this.pesHeaderBuffer.length);
|
||||
const dataStart = Math.max(0, offset - pesHeaderBuffer.length);
|
||||
const dataEnd = dataStart + (payloadSize - payloadOffset);
|
||||
if (payloadOffset < payloadSize) {
|
||||
payload.set(queuedPacket.data.subarray(dataStart, dataEnd), payloadOffset);
|
||||
@@ -658,13 +707,18 @@ export class MpegTsMuxer extends Muxer {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
override async onTrackClose() {
|
||||
override async onTrackClose(track: OutputTrack) {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
if (this.allTracksAreKnown()) {
|
||||
this.allTracksKnown.resolve();
|
||||
}
|
||||
|
||||
const trackData = this.trackDatas.find(x => x.track === track);
|
||||
if (trackData) {
|
||||
await this.flushTimestampQueue(trackData, false);
|
||||
}
|
||||
|
||||
await this.interleavePackets();
|
||||
|
||||
release();
|
||||
@@ -675,6 +729,10 @@ export class MpegTsMuxer extends Muxer {
|
||||
|
||||
this.allTracksKnown.resolve();
|
||||
|
||||
for (const trackData of this.trackDatas) {
|
||||
await this.flushTimestampQueue(trackData, false);
|
||||
}
|
||||
|
||||
await this.interleavePackets(true);
|
||||
|
||||
release();
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||
import { assert, UNDETERMINED_LANGUAGE } from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { readAscii, readBytes, Reader, readU16, readU32, readU64 } from '../reader';
|
||||
import { parseId3V2Tag, readId3V2Header } from '../id3';
|
||||
import { ID3_V2_HEADER_SIZE, parseId3V2Tag, readId3V2Header } from '../id3';
|
||||
|
||||
export enum WaveFormat {
|
||||
PCM = 0x0001,
|
||||
@@ -286,10 +286,15 @@ export class WaveDemuxer extends Demuxer {
|
||||
|
||||
const id3V2Header = readId3V2Header(slice);
|
||||
if (id3V2Header) {
|
||||
// Extract the content portion (skip the 10-byte header)
|
||||
const contentSlice = slice.slice(startPos + 10, id3V2Header.size);
|
||||
// Clamp to the available data in case the ID3 header claims more than the WAV chunk provides
|
||||
// https://github.com/Vanilagy/mediabunny/issues/300
|
||||
const availableSize = size - ID3_V2_HEADER_SIZE;
|
||||
id3V2Header.size = Math.min(id3V2Header.size, availableSize);
|
||||
|
||||
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
|
||||
if (id3V2Header.size > 0) {
|
||||
const contentSlice = slice.slice(startPos + ID3_V2_HEADER_SIZE, id3V2Header.size);
|
||||
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -703,12 +703,14 @@ test('MPEG-TS muxing with StreamTarget', async () => {
|
||||
const frameDuration = 1 / fps;
|
||||
|
||||
for (let i = 0; i < frameCount; i++) {
|
||||
await videoSource.add(i * frameDuration, frameDuration);
|
||||
await videoSource.add(i * frameDuration, frameDuration, {
|
||||
keyFrame: true, // Otherwise all packets get written at once due to the DTS logic
|
||||
});
|
||||
}
|
||||
|
||||
await output.finalize();
|
||||
|
||||
expect(chunks.length).toBe(frameCount);
|
||||
expect(chunks).toHaveLength(frameCount);
|
||||
|
||||
const buffer = new Uint8Array(nextPos);
|
||||
nextPos = 0;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { test } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { ALL_FORMATS, EncodedPacketSink, Input, FilePathSource } from '../../src/index.js';
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname;
|
||||
|
||||
test('Should handle WAV file with oversized ID3 chunk', async () => {
|
||||
const filePath = path.join(__dirname, '..', 'public/oversized-id3.wav');
|
||||
using input = new Input({
|
||||
source: new FilePathSource(filePath),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
if (!track) {
|
||||
throw new Error('No audio track found');
|
||||
}
|
||||
|
||||
const sink = new EncodedPacketSink(track);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
void packet;
|
||||
}
|
||||
});
|
||||
Binary file not shown.
Reference in New Issue
Block a user