Comparing version 3.0.5 to 4.0.0
@@ -35,12 +35,6 @@ declare interface VideoOptions { | ||
type NoInfer<T> = T extends infer S ? S : never; | ||
/** | ||
* Describes the properties used to configure an instance of `Muxer`. | ||
*/ | ||
declare type MuxerOptions< | ||
T extends Target, | ||
V extends VideoOptions | undefined = undefined, | ||
A extends AudioOptions | undefined = undefined | ||
> = { | ||
declare type MuxerOptions<T extends Target> = { | ||
/** | ||
@@ -54,3 +48,3 @@ * Specifies what happens with the data created by the muxer. | ||
*/ | ||
video?: V, | ||
video?: VideoOptions, | ||
@@ -60,3 +54,3 @@ /** | ||
*/ | ||
audio?: A, | ||
audio?: AudioOptions, | ||
@@ -74,2 +68,8 @@ /** | ||
* | ||
* Use `'fragmented'` to place metadata at the start of the file by creating a fragmented "fMP4" file. In a | ||
* fragmented file, chunks of media and their metadata are written to the file in "fragments", eliminating the need | ||
* to put all metadata in one place. Fragmented files are useful for streaming, as they allow for better random | ||
* access. Furthermore, they remain lightweight to create even for very large files, as they don't require all media | ||
* to be kept in memory. However, fragmented files are not as widely supported as regular MP4 files. | ||
* | ||
* Use an object to produce a file with Fast Start by reserving space for metadata when muxing starts. In order to | ||
@@ -79,6 +79,6 @@ * know how much space needs to be reserved, you'll need to tell it the upper bound of how many media chunks will be | ||
*/ | ||
fastStart: false | 'in-memory' | ( | ||
(NoInfer<V> extends undefined ? { expectedVideoChunks?: never } : { expectedVideoChunks: number }) | ||
& (NoInfer<A> extends undefined ? { expectedAudioChunks?: never } : { expectedAudioChunks: number }) | ||
), | ||
fastStart: false | 'in-memory' | 'fragmented' | { | ||
expectedVideoChunks?: number, | ||
expectedAudioChunks?: number | ||
}, | ||
@@ -114,7 +114,7 @@ /** | ||
declare class StreamTarget { | ||
constructor( | ||
onData: (data: Uint8Array, position: number) => void, | ||
onDone?: () => void, | ||
options?: { chunked?: boolean, chunkSize?: number } | ||
); | ||
constructor(options: { | ||
onData?: (data: Uint8Array, position: number) => void, | ||
chunked?: boolean, | ||
chunkSize?: number | ||
}); | ||
} | ||
@@ -138,7 +138,3 @@ | ||
*/ | ||
declare class Muxer< | ||
T extends Target, | ||
V extends VideoOptions | undefined = undefined, | ||
A extends AudioOptions | undefined = undefined | ||
> { | ||
declare class Muxer<T extends Target> { | ||
target: T; | ||
@@ -150,3 +146,3 @@ | ||
*/ | ||
constructor(options: MuxerOptions<T, V, A>); | ||
constructor(options: MuxerOptions<T>); | ||
@@ -153,0 +149,0 @@ /** |
@@ -38,2 +38,10 @@ "use strict"; | ||
}; | ||
var __privateWrapper = (obj, member, setter, getter) => ({ | ||
set _(value) { | ||
__privateSet(obj, member, value, setter); | ||
}, | ||
get _() { | ||
return __privateGet(obj, member, getter); | ||
} | ||
}); | ||
var __privateMethod = (obj, member, method) => { | ||
@@ -144,2 +152,5 @@ __accessCheck(obj, member, "access private method"); | ||
}; | ||
var isU32 = (value) => { | ||
return value >= 0 && value < 2 ** 32; | ||
}; | ||
@@ -157,13 +168,14 @@ // src/box.ts | ||
); | ||
var ftyp = (holdsHevc) => { | ||
if (holdsHevc) | ||
var ftyp = (details) => { | ||
let minorVersion = 512; | ||
if (details.fragmented) | ||
return box("ftyp", [ | ||
ascii("isom"), | ||
ascii("iso5"), | ||
// Major brand | ||
u32(0), | ||
u32(minorVersion), | ||
// Minor version | ||
ascii("iso4"), | ||
// Compatible brand 1 | ||
ascii("hvc1") | ||
// Compatible brand 2 | ||
// Compatible brands | ||
ascii("iso5"), | ||
ascii("iso6"), | ||
ascii("mp41") | ||
]); | ||
@@ -173,10 +185,8 @@ return box("ftyp", [ | ||
// Major brand | ||
u32(0), | ||
u32(minorVersion), | ||
// Minor version | ||
// Compatible brands | ||
ascii("isom"), | ||
// Compatible brand 1 | ||
ascii("avc1"), | ||
// Compatible brand 2 | ||
details.holdsAvc ? ascii("avc1") : [], | ||
ascii("mp41") | ||
// Compatible brand 3 | ||
]); | ||
@@ -186,5 +196,6 @@ }; | ||
var free = (size) => ({ type: "free", size }); | ||
var moov = (tracks, creationTime) => box("moov", null, [ | ||
var moov = (tracks, creationTime, fragmented = false) => box("moov", null, [ | ||
mvhd(creationTime, tracks), | ||
...tracks.map((x) => trak(x, creationTime)) | ||
...tracks.map((x) => trak(x, creationTime)), | ||
fragmented ? mvex(tracks) : null | ||
]); | ||
@@ -197,10 +208,12 @@ var mvhd = (creationTime, tracks) => { | ||
let nextTrackId = Math.max(...tracks.map((x) => x.id)) + 1; | ||
return fullBox("mvhd", 0, 0, [ | ||
u32(creationTime), | ||
let needsU64 = !isU32(creationTime) || !isU32(duration); | ||
let u32OrU64 = needsU64 ? u64 : u32; | ||
return fullBox("mvhd", +needsU64, 0, [ | ||
u32OrU64(creationTime), | ||
// Creation time | ||
u32(creationTime), | ||
u32OrU64(creationTime), | ||
// Modification time | ||
u32(GLOBAL_TIMESCALE), | ||
// Timescale | ||
u32(duration), | ||
u32OrU64(duration), | ||
// Duration | ||
@@ -231,6 +244,8 @@ fixed_16_16(1), | ||
); | ||
return fullBox("tkhd", 0, 3, [ | ||
u32(creationTime), | ||
let needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale); | ||
let u32OrU64 = needsU64 ? u64 : u32; | ||
return fullBox("tkhd", +needsU64, 3, [ | ||
u32OrU64(creationTime), | ||
// Creation time | ||
u32(creationTime), | ||
u32OrU64(creationTime), | ||
// Modification time | ||
@@ -241,3 +256,3 @@ u32(track.id), | ||
// Reserved | ||
u32(durationInGlobalTimescale), | ||
u32OrU64(durationInGlobalTimescale), | ||
// Duration | ||
@@ -273,10 +288,12 @@ Array(8).fill(0), | ||
); | ||
return fullBox("mdhd", 0, 0, [ | ||
u32(creationTime), | ||
let needsU64 = !isU32(creationTime) || !isU32(localDuration); | ||
let u32OrU64 = needsU64 ? u64 : u32; | ||
return fullBox("mdhd", +needsU64, 0, [ | ||
u32OrU64(creationTime), | ||
// Creation time | ||
u32(creationTime), | ||
u32OrU64(creationTime), | ||
// Modification time | ||
u32(track.timescale), | ||
// Timescale | ||
u32(localDuration), | ||
u32OrU64(localDuration), | ||
// Duration | ||
@@ -300,3 +317,3 @@ u16(21956), | ||
// Component flags mask | ||
ascii("mp4-muxer-hdlr") | ||
ascii("mp4-muxer-hdlr", true) | ||
// Component name | ||
@@ -528,2 +545,151 @@ ]); | ||
}; | ||
var mvex = (tracks) => { | ||
return box("mvex", null, tracks.map(trex)); | ||
}; | ||
var trex = (track) => { | ||
return fullBox("trex", 0, 0, [ | ||
u32(track.id), | ||
// Track ID | ||
u32(1), | ||
// Default sample description index | ||
u32(0), | ||
// Default sample duration | ||
u32(0), | ||
// Default sample size | ||
u32(0) | ||
// Default sample flags | ||
]); | ||
}; | ||
var moof = (sequenceNumber, tracks) => { | ||
return box("moof", null, [ | ||
mfhd(sequenceNumber), | ||
...tracks.map(traf) | ||
]); | ||
}; | ||
var mfhd = (sequenceNumber) => { | ||
return fullBox("mfhd", 0, 0, [ | ||
u32(sequenceNumber) | ||
// Sequence number | ||
]); | ||
}; | ||
var fragmentSampleFlags = (sample) => { | ||
let byte1 = 0; | ||
let byte2 = 0; | ||
let byte3 = 0; | ||
let byte4 = 0; | ||
let sampleIsDifferenceSample = sample.type === "delta"; | ||
byte2 |= +sampleIsDifferenceSample; | ||
if (sampleIsDifferenceSample) { | ||
byte1 |= 1; | ||
} else { | ||
byte1 |= 2; | ||
} | ||
return byte1 << 24 | byte2 << 16 | byte3 << 8 | byte4; | ||
}; | ||
var traf = (track) => { | ||
return box("traf", null, [ | ||
tfhd(track), | ||
tfdt(track), | ||
trun(track) | ||
]); | ||
}; | ||
var tfhd = (track) => { | ||
let tfFlags = 0; | ||
tfFlags |= 8; | ||
tfFlags |= 16; | ||
tfFlags |= 32; | ||
tfFlags |= 131072; | ||
let referenceSample = track.currentChunk.samples[1] ?? track.currentChunk.samples[0]; | ||
let referenceSampleInfo = { | ||
duration: referenceSample.timescaleUnitsToNextSample, | ||
size: referenceSample.size, | ||
flags: fragmentSampleFlags(referenceSample) | ||
}; | ||
return fullBox("tfhd", 0, tfFlags, [ | ||
u32(track.id), | ||
// Track ID | ||
u32(referenceSampleInfo.duration), | ||
// Default sample duration | ||
u32(referenceSampleInfo.size), | ||
// Default sample size | ||
u32(referenceSampleInfo.flags) | ||
// Default sample flags | ||
]); | ||
}; | ||
var tfdt = (track) => { | ||
return fullBox("tfdt", 1, 0, [ | ||
u64(intoTimescale(track.currentChunk.startTimestamp, track.timescale)) | ||
// Base Media Decode Time | ||
]); | ||
}; | ||
var trun = (track) => { | ||
let allSampleDurations = track.currentChunk.samples.map((x) => x.timescaleUnitsToNextSample); | ||
let allSampleSizes = track.currentChunk.samples.map((x) => x.size); | ||
let allSampleFlags = track.currentChunk.samples.map(fragmentSampleFlags); | ||
let uniqueSampleDurations = new Set(allSampleDurations); | ||
let uniqueSampleSizes = new Set(allSampleSizes); | ||
let uniqueSampleFlags = new Set(allSampleFlags); | ||
let firstSampleFlagsPresent = uniqueSampleFlags.size === 2 && allSampleFlags[0] !== allSampleFlags[1]; | ||
let sampleDurationPresent = uniqueSampleDurations.size > 1; | ||
let sampleSizePresent = uniqueSampleSizes.size > 1; | ||
let sampleFlagsPresent = !firstSampleFlagsPresent && uniqueSampleFlags.size > 1; | ||
let flags = 0; | ||
flags |= 1; | ||
flags |= 4 * +firstSampleFlagsPresent; | ||
flags |= 256 * +sampleDurationPresent; | ||
flags |= 512 * +sampleSizePresent; | ||
flags |= 1024 * +sampleFlagsPresent; | ||
return fullBox("trun", 0, flags, [ | ||
u32(track.currentChunk.samples.length), | ||
// Sample count | ||
u32(track.currentChunk.offset - track.currentChunk.moofOffset || 0), | ||
// Data offset | ||
firstSampleFlagsPresent ? u32(allSampleFlags[0]) : [], | ||
track.currentChunk.samples.map((_, i) => [ | ||
sampleDurationPresent ? u32(allSampleDurations[i]) : [], | ||
// Sample duration | ||
sampleSizePresent ? u32(allSampleSizes[i]) : [], | ||
// Sample size | ||
sampleFlagsPresent ? u32(allSampleFlags[i]) : [] | ||
// Sample flags | ||
]) | ||
]); | ||
}; | ||
var mfra = (tracks) => { | ||
return box("mfra", null, [ | ||
...tracks.map(tfra), | ||
mfro() | ||
]); | ||
}; | ||
var tfra = (track, trackIndex) => { | ||
let version = 1; | ||
return fullBox("tfra", version, 0, [ | ||
u32(track.id), | ||
// Track ID | ||
u32(63), | ||
// This specifies that traf number, trun number and sample number are 32-bit ints | ||
u32(track.finalizedChunks.length), | ||
// Number of entries | ||
track.finalizedChunks.map((chunk) => [ | ||
u64(intoTimescale(chunk.startTimestamp, track.timescale)), | ||
// Time | ||
u64(chunk.moofOffset), | ||
// moof offset | ||
u32(trackIndex + 1), | ||
// traf number | ||
u32(1), | ||
// trun number | ||
u32(1) | ||
// Sample number | ||
]) | ||
]); | ||
}; | ||
var mfro = () => { | ||
return fullBox("mfro", 0, 0, [ | ||
// This value needs to be overwritten manually from the outside, where the actual size of the enclosing mfra box | ||
// is known | ||
u32(0) | ||
// Size | ||
]); | ||
}; | ||
var VIDEO_CODEC_TO_BOX_NAME = { | ||
@@ -557,5 +723,3 @@ "avc": "avc1", | ||
var StreamTarget = class { | ||
constructor(onData, onDone, options) { | ||
this.onData = onData; | ||
this.onDone = onDone; | ||
constructor(options) { | ||
this.options = options; | ||
@@ -745,3 +909,3 @@ } | ||
} | ||
__privateGet(this, _target2).onData(chunk.data, chunk.start); | ||
__privateGet(this, _target2).options.onData?.(chunk.data, chunk.start); | ||
} | ||
@@ -751,3 +915,2 @@ __privateGet(this, _sections).length = 0; | ||
finalize() { | ||
__privateGet(this, _target2).onDone?.(); | ||
} | ||
@@ -787,3 +950,2 @@ }; | ||
__privateMethod(this, _flushChunks, flushChunks_fn).call(this, true); | ||
__privateGet(this, _target3).onDone?.(); | ||
} | ||
@@ -863,3 +1025,3 @@ }; | ||
for (let section of chunk.written) { | ||
__privateGet(this, _target3).onData( | ||
__privateGet(this, _target3).options.onData?.( | ||
chunk.data.subarray(section.start, section.end), | ||
@@ -874,4 +1036,4 @@ chunk.start + section.start | ||
constructor(target) { | ||
super(new StreamTarget( | ||
(data, position) => target.stream.write({ | ||
super(new StreamTarget({ | ||
onData: (data, position) => target.stream.write({ | ||
type: "write", | ||
@@ -881,5 +1043,4 @@ data, | ||
}), | ||
void 0, | ||
{ chunkSize: target.options?.chunkSize } | ||
)); | ||
chunkSize: target.options?.chunkSize | ||
})); | ||
} | ||
@@ -893,5 +1054,4 @@ }; | ||
var TIMESTAMP_OFFSET = 2082844800; | ||
var MAX_CHUNK_DURATION = 0.5; | ||
var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset"]; | ||
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 _options, _writer, _ftypSize, _mdat, _videoTrack, _audioTrack, _creationTime, _finalizedChunks, _nextFragmentNumber, _videoSampleQueue, _audioSampleQueue, _finalized, _validateOptions, validateOptions_fn, _writeHeader, writeHeader_fn, _computeMoovSizeUpperBound, computeMoovSizeUpperBound_fn, _prepareTracks, prepareTracks_fn, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn, _createSampleForTrack, createSampleForTrack_fn, _addSampleToTrack, addSampleToTrack_fn, _validateTimestamp, validateTimestamp_fn, _finalizeCurrentChunk, finalizeCurrentChunk_fn, _finalizeFragment, finalizeFragment_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _ensureNotFinalized, ensureNotFinalized_fn; | ||
var Muxer = class { | ||
@@ -905,5 +1065,7 @@ constructor(options) { | ||
__privateAdd(this, _generateMpeg4AudioSpecificConfig); | ||
__privateAdd(this, _createSampleForTrack); | ||
__privateAdd(this, _addSampleToTrack); | ||
__privateAdd(this, _validateTimestamp); | ||
__privateAdd(this, _finalizeCurrentChunk); | ||
__privateAdd(this, _finalizeFragment); | ||
__privateAdd(this, _maybeFlushStreamingTargetWriter); | ||
@@ -919,2 +1081,6 @@ __privateAdd(this, _ensureNotFinalized); | ||
__privateAdd(this, _finalizedChunks, []); | ||
// Fields for fragmented MP4: | ||
__privateAdd(this, _nextFragmentNumber, 1); | ||
__privateAdd(this, _videoSampleQueue, []); | ||
__privateAdd(this, _audioSampleQueue, []); | ||
__privateAdd(this, _finalized, false); | ||
@@ -939,4 +1105,4 @@ __privateMethod(this, _validateOptions, validateOptions_fn).call(this, options); | ||
} | ||
__privateMethod(this, _prepareTracks, prepareTracks_fn).call(this); | ||
__privateMethod(this, _writeHeader, writeHeader_fn).call(this); | ||
__privateMethod(this, _prepareTracks, prepareTracks_fn).call(this); | ||
} | ||
@@ -955,3 +1121,16 @@ addVideoChunk(sample, meta, timestamp) { | ||
} | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), data, type, timestamp, duration, meta); | ||
let videoSample = __privateMethod(this, _createSampleForTrack, createSampleForTrack_fn).call(this, __privateGet(this, _videoTrack), data, type, timestamp, duration, meta); | ||
if (__privateGet(this, _options).fastStart === "fragmented" && __privateGet(this, _audioTrack)) { | ||
while (__privateGet(this, _audioSampleQueue).length > 0 && __privateGet(this, _audioSampleQueue)[0].timestamp <= videoSample.timestamp) { | ||
let audioSample = __privateGet(this, _audioSampleQueue).shift(); | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample); | ||
} | ||
if (videoSample.timestamp <= __privateGet(this, _audioTrack).lastTimestamp) { | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample); | ||
} else { | ||
__privateGet(this, _videoSampleQueue).push(videoSample); | ||
} | ||
} else { | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample); | ||
} | ||
} | ||
@@ -970,3 +1149,16 @@ addAudioChunk(sample, meta, timestamp) { | ||
} | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), data, type, timestamp, duration, meta); | ||
let audioSample = __privateMethod(this, _createSampleForTrack, createSampleForTrack_fn).call(this, __privateGet(this, _audioTrack), data, type, timestamp, duration, meta); | ||
if (__privateGet(this, _options).fastStart === "fragmented" && __privateGet(this, _videoTrack)) { | ||
while (__privateGet(this, _videoSampleQueue).length > 0 && __privateGet(this, _videoSampleQueue)[0].timestamp <= audioSample.timestamp) { | ||
let videoSample = __privateGet(this, _videoSampleQueue).shift(); | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample); | ||
} | ||
if (audioSample.timestamp <= __privateGet(this, _videoTrack).lastTimestamp) { | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample); | ||
} else { | ||
__privateGet(this, _audioSampleQueue).push(audioSample); | ||
} | ||
} else { | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample); | ||
} | ||
} | ||
@@ -978,6 +1170,14 @@ /** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */ | ||
} | ||
if (__privateGet(this, _videoTrack)) | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _videoTrack)); | ||
if (__privateGet(this, _audioTrack)) | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _audioTrack)); | ||
if (__privateGet(this, _options).fastStart === "fragmented") { | ||
for (let videoSample of __privateGet(this, _videoSampleQueue)) | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample); | ||
for (let audioSample of __privateGet(this, _audioSampleQueue)) | ||
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample); | ||
__privateMethod(this, _finalizeFragment, finalizeFragment_fn).call(this, false); | ||
} else { | ||
if (__privateGet(this, _videoTrack)) | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _videoTrack)); | ||
if (__privateGet(this, _audioTrack)) | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _audioTrack)); | ||
} | ||
let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter(Boolean); | ||
@@ -993,5 +1193,5 @@ if (__privateGet(this, _options).fastStart === "in-memory") { | ||
chunk.offset = currentChunkPos; | ||
for (let bytes2 of chunk.sampleData) { | ||
currentChunkPos += bytes2.byteLength; | ||
mdatSize += bytes2.byteLength; | ||
for (let { data } of chunk.samples) { | ||
currentChunkPos += data.byteLength; | ||
mdatSize += data.byteLength; | ||
} | ||
@@ -1009,6 +1209,14 @@ } | ||
for (let chunk of __privateGet(this, _finalizedChunks)) { | ||
for (let bytes2 of chunk.sampleData) | ||
__privateGet(this, _writer).write(bytes2); | ||
chunk.sampleData = null; | ||
for (let sample of chunk.samples) { | ||
__privateGet(this, _writer).write(sample.data); | ||
sample.data = null; | ||
} | ||
} | ||
} else if (__privateGet(this, _options).fastStart === "fragmented") { | ||
let startPos = __privateGet(this, _writer).pos; | ||
let mfraBox = mfra(tracks); | ||
__privateGet(this, _writer).writeBox(mfraBox); | ||
let mfraBoxSize = __privateGet(this, _writer).pos - startPos; | ||
__privateGet(this, _writer).seek(__privateGet(this, _writer).pos - 4); | ||
__privateGet(this, _writer).writeU32(mfraBoxSize); | ||
} else { | ||
@@ -1043,2 +1251,5 @@ let mdatPos = __privateGet(this, _writer).offsets.get(__privateGet(this, _mdat)); | ||
_finalizedChunks = new WeakMap(); | ||
_nextFragmentNumber = new WeakMap(); | ||
_videoSampleQueue = new WeakMap(); | ||
_audioSampleQueue = new WeakMap(); | ||
_finalized = new WeakMap(); | ||
@@ -1068,4 +1279,4 @@ _validateOptions = new WeakSet(); | ||
} | ||
} else if (![false, "in-memory"].includes(options.fastStart)) { | ||
throw new Error(`'fastStart' option must be false, 'in-memory' or an object.`); | ||
} else if (![false, "in-memory", "fragmented"].includes(options.fastStart)) { | ||
throw new Error(`'fastStart' option must be false, 'in-memory', 'fragmented' or an object.`); | ||
} | ||
@@ -1075,7 +1286,10 @@ }; | ||
writeHeader_fn = function() { | ||
let holdsHevc = __privateGet(this, _options).video?.codec === "hevc"; | ||
__privateGet(this, _writer).writeBox(ftyp(holdsHevc)); | ||
__privateGet(this, _writer).writeBox(ftyp({ | ||
holdsAvc: __privateGet(this, _options).video?.codec === "avc", | ||
fragmented: __privateGet(this, _options).fastStart === "fragmented" | ||
})); | ||
__privateSet(this, _ftypSize, __privateGet(this, _writer).pos); | ||
if (__privateGet(this, _options).fastStart === "in-memory") { | ||
__privateSet(this, _mdat, mdat(false)); | ||
} else if (__privateGet(this, _options).fastStart === "fragmented") { | ||
} else { | ||
@@ -1124,4 +1338,4 @@ if (typeof __privateGet(this, _options).fastStart === "object") { | ||
}, | ||
timescale: 720, | ||
// = lcm(24, 30, 60, 120, 144, 240, 360), so should fit with many framerates | ||
timescale: 11520, | ||
// Timescale used by FFmpeg, contains many common frame rates as factors | ||
codecPrivate: new Uint8Array(0), | ||
@@ -1135,2 +1349,3 @@ samples: [], | ||
lastTimescaleUnits: null, | ||
lastSample: null, | ||
compactlyCodedChunkTable: [] | ||
@@ -1164,2 +1379,3 @@ }); | ||
lastTimescaleUnits: null, | ||
lastSample: null, | ||
compactlyCodedChunkTable: [] | ||
@@ -1188,54 +1404,81 @@ }); | ||
}; | ||
_addSampleToTrack = new WeakSet(); | ||
addSampleToTrack_fn = function(track, data, type, timestamp, duration, meta) { | ||
_createSampleForTrack = new WeakSet(); | ||
createSampleForTrack_fn = function(track, data, type, timestamp, duration, meta) { | ||
let timestampInSeconds = timestamp / 1e6; | ||
let durationInSeconds = duration / 1e6; | ||
if (track.firstTimestamp === void 0) | ||
track.firstTimestamp = timestampInSeconds; | ||
timestampInSeconds = __privateMethod(this, _validateTimestamp, validateTimestamp_fn).call(this, timestampInSeconds, track); | ||
track.lastTimestamp = timestampInSeconds; | ||
if (!track.currentChunk || timestampInSeconds - track.currentChunk.startTimestamp >= MAX_CHUNK_DURATION) { | ||
if (track.currentChunk) | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, track); | ||
track.currentChunk = { | ||
startTimestamp: timestampInSeconds, | ||
sampleData: [], | ||
sampleCount: 0 | ||
}; | ||
} | ||
track.currentChunk.sampleData.push(data); | ||
track.currentChunk.sampleCount++; | ||
if (meta?.decoderConfig?.description) { | ||
track.codecPrivate = new Uint8Array(meta.decoderConfig.description); | ||
} | ||
track.samples.push({ | ||
let sample = { | ||
timestamp: timestampInSeconds, | ||
duration: durationInSeconds, | ||
data, | ||
size: data.byteLength, | ||
type | ||
}); | ||
type, | ||
// Will be refined once the next sample comes in | ||
timescaleUnitsToNextSample: intoTimescale(durationInSeconds, track.timescale) | ||
}; | ||
return sample; | ||
}; | ||
_addSampleToTrack = new WeakSet(); | ||
addSampleToTrack_fn = function(track, sample) { | ||
if (__privateGet(this, _options).fastStart !== "fragmented") { | ||
track.samples.push(sample); | ||
} | ||
if (track.lastTimescaleUnits !== null) { | ||
let timescaleUnits = intoTimescale(timestampInSeconds, track.timescale, false); | ||
let timescaleUnits = intoTimescale(sample.timestamp, track.timescale, false); | ||
let delta = Math.round(timescaleUnits - track.lastTimescaleUnits); | ||
track.lastTimescaleUnits += delta; | ||
let lastTableEntry = last(track.timeToSampleTable); | ||
if (lastTableEntry.sampleCount === 1) { | ||
lastTableEntry.sampleDelta = delta; | ||
lastTableEntry.sampleCount++; | ||
} else if (lastTableEntry.sampleDelta === delta) { | ||
lastTableEntry.sampleCount++; | ||
} else { | ||
lastTableEntry.sampleCount--; | ||
track.lastSample.timescaleUnitsToNextSample = delta; | ||
if (__privateGet(this, _options).fastStart !== "fragmented") { | ||
let lastTableEntry = last(track.timeToSampleTable); | ||
if (lastTableEntry.sampleCount === 1) { | ||
lastTableEntry.sampleDelta = delta; | ||
lastTableEntry.sampleCount++; | ||
} else if (lastTableEntry.sampleDelta === delta) { | ||
lastTableEntry.sampleCount++; | ||
} else { | ||
lastTableEntry.sampleCount--; | ||
track.timeToSampleTable.push({ | ||
sampleCount: 2, | ||
sampleDelta: delta | ||
}); | ||
} | ||
} | ||
} else { | ||
track.lastTimescaleUnits = 0; | ||
if (__privateGet(this, _options).fastStart !== "fragmented") { | ||
track.timeToSampleTable.push({ | ||
sampleCount: 2, | ||
sampleDelta: delta | ||
sampleCount: 1, | ||
sampleDelta: intoTimescale(sample.duration, track.timescale) | ||
}); | ||
} | ||
} | ||
track.lastSample = sample; | ||
let beginNewChunk = false; | ||
if (!track.currentChunk) { | ||
beginNewChunk = true; | ||
} else { | ||
track.lastTimescaleUnits = 0; | ||
track.timeToSampleTable.push({ | ||
sampleCount: 1, | ||
sampleDelta: intoTimescale(durationInSeconds, track.timescale) | ||
}); | ||
let currentChunkDuration = sample.timestamp - track.currentChunk.startTimestamp; | ||
if (__privateGet(this, _options).fastStart === "fragmented") { | ||
let mostImportantTrack = __privateGet(this, _videoTrack) ?? __privateGet(this, _audioTrack); | ||
if (track === mostImportantTrack && sample.type === "key" && currentChunkDuration >= 1) { | ||
beginNewChunk = true; | ||
__privateMethod(this, _finalizeFragment, finalizeFragment_fn).call(this); | ||
} | ||
} else { | ||
beginNewChunk = currentChunkDuration >= 0.5; | ||
} | ||
} | ||
if (beginNewChunk) { | ||
if (track.currentChunk) { | ||
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, track); | ||
} | ||
track.currentChunk = { | ||
startTimestamp: sample.timestamp, | ||
samples: [] | ||
}; | ||
} | ||
track.currentChunk.samples.push(sample); | ||
}; | ||
@@ -1252,2 +1495,5 @@ _validateTimestamp = new WeakSet(); | ||
} else if (__privateGet(this, _options).firstTimestampBehavior === "offset") { | ||
if (track.firstTimestamp === void 0) { | ||
track.firstTimestamp = timestamp; | ||
} | ||
timestamp -= track.firstTimestamp; | ||
@@ -1260,2 +1506,3 @@ } | ||
} | ||
track.lastTimestamp = timestamp; | ||
return timestamp; | ||
@@ -1265,13 +1512,16 @@ }; | ||
finalizeCurrentChunk_fn = function(track) { | ||
if (__privateGet(this, _options).fastStart === "fragmented") { | ||
throw new Error("Can't finalize individual chunks 'fastStart' is set to 'fragmented'."); | ||
} | ||
if (!track.currentChunk) | ||
return; | ||
if (track.compactlyCodedChunkTable.length === 0 || last(track.compactlyCodedChunkTable).samplesPerChunk !== track.currentChunk.sampleCount) { | ||
track.finalizedChunks.push(track.currentChunk); | ||
__privateGet(this, _finalizedChunks).push(track.currentChunk); | ||
if (track.compactlyCodedChunkTable.length === 0 || last(track.compactlyCodedChunkTable).samplesPerChunk !== track.currentChunk.samples.length) { | ||
track.compactlyCodedChunkTable.push({ | ||
firstChunk: track.finalizedChunks.length + 1, | ||
firstChunk: track.finalizedChunks.length, | ||
// 1-indexed | ||
samplesPerChunk: track.currentChunk.sampleCount | ||
samplesPerChunk: track.currentChunk.samples.length | ||
}); | ||
} | ||
track.finalizedChunks.push(track.currentChunk); | ||
__privateGet(this, _finalizedChunks).push(track.currentChunk); | ||
if (__privateGet(this, _options).fastStart === "in-memory") { | ||
@@ -1282,7 +1532,62 @@ track.currentChunk.offset = 0; | ||
track.currentChunk.offset = __privateGet(this, _writer).pos; | ||
for (let bytes2 of track.currentChunk.sampleData) | ||
__privateGet(this, _writer).write(bytes2); | ||
track.currentChunk.sampleData = null; | ||
for (let sample of track.currentChunk.samples) { | ||
__privateGet(this, _writer).write(sample.data); | ||
sample.data = null; | ||
} | ||
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); | ||
}; | ||
_finalizeFragment = new WeakSet(); | ||
finalizeFragment_fn = function(flushStreamingWriter = true) { | ||
if (__privateGet(this, _options).fastStart !== "fragmented") { | ||
throw new Error("Can't finalize a fragment unless 'fastStart' is set to 'fragmented'."); | ||
} | ||
let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter((track) => track && track.currentChunk); | ||
if (tracks.length === 0) | ||
return; | ||
let fragmentNumber = __privateWrapper(this, _nextFragmentNumber)._++; | ||
if (fragmentNumber === 1) { | ||
let movieBox = moov(tracks, __privateGet(this, _creationTime), true); | ||
__privateGet(this, _writer).writeBox(movieBox); | ||
} | ||
let moofOffset = __privateGet(this, _writer).pos; | ||
let moofBox = moof(fragmentNumber, tracks); | ||
__privateGet(this, _writer).writeBox(moofBox); | ||
{ | ||
let mdatBox = mdat(false); | ||
let totalTrackSampleSize = 0; | ||
for (let track of tracks) { | ||
for (let sample of track.currentChunk.samples) { | ||
totalTrackSampleSize += sample.size; | ||
} | ||
} | ||
let mdatSize = __privateGet(this, _writer).measureBox(mdatBox) + totalTrackSampleSize; | ||
if (mdatSize >= 2 ** 32) { | ||
mdatBox.largeSize = true; | ||
mdatSize = __privateGet(this, _writer).measureBox(mdatBox) + totalTrackSampleSize; | ||
} | ||
mdatBox.size = mdatSize; | ||
__privateGet(this, _writer).writeBox(mdatBox); | ||
} | ||
for (let track of tracks) { | ||
track.currentChunk.offset = __privateGet(this, _writer).pos; | ||
track.currentChunk.moofOffset = moofOffset; | ||
for (let sample of track.currentChunk.samples) { | ||
__privateGet(this, _writer).write(sample.data); | ||
sample.data = null; | ||
} | ||
} | ||
let endPos = __privateGet(this, _writer).pos; | ||
__privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(moofBox)); | ||
let newMoofBox = moof(fragmentNumber, tracks); | ||
__privateGet(this, _writer).writeBox(newMoofBox); | ||
__privateGet(this, _writer).seek(endPos); | ||
for (let track of tracks) { | ||
track.finalizedChunks.push(track.currentChunk); | ||
__privateGet(this, _finalizedChunks).push(track.currentChunk); | ||
track.currentChunk = null; | ||
} | ||
if (flushStreamingWriter) { | ||
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); | ||
} | ||
}; | ||
_maybeFlushStreamingTargetWriter = new WeakSet(); | ||
@@ -1289,0 +1594,0 @@ maybeFlushStreamingTargetWriter_fn = function() { |
{ | ||
"name": "mp4-muxer", | ||
"version": "3.0.5", | ||
"version": "4.0.0", | ||
"description": "MP4 multiplexer in pure TypeScript with support for WebCodecs API, video & audio.", | ||
@@ -50,2 +50,3 @@ "main": "./build/mp4-muxer.js", | ||
"mp4", | ||
"fmp4", | ||
"muxer", | ||
@@ -52,0 +53,0 @@ "muxing", |
@@ -8,7 +8,10 @@ # mp4-muxer - JavaScript MP4 multiplexer | ||
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 | ||
high-quality, fast and tiny, and supports both video and audio. | ||
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 as well as various internal layouts such as Fast Start or | ||
fragmented MP4. | ||
[Demo: Muxing into a file](https://vanilagy.github.io/mp4-muxer/demo/) | ||
[Demo: Live streaming](https://vanilagy.github.io/mp4-muxer/demo-streaming) | ||
> **Note:** If you're looking to create **WebM** files, check out [webm-muxer](https://github.com/Vanilagy/webm-muxer), | ||
@@ -107,2 +110,3 @@ the sister library to mp4-muxer. | ||
| 'in-memory' | ||
| 'fragmented' | ||
| { expectedVideoChunks?: number, expectedAudioChunks?: number } | ||
@@ -136,19 +140,21 @@ | ||
```ts | ||
constructor( | ||
onData: (data: Uint8Array, position: number) => void, | ||
onDone?: () => void, | ||
options?: { chunked?: boolean, chunkSize?: number } | ||
); | ||
constructor(options: { | ||
onData?: (data: Uint8Array, position: number) => void, | ||
chunked?: boolean, | ||
chunkSize?: number | ||
}); | ||
``` | ||
The `position` argument specifies the offset in bytes at which the data has to be written. Since the data written by | ||
the muxer is not entirely sequential, **make sure to respect this argument**. | ||
`onData` is called for each new chunk of available data. The `position` argument specifies the offset in bytes at | ||
which the data has to be written. Since the data written by the muxer is not always sequential, **make sure to | ||
respect this argument**. | ||
When using `chunked: true` in the options, data created by the muxer will first be accumulated and only written out | ||
once it has reached sufficient size. This is useful for reducing the total amount of writes, at the cost of | ||
latency. It using a default chunk size of 16 MiB, which can be overridden by manually setting `chunkSize` to the | ||
desired byte length. | ||
Note that this target is **not** intended for *live-streaming*, i.e. playback before muxing has finished. | ||
When using `chunked: true`, data created by the muxer will first be accumulated and only written out once it has | ||
reached sufficient size. This is useful for reducing the total amount of writes, at the cost of latency. It using a | ||
default chunk size of 16 MiB, which can be overridden by manually setting `chunkSize` to the desired byte length. | ||
If you want to use this target for *live-streaming*, i.e. playback before muxing has finished, you also need to set | ||
`fastStart: 'fragmented'`. | ||
Usage example: | ||
```js | ||
@@ -158,6 +164,5 @@ import { Muxer, StreamTarget } from 'mp4-muxer'; | ||
let muxer = new Muxer({ | ||
target: new StreamTarget( | ||
(data, position) => { /* Do something with the data */ }, | ||
() => { /* Muxing has finished */ } | ||
), | ||
target: new StreamTarget({ | ||
onData: (data, position) => { /* Do something with the data */ } | ||
}), | ||
fastStart: false, | ||
@@ -203,13 +208,19 @@ // ... | ||
#### `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 | ||
By default, MP4 metadata (track info, sample timing, etc.) is stored at the end of the file - this makes writing the | ||
file faster and easier. However, placing this metadata 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 metadata | ||
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. | ||
- `false`: Disables Fast Start, placing all 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. | ||
requirements. This is the preferred option when using `ArrayBufferTarget` as it will result in a higher-quality | ||
output with no change in memory footprint. | ||
- `'fragmented'`: Produces a _fragmented MP4 (fMP4)_ file, evenly placing sample metadata throughout the file by grouping | ||
it into "fragments" (short sections of media), while placing general metadata at the beginning of the file. | ||
Fragmented files are ideal for streaming, as they are optimized for random access with minimal to no seeking. | ||
Furthermore, they remain lightweight to create no matter how large the file becomes, as they don't require media to | ||
be kept in memory for very long. While fragmented files are not as widely supported as regular MP4 files, this | ||
option provides powerful benefits with very little downsides. Further details [here](#fragmented-mp4-notes). | ||
- `object`: Produces a file with Fast Start by reserving space for metadata when muxing begins. To know | ||
@@ -306,2 +317,23 @@ how many bytes need to be reserved to be safe, you'll have to provide the following data: | ||
### Additional notes about fragmented MP4 files | ||
By breaking up the media and related metadata into small fragments, fMP4 files optimize for random access and are ideal | ||
for streaming, while remaining cheap to write even for long files. However, you should keep these things in mind: | ||
- **Media chunk buffering:** | ||
When muxing a file with a video **and** an audio track, the muxer needs to wait for the chunks from _both_ media | ||
to finalize any given fragment. In other words, it must buffer chunks of one medium if the other medium has not yet | ||
encoded chunks up to that timestamp. For example, should you first encode all your video frames and then encode the | ||
audio afterward, the multiplexer will have to hold all those video frames in memory until the audio chunks start | ||
coming in. This might lead to memory exhaustion should your video be very long. When there is only one media track, | ||
this issue does not arise. So, when muxing a multimedia file, make sure it is somewhat limited in size or the chunks | ||
are encoded in a somewhat interleaved way (like is the case for live media). This will keep memory usage at a | ||
constant low. | ||
- **Video key frame frequency:** | ||
Every track's first sample in a fragment must be a key frame in order to be able to play said fragment without the | ||
knowledge of previous ones. However, this means that the muxer needs to wait for a video key frame to begin a new | ||
fragment. If these key frames are too infrequent, fragments become too large, harming random access. Therefore, | ||
every 5–10 seconds, you should force a video key frame like so: | ||
```js | ||
videoEncoder.encode(frame, { keyFrame: true }); | ||
``` | ||
## Implementation & development | ||
@@ -311,3 +343,3 @@ MP4 files are based on the ISO Base Media Format, which structures its files as a hierarchy of boxes (or atoms). The | ||
[ISO/IEC 14496-1](http://netmedia.zju.edu.cn/multimedia2013/mpeg-4/ISO%20IEC%2014496-1%20MPEG-4%20System%20Standard.pdf), | ||
[ISO/IEC 14496-12](https://web.archive.org/web/20180219054429/http://l.web.umkc.edu/lizhu/teaching/2016sp.video-communication/ref/mp4.pdf) | ||
[ISO/IEC 14496-12](https://web.archive.org/web/20231123030701/https://b.goeswhere.com/ISO_IEC_14496-12_2015.pdf) | ||
and | ||
@@ -314,0 +346,0 @@ [ISO/IEC 14496-14](https://github.com/OpenAnsible/rust-mp4/raw/master/docs/ISO_IEC_14496-14_2003-11-15.pdf). |
Sorry, the diff of this file is not supported yet
138797
3292
346