Comparing version 2.3.0 to 3.0.0
@@ -0,5 +1,45 @@ | ||
declare interface VideoOptions { | ||
/** | ||
* The codec of the encoded video chunks. | ||
*/ | ||
codec: 'avc' | 'hevc' | 'vp9' | 'av1', | ||
/** | ||
* The width of the video in pixels. | ||
*/ | ||
width: number, | ||
/** | ||
* The height of the video in pixels. | ||
*/ | ||
height: number, | ||
/** | ||
* The clockwise rotation of the video in degrees. | ||
*/ | ||
rotation?: 0 | 90 | 180 | 270 | ||
} | ||
declare interface AudioOptions { | ||
/** | ||
* The codec of the encoded audio chunks. | ||
*/ | ||
codec: 'aac' | 'opus', | ||
/** | ||
* The number of audio channels in the audio track. | ||
*/ | ||
numberOfChannels: number, | ||
/** | ||
* The sample rate of the audio track in samples per second per channel. | ||
*/ | ||
sampleRate: number | ||
} | ||
type NoInfer<T> = T extends infer S ? S : never; | ||
/** | ||
* Describes the properties used to configure an instance of `Muxer`. | ||
*/ | ||
declare interface MuxerOptions<T extends Target> { | ||
declare type MuxerOptions< | ||
T extends Target, | ||
V extends VideoOptions | undefined = undefined, | ||
A extends AudioOptions | undefined = undefined | ||
> = { | ||
/** | ||
@@ -13,20 +53,3 @@ * Specifies what happens with the data created by the muxer. | ||
*/ | ||
video?: { | ||
/** | ||
* The codec of the encoded video chunks. | ||
*/ | ||
codec: 'avc' | 'hevc' | 'vp9' | 'av1', | ||
/** | ||
* The width of the video in pixels. | ||
*/ | ||
width: number, | ||
/** | ||
* The height of the video in pixels. | ||
*/ | ||
height: number, | ||
/** | ||
* The clockwise rotation of the video in degrees. | ||
*/ | ||
rotation?: 0 | 90 | 180 | 270 | ||
}, | ||
video?: V, | ||
@@ -36,18 +59,25 @@ /** | ||
*/ | ||
audio?: { | ||
/** | ||
* The codec of the encoded audio chunks. | ||
*/ | ||
codec: 'aac' | 'opus', | ||
/** | ||
* The number of audio channels in the audio track. | ||
*/ | ||
numberOfChannels: number, | ||
/** | ||
* The sample rate of the audio track in samples per second per channel. | ||
*/ | ||
sampleRate: number | ||
}, | ||
audio?: A, | ||
/** | ||
* Controls the placement of metadata in the file. Placing metadata at the start of the file is known as "Fast | ||
* Start", which results in better playback at the cost of more required processing or memory. | ||
* | ||
* Use `false` to disable Fast Start, placing the metadata at the end of the file. Fastest and uses the least | ||
* memory. | ||
* | ||
* Use `'in-memory'` to produce a file with Fast Start by keeping all media chunks in memory until the file is | ||
* finalized. This produces a high-quality and compact output at the cost of a more expensive finalization step and | ||
* higher memory requirements. | ||
* | ||
* Use an object to produce a file with Fast Start by reserving space for metadata when muxing starts. In order to | ||
* know how much space needs to be reserved, you'll need to tell it the upper bound of how many media chunks will be | ||
* muxed. Do this by setting `expectedVideoChunks` and/or `expectedAudioChunks`. | ||
*/ | ||
fastStart: false | 'in-memory' | ( | ||
(NoInfer<V> extends undefined ? { expectedVideoChunks?: never } : { expectedVideoChunks: number }) | ||
& (NoInfer<A> extends undefined ? { expectedAudioChunks?: never } : { expectedAudioChunks: number }) | ||
), | ||
/** | ||
* Specifies how to deal with the first chunk in each track having a non-zero timestamp. In the default strict mode, | ||
@@ -63,3 +93,3 @@ * timestamps must start with 0 to ensure proper playback. However, when directly piping video frames or audio data | ||
firstTimestampBehavior?: 'strict' | 'offset' | ||
} | ||
}; | ||
@@ -66,0 +96,0 @@ declare type Target = ArrayBufferTarget | StreamTarget | FileSystemWritableFileStreamTarget; |
@@ -6,18 +6,3 @@ "use strict"; | ||
var __getOwnPropNames = Object.getOwnPropertyNames; | ||
var __getOwnPropSymbols = Object.getOwnPropertySymbols; | ||
var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
var __propIsEnum = Object.prototype.propertyIsEnumerable; | ||
var __pow = Math.pow; | ||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; | ||
var __spreadValues = (a, b) => { | ||
for (var prop in b ||= {}) | ||
if (__hasOwnProp.call(b, prop)) | ||
__defNormalProp(a, prop, b[prop]); | ||
if (__getOwnPropSymbols) | ||
for (var prop of __getOwnPropSymbols(b)) { | ||
if (__propIsEnum.call(b, prop)) | ||
__defNormalProp(a, prop, b[prop]); | ||
} | ||
return a; | ||
}; | ||
var __export = (target, all) => { | ||
@@ -91,3 +76,3 @@ for (var name in all) | ||
var u64 = (value) => { | ||
view.setUint32(0, Math.floor(value / __pow(2, 32)), false); | ||
view.setUint32(0, Math.floor(value / 2 ** 32), false); | ||
view.setUint32(4, value, false); | ||
@@ -97,11 +82,11 @@ return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]]; | ||
var fixed_8_8 = (value) => { | ||
view.setInt16(0, __pow(2, 8) * value, false); | ||
view.setInt16(0, 2 ** 8 * value, false); | ||
return [bytes[0], bytes[1]]; | ||
}; | ||
var fixed_16_16 = (value) => { | ||
view.setInt32(0, __pow(2, 16) * value, false); | ||
view.setInt32(0, 2 ** 16 * value, false); | ||
return [bytes[0], bytes[1], bytes[2], bytes[3]]; | ||
}; | ||
var fixed_2_30 = (value) => { | ||
view.setInt32(0, __pow(2, 30) * value, false); | ||
view.setInt32(0, 2 ** 30 * value, false); | ||
return [bytes[0], bytes[1], bytes[2], bytes[3]]; | ||
@@ -152,2 +137,11 @@ }; | ||
}; | ||
var deepClone = (x) => { | ||
if (!x) | ||
return x; | ||
if (typeof x !== "object") | ||
return x; | ||
if (Array.isArray(x)) | ||
return x.map(deepClone); | ||
return Object.fromEntries(Object.entries(x).map(([key, value]) => [key, deepClone(value)])); | ||
}; | ||
@@ -162,3 +156,3 @@ // src/box.ts | ||
type, | ||
[u8(version), u24(flags), contents != null ? contents : []], | ||
[u8(version), u24(flags), contents ?? []], | ||
children | ||
@@ -191,3 +185,4 @@ ); | ||
}; | ||
var mdat = () => ({ type: "mdat", largeSize: true }); | ||
var mdat = (reserveLargeSize) => ({ type: "mdat", largeSize: reserveLargeSize }); | ||
var free = (size) => ({ type: "free", size }); | ||
var moov = (tracks, creationTime) => box("moov", null, [ | ||
@@ -514,7 +509,7 @@ mvhd(creationTime, tracks), | ||
var stco = (track) => { | ||
if (track.writtenChunks.length > 0 && last(track.writtenChunks).offset >= __pow(2, 32)) { | ||
if (track.finalizedChunks.length > 0 && last(track.finalizedChunks).offset >= 2 ** 32) { | ||
return fullBox("co64", 0, 0, [ | ||
u32(track.writtenChunks.length), | ||
u32(track.finalizedChunks.length), | ||
// Number of entries | ||
track.writtenChunks.map((x) => u64(x.offset)) | ||
track.finalizedChunks.map((x) => u64(x.offset)) | ||
// Chunk offset table | ||
@@ -524,5 +519,5 @@ ]); | ||
return fullBox("stco", 0, 0, [ | ||
u32(track.writtenChunks.length), | ||
u32(track.finalizedChunks.length), | ||
// Number of entries | ||
track.writtenChunks.map((x) => u32(x.offset)) | ||
track.finalizedChunks.map((x) => u32(x.offset)) | ||
// Chunk offset table | ||
@@ -594,3 +589,3 @@ ]); | ||
writeU64(value) { | ||
__privateGet(this, _helperView).setUint32(0, Math.floor(value / __pow(2, 32)), false); | ||
__privateGet(this, _helperView).setUint32(0, Math.floor(value / 2 ** 32), false); | ||
__privateGet(this, _helperView).setUint32(4, value, false); | ||
@@ -610,6 +605,5 @@ this.write(__privateGet(this, _helper).subarray(0, 8)); | ||
writeBox(box2) { | ||
var _a, _b; | ||
this.offsets.set(box2, this.pos); | ||
if (box2.contents && !box2.children) { | ||
this.writeBoxHeader(box2, (_a = box2.size) != null ? _a : box2.contents.byteLength + 8); | ||
this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8); | ||
this.write(box2.contents); | ||
@@ -627,3 +621,3 @@ } else { | ||
let endPos = this.pos; | ||
let size = (_b = box2.size) != null ? _b : endPos - startPos; | ||
let size = box2.size ?? endPos - startPos; | ||
this.seek(startPos); | ||
@@ -640,2 +634,5 @@ this.writeBoxHeader(box2, size); | ||
} | ||
measureBoxHeader(box2) { | ||
return 8 + (box2.largeSize ? 8 : 0); | ||
} | ||
patchBox(box2) { | ||
@@ -647,6 +644,22 @@ let endPos = this.pos; | ||
} | ||
measureBox(box2) { | ||
if (box2.contents && !box2.children) { | ||
let headerSize = this.measureBoxHeader(box2); | ||
return headerSize + box2.contents.byteLength; | ||
} else { | ||
let result = this.measureBoxHeader(box2); | ||
if (box2.contents) | ||
result += box2.contents.byteLength; | ||
if (box2.children) { | ||
for (let child of box2.children) | ||
if (child) | ||
result += this.measureBox(child); | ||
} | ||
return result; | ||
} | ||
} | ||
}; | ||
_helper = new WeakMap(); | ||
_helperView = new WeakMap(); | ||
var _target, _buffer, _bytes, _ensureSize, ensureSize_fn; | ||
var _target, _buffer, _bytes, _maxPos, _ensureSize, ensureSize_fn; | ||
var ArrayBufferTargetWriter = class extends Writer { | ||
@@ -657,4 +670,5 @@ constructor(target) { | ||
__privateAdd(this, _target, void 0); | ||
__privateAdd(this, _buffer, new ArrayBuffer(__pow(2, 16))); | ||
__privateAdd(this, _buffer, new ArrayBuffer(2 ** 16)); | ||
__privateAdd(this, _bytes, new Uint8Array(__privateGet(this, _buffer))); | ||
__privateAdd(this, _maxPos, 0); | ||
__privateSet(this, _target, target); | ||
@@ -666,6 +680,7 @@ } | ||
this.pos += data.byteLength; | ||
__privateSet(this, _maxPos, Math.max(__privateGet(this, _maxPos), this.pos)); | ||
} | ||
finalize() { | ||
__privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos); | ||
__privateGet(this, _target).buffer = __privateGet(this, _buffer).slice(0, this.pos); | ||
__privateGet(this, _target).buffer = __privateGet(this, _buffer).slice(0, Math.max(__privateGet(this, _maxPos), this.pos)); | ||
} | ||
@@ -676,2 +691,3 @@ }; | ||
_bytes = new WeakMap(); | ||
_maxPos = new WeakMap(); | ||
_ensureSize = new WeakSet(); | ||
@@ -738,4 +754,3 @@ ensureSize_fn = function(size) { | ||
finalize() { | ||
var _a, _b; | ||
(_b = (_a = __privateGet(this, _target2)).onDone) == null ? void 0 : _b.call(_a); | ||
__privateGet(this, _target2).onDone?.(); | ||
} | ||
@@ -745,3 +760,3 @@ }; | ||
_sections = new WeakMap(); | ||
var DEFAULT_CHUNK_SIZE = __pow(2, 24); | ||
var DEFAULT_CHUNK_SIZE = 2 ** 24; | ||
var MAX_CHUNKS_AT_ONCE = 2; | ||
@@ -751,3 +766,2 @@ var _target3, _chunkSize, _chunks, _writeDataIntoChunks, writeDataIntoChunks_fn, _insertSectionIntoChunk, insertSectionIntoChunk_fn, _createChunk, createChunk_fn, _flushChunks, flushChunks_fn; | ||
constructor(target) { | ||
var _a, _b; | ||
super(); | ||
@@ -766,4 +780,4 @@ __privateAdd(this, _writeDataIntoChunks); | ||
__privateSet(this, _target3, target); | ||
__privateSet(this, _chunkSize, (_b = (_a = target.options) == null ? void 0 : _a.chunkSize) != null ? _b : DEFAULT_CHUNK_SIZE); | ||
if (!Number.isInteger(__privateGet(this, _chunkSize)) || __privateGet(this, _chunkSize) < __pow(2, 10)) { | ||
__privateSet(this, _chunkSize, target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE); | ||
if (!Number.isInteger(__privateGet(this, _chunkSize)) || __privateGet(this, _chunkSize) < 2 ** 10) { | ||
throw new Error("Invalid StreamTarget options: chunkSize must be an integer not smaller than 1024."); | ||
@@ -778,5 +792,4 @@ } | ||
finalize() { | ||
var _a, _b; | ||
__privateMethod(this, _flushChunks, flushChunks_fn).call(this, true); | ||
(_b = (_a = __privateGet(this, _target3)).onDone) == null ? void 0 : _b.call(_a); | ||
__privateGet(this, _target3).onDone?.(); | ||
} | ||
@@ -866,3 +879,2 @@ }; | ||
constructor(target) { | ||
var _a; | ||
super(new StreamTarget( | ||
@@ -875,3 +887,3 @@ (data, position) => target.stream.write({ | ||
void 0, | ||
{ chunkSize: (_a = target.options) == null ? void 0 : _a.chunkSize } | ||
{ chunkSize: target.options?.chunkSize } | ||
)); | ||
@@ -888,3 +900,3 @@ } | ||
var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset"]; | ||
var _options, _writer, _mdat, _videoTrack, _audioTrack, _creationTime, _finalized, _validateOptions, validateOptions_fn, _writeHeader, writeHeader_fn, _prepareTracks, prepareTracks_fn, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn, _addSampleToTrack, addSampleToTrack_fn, _validateTimestamp, validateTimestamp_fn, _writeCurrentChunk, writeCurrentChunk_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _ensureNotFinalized, ensureNotFinalized_fn; | ||
var _options, _writer, _ftypSize, _mdat, _videoTrack, _audioTrack, _creationTime, _finalizedChunks, _finalized, _validateOptions, validateOptions_fn, _writeHeader, writeHeader_fn, _computeMoovSizeUpperBound, computeMoovSizeUpperBound_fn, _prepareTracks, prepareTracks_fn, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn, _addSampleToTrack, addSampleToTrack_fn, _validateTimestamp, validateTimestamp_fn, _finalizeCurrentChunk, finalizeCurrentChunk_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _ensureNotFinalized, ensureNotFinalized_fn; | ||
var Muxer = class { | ||
@@ -894,2 +906,3 @@ constructor(options) { | ||
__privateAdd(this, _writeHeader); | ||
__privateAdd(this, _computeMoovSizeUpperBound); | ||
__privateAdd(this, _prepareTracks); | ||
@@ -900,3 +913,3 @@ // https://wiki.multimedia.cx/index.php/MPEG-4_Audio | ||
__privateAdd(this, _validateTimestamp); | ||
__privateAdd(this, _writeCurrentChunk); | ||
__privateAdd(this, _finalizeCurrentChunk); | ||
__privateAdd(this, _maybeFlushStreamingTargetWriter); | ||
@@ -906,2 +919,3 @@ __privateAdd(this, _ensureNotFinalized); | ||
__privateAdd(this, _writer, void 0); | ||
__privateAdd(this, _ftypSize, void 0); | ||
__privateAdd(this, _mdat, void 0); | ||
@@ -911,13 +925,17 @@ __privateAdd(this, _videoTrack, null); | ||
__privateAdd(this, _creationTime, Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET); | ||
__privateAdd(this, _finalizedChunks, []); | ||
__privateAdd(this, _finalized, false); | ||
var _a; | ||
__privateMethod(this, _validateOptions, validateOptions_fn).call(this, options); | ||
options.video = deepClone(options.video); | ||
options.audio = deepClone(options.audio); | ||
options.fastStart = deepClone(options.fastStart); | ||
this.target = options.target; | ||
__privateSet(this, _options, __spreadValues({ | ||
firstTimestampBehavior: "strict" | ||
}, options)); | ||
__privateSet(this, _options, { | ||
firstTimestampBehavior: "strict", | ||
...options | ||
}); | ||
if (options.target instanceof ArrayBufferTarget) { | ||
__privateSet(this, _writer, new ArrayBufferTargetWriter(options.target)); | ||
} else if (options.target instanceof StreamTarget) { | ||
__privateSet(this, _writer, ((_a = options.target.options) == null ? void 0 : _a.chunked) ? new ChunkedStreamTargetWriter(options.target) : new StreamTargetWriter(options.target)); | ||
__privateSet(this, _writer, options.target.options?.chunked ? new ChunkedStreamTargetWriter(options.target) : new StreamTargetWriter(options.target)); | ||
} else if (options.target instanceof FileSystemWritableFileStreamTarget) { | ||
@@ -934,3 +952,3 @@ __privateSet(this, _writer, new FileSystemWritableFileStreamTargetWriter(options.target)); | ||
sample.copyTo(data); | ||
this.addVideoChunkRaw(data, sample.type, timestamp != null ? timestamp : sample.timestamp, sample.duration, meta); | ||
this.addVideoChunkRaw(data, sample.type, timestamp ?? sample.timestamp, sample.duration, meta); | ||
} | ||
@@ -941,2 +959,5 @@ addVideoChunkRaw(data, type, timestamp, duration, meta) { | ||
throw new Error("No video track declared."); | ||
if (typeof __privateGet(this, _options).fastStart === "object" && __privateGet(this, _videoTrack).samples.length === __privateGet(this, _options).fastStart.expectedVideoChunks) { | ||
throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${__privateGet(this, _options).fastStart.expectedVideoChunks}).`); | ||
} | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), data, type, timestamp, duration, meta); | ||
@@ -947,3 +968,3 @@ } | ||
sample.copyTo(data); | ||
this.addAudioChunkRaw(data, sample.type, timestamp != null ? timestamp : sample.timestamp, sample.duration, meta); | ||
this.addAudioChunkRaw(data, sample.type, timestamp ?? sample.timestamp, sample.duration, meta); | ||
} | ||
@@ -954,2 +975,5 @@ addAudioChunkRaw(data, type, timestamp, duration, meta) { | ||
throw new Error("No audio track declared."); | ||
if (typeof __privateGet(this, _options).fastStart === "object" && __privateGet(this, _audioTrack).samples.length === __privateGet(this, _options).fastStart.expectedAudioChunks) { | ||
throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${__privateGet(this, _options).fastStart.expectedAudioChunks}).`); | ||
} | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), data, type, timestamp, duration, meta); | ||
@@ -960,11 +984,50 @@ } | ||
if (__privateGet(this, _videoTrack)) | ||
__privateMethod(this, _writeCurrentChunk, writeCurrentChunk_fn).call(this, __privateGet(this, _videoTrack)); | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _videoTrack)); | ||
if (__privateGet(this, _audioTrack)) | ||
__privateMethod(this, _writeCurrentChunk, writeCurrentChunk_fn).call(this, __privateGet(this, _audioTrack)); | ||
let mdatPos = __privateGet(this, _writer).offsets.get(__privateGet(this, _mdat)); | ||
let mdatSize = __privateGet(this, _writer).pos - mdatPos; | ||
__privateGet(this, _mdat).size = mdatSize; | ||
__privateGet(this, _writer).patchBox(__privateGet(this, _mdat)); | ||
let movieBox = moov([__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter(Boolean), __privateGet(this, _creationTime)); | ||
__privateGet(this, _writer).writeBox(movieBox); | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _audioTrack)); | ||
let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter(Boolean); | ||
if (__privateGet(this, _options).fastStart === "in-memory") { | ||
let mdatSize; | ||
for (let i = 0; i < 2; i++) { | ||
let movieBox2 = moov(tracks, __privateGet(this, _creationTime)); | ||
let movieBoxSize = __privateGet(this, _writer).measureBox(movieBox2); | ||
mdatSize = __privateGet(this, _writer).measureBox(__privateGet(this, _mdat)); | ||
let currentChunkPos = __privateGet(this, _writer).pos + movieBoxSize + mdatSize; | ||
for (let chunk of __privateGet(this, _finalizedChunks)) { | ||
chunk.offset = currentChunkPos; | ||
for (let bytes2 of chunk.sampleData) { | ||
currentChunkPos += bytes2.byteLength; | ||
mdatSize += bytes2.byteLength; | ||
} | ||
} | ||
if (currentChunkPos < 2 ** 32) | ||
break; | ||
if (mdatSize >= 2 ** 32) | ||
__privateGet(this, _mdat).largeSize = true; | ||
} | ||
let movieBox = moov(tracks, __privateGet(this, _creationTime)); | ||
__privateGet(this, _writer).writeBox(movieBox); | ||
__privateGet(this, _mdat).size = mdatSize; | ||
__privateGet(this, _writer).writeBox(__privateGet(this, _mdat)); | ||
for (let chunk of __privateGet(this, _finalizedChunks)) { | ||
for (let bytes2 of chunk.sampleData) | ||
__privateGet(this, _writer).write(bytes2); | ||
chunk.sampleData = null; | ||
} | ||
} else { | ||
let mdatPos = __privateGet(this, _writer).offsets.get(__privateGet(this, _mdat)); | ||
let mdatSize = __privateGet(this, _writer).pos - mdatPos; | ||
__privateGet(this, _mdat).size = mdatSize; | ||
__privateGet(this, _mdat).largeSize = mdatSize >= 2 ** 32; | ||
__privateGet(this, _writer).patchBox(__privateGet(this, _mdat)); | ||
let movieBox = moov(tracks, __privateGet(this, _creationTime)); | ||
if (typeof __privateGet(this, _options).fastStart === "object") { | ||
__privateGet(this, _writer).seek(__privateGet(this, _ftypSize)); | ||
__privateGet(this, _writer).writeBox(movieBox); | ||
let remainingBytes = mdatPos - __privateGet(this, _writer).pos; | ||
__privateGet(this, _writer).writeBox(free(remainingBytes)); | ||
} else { | ||
__privateGet(this, _writer).writeBox(movieBox); | ||
} | ||
} | ||
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); | ||
@@ -977,2 +1040,3 @@ __privateGet(this, _writer).finalize(); | ||
_writer = new WeakMap(); | ||
_ftypSize = new WeakMap(); | ||
_mdat = new WeakMap(); | ||
@@ -982,2 +1046,3 @@ _videoTrack = new WeakMap(); | ||
_creationTime = new WeakMap(); | ||
_finalizedChunks = new WeakMap(); | ||
_finalized = new WeakMap(); | ||
@@ -1000,15 +1065,53 @@ _validateOptions = new WeakSet(); | ||
} | ||
if (typeof options.fastStart === "object") { | ||
if (options.video && options.fastStart.expectedVideoChunks === void 0) { | ||
throw new Error(`'fastStart' is an object but is missing property 'expectedVideoChunks'.`); | ||
} | ||
if (options.audio && options.fastStart.expectedAudioChunks === void 0) { | ||
throw new Error(`'fastStart' is an object but is missing property 'expectedAudioChunks'.`); | ||
} | ||
} else if (![false, "in-memory"].includes(options.fastStart)) { | ||
throw new Error(`'fastStart' option must be false, 'in-memory' or an object.`); | ||
} | ||
}; | ||
_writeHeader = new WeakSet(); | ||
writeHeader_fn = function() { | ||
var _a; | ||
let holdsHevc = ((_a = __privateGet(this, _options).video) == null ? void 0 : _a.codec) === "hevc"; | ||
let holdsHevc = __privateGet(this, _options).video?.codec === "hevc"; | ||
__privateGet(this, _writer).writeBox(ftyp(holdsHevc)); | ||
__privateSet(this, _mdat, mdat()); | ||
__privateGet(this, _writer).writeBox(__privateGet(this, _mdat)); | ||
__privateSet(this, _ftypSize, __privateGet(this, _writer).pos); | ||
if (__privateGet(this, _options).fastStart === "in-memory") { | ||
__privateSet(this, _mdat, mdat(false)); | ||
} else { | ||
if (typeof __privateGet(this, _options).fastStart === "object") { | ||
let moovSizeUpperBound = __privateMethod(this, _computeMoovSizeUpperBound, computeMoovSizeUpperBound_fn).call(this); | ||
__privateGet(this, _writer).seek(__privateGet(this, _writer).pos + moovSizeUpperBound); | ||
} | ||
__privateSet(this, _mdat, mdat(true)); | ||
__privateGet(this, _writer).writeBox(__privateGet(this, _mdat)); | ||
} | ||
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); | ||
}; | ||
_computeMoovSizeUpperBound = new WeakSet(); | ||
computeMoovSizeUpperBound_fn = function() { | ||
if (typeof __privateGet(this, _options).fastStart !== "object") | ||
return; | ||
let upperBound = 0; | ||
let sampleCounts = [ | ||
__privateGet(this, _options).fastStart.expectedVideoChunks, | ||
__privateGet(this, _options).fastStart.expectedAudioChunks | ||
]; | ||
for (let n of sampleCounts) { | ||
if (!n) | ||
continue; | ||
upperBound += (4 + 4) * Math.ceil(2 / 3 * n); | ||
upperBound += 4 * n; | ||
upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n); | ||
upperBound += 4 * n; | ||
upperBound += 8 * n; | ||
} | ||
upperBound += 4096; | ||
return upperBound; | ||
}; | ||
_prepareTracks = new WeakSet(); | ||
prepareTracks_fn = function() { | ||
var _a; | ||
if (__privateGet(this, _options).video) { | ||
@@ -1022,3 +1125,3 @@ __privateSet(this, _videoTrack, { | ||
height: __privateGet(this, _options).video.height, | ||
rotation: (_a = __privateGet(this, _options).video.rotation) != null ? _a : 0 | ||
rotation: __privateGet(this, _options).video.rotation ?? 0 | ||
}, | ||
@@ -1029,3 +1132,3 @@ timescale: 720, | ||
samples: [], | ||
writtenChunks: [], | ||
finalizedChunks: [], | ||
currentChunk: null, | ||
@@ -1058,3 +1161,3 @@ firstTimestamp: void 0, | ||
samples: [], | ||
writtenChunks: [], | ||
finalizedChunks: [], | ||
currentChunk: null, | ||
@@ -1090,3 +1193,2 @@ firstTimestamp: void 0, | ||
addSampleToTrack_fn = function(track, data, type, timestamp, duration, meta) { | ||
var _a; | ||
let timestampInSeconds = timestamp / 1e6; | ||
@@ -1100,3 +1202,3 @@ let durationInSeconds = duration / 1e6; | ||
if (track.currentChunk) | ||
__privateMethod(this, _writeCurrentChunk, writeCurrentChunk_fn).call(this, track); | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, track); | ||
track.currentChunk = { | ||
@@ -1110,3 +1212,3 @@ startTimestamp: timestampInSeconds, | ||
track.currentChunk.sampleCount++; | ||
if ((_a = meta == null ? void 0 : meta.decoderConfig) == null ? void 0 : _a.description) { | ||
if (meta?.decoderConfig?.description) { | ||
track.codecPrivate = new Uint8Array(meta.decoderConfig.description); | ||
@@ -1164,13 +1266,9 @@ } | ||
}; | ||
_writeCurrentChunk = new WeakSet(); | ||
writeCurrentChunk_fn = function(track) { | ||
_finalizeCurrentChunk = new WeakSet(); | ||
finalizeCurrentChunk_fn = function(track) { | ||
if (!track.currentChunk) | ||
return; | ||
track.currentChunk.offset = __privateGet(this, _writer).pos; | ||
for (let bytes2 of track.currentChunk.sampleData) | ||
__privateGet(this, _writer).write(bytes2); | ||
track.currentChunk.sampleData = null; | ||
if (track.compactlyCodedChunkTable.length === 0 || last(track.compactlyCodedChunkTable).samplesPerChunk !== track.currentChunk.sampleCount) { | ||
track.compactlyCodedChunkTable.push({ | ||
firstChunk: track.writtenChunks.length + 1, | ||
firstChunk: track.finalizedChunks.length + 1, | ||
// 1-indexed | ||
@@ -1180,3 +1278,12 @@ samplesPerChunk: track.currentChunk.sampleCount | ||
} | ||
track.writtenChunks.push(track.currentChunk); | ||
track.finalizedChunks.push(track.currentChunk); | ||
__privateGet(this, _finalizedChunks).push(track.currentChunk); | ||
if (__privateGet(this, _options).fastStart === "in-memory") { | ||
track.currentChunk.offset = 0; | ||
return; | ||
} | ||
track.currentChunk.offset = __privateGet(this, _writer).pos; | ||
for (let bytes2 of track.currentChunk.sampleData) | ||
__privateGet(this, _writer).write(bytes2); | ||
track.currentChunk.sampleData = null; | ||
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); | ||
@@ -1183,0 +1290,0 @@ }; |
{ | ||
"name": "mp4-muxer", | ||
"version": "2.3.0", | ||
"version": "3.0.0", | ||
"description": "MP4 multiplexer in pure TypeScript with support for WebCodecs API, video & audio.", | ||
@@ -5,0 +5,0 @@ "main": "./build/mp4-muxer.js", |
@@ -5,5 +5,6 @@ # mp4-muxer - JavaScript MP4 multiplexer | ||
[![](https://img.shields.io/bundlephobia/minzip/mp4-muxer)](https://bundlephobia.com/package/mp4-muxer) | ||
[![](https://img.shields.io/npm/dm/mp4-muxer)](https://www.npmjs.com/package/mp4-muxer) | ||
The WebCodecs API provides low-level access to media codecs, but provides no way of actually packaging (multiplexing) | ||
the encoded media into a playable file. This project implements an MP4 multiplexer in pure TypeScript, which is | ||
the encoded media into a playable file. This project implements an MP4 multiplexer in pure TypeScript which is | ||
high-quality, fast and tiny, and supports both video and audio. | ||
@@ -16,2 +17,4 @@ | ||
> Consider [donating](https://ko-fi.com/vanilagy) if you've found this library useful and wish to support it ❤️ | ||
## Quick start | ||
@@ -28,3 +31,4 @@ The following is an example for a common usage of this library: | ||
height: 720 | ||
} | ||
}, | ||
fastStart: 'in-memory' | ||
}); | ||
@@ -102,2 +106,7 @@ | ||
fastStart: | ||
| false | ||
| 'in-memory' | ||
| { expectedVideoChunks?: number, expectedAudioChunks?: number } | ||
firstTimestampBehavior?: 'strict' | 'offset' | ||
@@ -107,3 +116,3 @@ } | ||
Codecs currently supported by this library are AVC/H.264, HEVC/H.265, VP9 and AV1 for video, and AAC and Opus for audio. | ||
#### `target` | ||
#### `target` (required) | ||
This option specifies where the data created by the muxer will be written. The options are: | ||
@@ -117,2 +126,3 @@ - `ArrayBufferTarget`: The file data will be written into a single large buffer, which is then stored in the target. | ||
target: new ArrayBufferTarget(), | ||
fastStart: 'in-memory', | ||
// ... | ||
@@ -155,2 +165,3 @@ }); | ||
), | ||
fastStart: false, | ||
// ... | ||
@@ -185,2 +196,3 @@ }); | ||
target: new FileSystemWritableFileStreamTarget(fileStream), | ||
fastStart: false, | ||
// ... | ||
@@ -194,2 +206,27 @@ }); | ||
``` | ||
#### `fastStart` (required) | ||
By default, MP4 metadata is stored at the end of the file in the `moov` box - this makes writing the file faster and | ||
easier. However, placing this `moov` box at the _start_ of the file instead (known as "Fast Start") provides certain | ||
benefits: The file becomes easier to stream over the web without range requests, and sites like YouTube can start | ||
processing the video while it's uploading. This library provides full control over the placement of the `moov` box by | ||
setting `fastStart` to one of these options: | ||
- `false`: Disables Fast Start, placing metadata at the end of the file. This option is the fastest and uses the least | ||
memory. This option is recommended for large, unbounded files that are streamed directly to disk. | ||
- `'in-memory'`: Produces a file with Fast Start by keeping all media chunks in memory until the file is finalized. This | ||
option produces the most compact output possible at the cost of a more expensive finalization step and higher memory | ||
requirements. You should _always_ use this option when using `ArrayBufferTarget` as it will result in a | ||
higher-quality output with no change in memory footprint. | ||
- `object`: Produces a file with Fast Start by reserving space for metadata when muxing begins. To know | ||
how many bytes need to be reserved to be safe, you'll have to provide the following data: | ||
```ts | ||
{ | ||
expectedVideoChunks?: number, | ||
expectedAudioChunks?: number | ||
} | ||
``` | ||
Note that the property `expectedVideoChunks` is _required_ if you have a video track - the same goes for audio. With | ||
this option set, you cannot mux more chunks than the number you've specified (although less is fine). | ||
This option is faster than `'in-memory'` and uses no additional memory, but results in a slightly larger output, | ||
making it useful for when you want to stream the file to disk while still retaining Fast Start. | ||
#### `firstTimestampBehavior` (optional) | ||
@@ -196,0 +233,0 @@ Specifies how to deal with the first chunk in each track having a non-zero timestamp. In the default strict mode, |
Sorry, the diff of this file is not supported yet
111294
2675
314