From b14daf6dca80927a6ea575938b0afb62e7abde6c Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sat, 22 Mar 2025 15:33:00 +0100 Subject: [PATCH] Add configurable minimum MP4 fragment duration and Matroska cluster duration --- dev/convert.html | 4 +++- dev/mux.html | 21 ++++----------------- src/isobmff/isobmff-muxer.ts | 5 ++++- src/matroska/matroska-muxer.ts | 2 +- src/output-format.ts | 24 ++++++++++++++++++++++++ todo.txt | 4 ++-- 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/dev/convert.html b/dev/convert.html index f3036a2..22e8437 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -16,7 +16,9 @@ const source = new Metamuxer.BlobSource(file); const target = new Metamuxer.BufferTarget(); - const outputFormat = new Metamuxer.WebMOutputFormat() + const outputFormat = new Metamuxer.Mp4OutputFormat({ + fastStart: 'fragmented' + }) const button = document.createElement('button'); button.textContent = 'Cancel'; diff --git a/dev/mux.html b/dev/mux.html index 7d05a50..fb38b1c 100644 --- a/dev/mux.html +++ b/dev/mux.html @@ -41,8 +41,8 @@ let format = new Metamuxer.MkvOutputFormat({ streamable: false }); format = new Metamuxer.Mp4OutputFormat({ fastStart: 'fragmented' }); // new Metamuxer.MkvOutputFormat();// new Metamuxer.Mp4OutputFormat({ fastStart: false }); format = new Metamuxer.OggOutputFormat(); - format = new Metamuxer.Mp4OutputFormat(); - format = new Metamuxer.WebMOutputFormat(); + format = new Metamuxer.Mp4OutputFormat({ fastStart: 'fragmented', minimumFragmentDuration: 2 }); + format = new Metamuxer.WebMOutputFormat({ minimumClusterDuration: 2 }); let target = new Metamuxer.BufferTarget(); /* @@ -86,12 +86,10 @@ download(new Blob([target.buffer]), 'test.mkv'); */ - /* let videoSource = new Metamuxer.CanvasSource(canvas, { codec: 'av1', bitrate: 1e6 - });*/ - let videoSource = new Metamuxer.EncodedVideoPacketSource('av1'); + }); let audioSource = new Metamuxer.AudioBufferSource({ codec: 'opus', bitrate: 128e3, @@ -162,22 +160,11 @@ Testing... <00:17.350>One... <00:18.125>Two... //subtitleSource.add(simpleWebvttFile); //subtitleSource.close(); - /* for (let i = 0; i < 100; i++) { context.fillStyle = ['red', 'green', 'blue', 'yellow'][i % 4]; context.fillRect(canvas.width * Math.random(), canvas.height * Math.random(), canvas.width * Math.random(), canvas.height * Math.random()); - await videoSource.add(i / 10, 1 / 10); - } - */ - for (let i = 0; i < 40; i += 0.1) { - await videoSource.add(new Metamuxer.EncodedPacket(new Uint8Array(1234), i === 0 ? 'key' : 'delta', i, 0.1), { - decoderConfig: { - codec: 'vp09.00.10.08', - codedWidth: 64, - codedHeight: 64 - } - }); + await videoSource.add(i / 10, 1 / 10, { keyFrame: true }); } let audioContext = new AudioContext(); diff --git a/src/isobmff/isobmff-muxer.ts b/src/isobmff/isobmff-muxer.ts index 3b0d239..2283d8a 100644 --- a/src/isobmff/isobmff-muxer.ts +++ b/src/isobmff/isobmff-muxer.ts @@ -118,6 +118,7 @@ export class IsobmffMuxer extends Muxer { private nextFragmentNumber = 1; // Only relevant for fragmented files, to make sure new fragments start with the highest timestamp seen so far private maxWrittenTimestamp = -Infinity; + private minimumFragmentDuration: number; constructor(output: Output, format: IsobmffOutputFormat) { super(output); @@ -136,6 +137,8 @@ export class IsobmffMuxer extends Muxer { if (this.fastStart === 'in-memory' || this.isFragmented) { this.writer.ensureMonotonicity = true; } + + this.minimumFragmentDuration = format._options.minimumFragmentDuration ?? 1; } async start() { @@ -683,7 +686,7 @@ export class IsobmffMuxer extends Muxer { }); if ( - currentChunkDuration >= 1.0 + currentChunkDuration >= this.minimumFragmentDuration && keyFrameQueuedEverywhere && sample.timestamp > this.maxWrittenTimestamp ) { diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index 3139e0f..09e12f7 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -703,7 +703,7 @@ export class MatroskaMuxer extends Muxer { // CURRENT chunk, meaning that starting the next cluster at the same timestamp is forbidden (since // the already-written block would belong into it instead). && msTimestamp > this.currentClusterMaxMsTimestamp - && relativeTimestamp >= 1000 + && relativeTimestamp >= 1000 * (this.format._options.minimumClusterDuration ?? 1) ) // The cluster would exceed its maximum allowed length. This puts us in an unfortunate position and forces // us to begin the next cluster with a delta frame. Although this is undesirable, it is not forbidden by the diff --git a/src/output-format.ts b/src/output-format.ts index 9ecc6fd..ea16392 100644 --- a/src/output-format.ts +++ b/src/output-format.ts @@ -110,6 +110,12 @@ export type IsobmffOutputFormatOptions = { * the type of output target used. */ fastStart?: false | 'in-memory' | 'fragmented'; + + /** + * When using `fastStart: 'fragmented'`, this field controls the minimum duration of each fragment, in seconds. + * New fragments will only be created when the current fragment is longer than this value. Defaults to 1 second. + */ + minimumFragmentDuration?: number; }; /** @@ -127,6 +133,12 @@ export abstract class IsobmffOutputFormat extends OutputFormat { if (options.fastStart !== undefined && ![false, 'in-memory', 'fragmented'].includes(options.fastStart)) { throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".'); } + if ( + options.minimumFragmentDuration !== undefined + && (!Number.isFinite(options.minimumFragmentDuration) || options.minimumFragmentDuration < 0) + ) { + throw new TypeError('options.minimumFragmentDuration, when provided, must be a non-negative number.'); + } super(); @@ -226,6 +238,12 @@ export type MkvOutputFormatOptions = { * option when you want to write out a file for later use. */ streamable?: boolean; + + /** + * This field controls the minimum duration of each Matroska cluster, in seconds. New clusters will only be created + * when the current cluster is longer than this value. Defaults to 1 second. + */ + minimumClusterDuration?: number; }; /** @@ -243,6 +261,12 @@ export class MkvOutputFormat extends OutputFormat { if (options.streamable !== undefined && typeof options.streamable !== 'boolean') { throw new TypeError('options.streamable, when provided, must be a boolean.'); } + if ( + options.minimumClusterDuration !== undefined + && (!Number.isFinite(options.minimumClusterDuration) || options.minimumClusterDuration < 0) + ) { + throw new TypeError('options.minimumClusterDuration, when provided, must be a non-negative number.'); + } super(); diff --git a/todo.txt b/todo.txt index 07318e7..e444d1e 100644 --- a/todo.txt +++ b/todo.txt @@ -2,5 +2,5 @@ - https://github.com/Vanilagy/mp4-muxer/issues/83 tell him it's possible now - is this fixed? https://github.com/Vanilagy/webm-muxer/issues/50 - cross-track offset for streaming sources -- configurable fragmented mp4 fragment size, like the mp4-muxer PR -- textsubtitlesource, chunked piping \ No newline at end of file +- textsubtitlesource, chunked piping +- More efficient MP3 loading when reading sequentially \ No newline at end of file