mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 05:13:50 +02:00
Implement optimized fragmented MP4 demuxing
This commit is contained in:
@@ -39,3 +39,5 @@ export { Source, ArrayBufferSource, BlobSource } from './source';
|
||||
export { ALL_FORMATS, ISOBMFF, MP4, MOV, MATROSKA, MKV, WEBM } from './input-format';
|
||||
export { Input, InputOptions } from './input';
|
||||
export { EncodedVideoChunkDrain, VideoFrameDrain } from './media-drain';
|
||||
|
||||
// 🐡🦔
|
||||
|
||||
@@ -20,8 +20,6 @@ class IsobmffInputFormat extends InputFormat {
|
||||
return false;
|
||||
}
|
||||
|
||||
await input._mainReader.loadRange(4, 8);
|
||||
|
||||
const isobmffReader = new IsobmffReader(input._mainReader);
|
||||
isobmffReader.pos = 4;
|
||||
const fourCc = isobmffReader.readAscii(4);
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { AudioCodec, MediaCodec, VideoCodec } from './codec';
|
||||
import { Rotation } from './misc';
|
||||
|
||||
export interface InputTrackBacking {
|
||||
getCodec(): Promise<MediaCodec>;
|
||||
@@ -34,7 +35,7 @@ export interface InputVideoTrackBacking extends InputTrackBacking {
|
||||
getCodec(): Promise<VideoCodec>;
|
||||
getWidth(): Promise<number>;
|
||||
getHeight(): Promise<number>;
|
||||
getRotation(): Promise<number>;
|
||||
getRotation(): Promise<Rotation>;
|
||||
getDecoderConfig(): Promise<VideoDecoderConfig>;
|
||||
getFirstChunk(): Promise<EncodedVideoChunk | null>;
|
||||
getChunk(timestamp: number): Promise<EncodedVideoChunk | null>;
|
||||
|
||||
@@ -30,6 +30,8 @@ export class Input {
|
||||
/** @internal */
|
||||
_getDemuxer() {
|
||||
return this._demuxerPromise ??= (async () => {
|
||||
await this._mainReader.loadRange(0, 4096); // Load the first 4 kiB so we can determine the format
|
||||
|
||||
for (const format of this._formats) {
|
||||
const canRead = await format._canReadInput(this);
|
||||
if (canRead) {
|
||||
|
||||
@@ -989,7 +989,7 @@ export const tfra = (trackData: IsobmffTrackData, trackIndex: number) => {
|
||||
u32(0b111111), // This specifies that traf number, trun number and sample number are 32-bit ints
|
||||
u32(trackData.finalizedChunks.length), // Number of entries
|
||||
trackData.finalizedChunks.map(chunk => [
|
||||
u64(intoTimescale(chunk.startTimestamp, trackData.timescale)), // Time
|
||||
u64(intoTimescale(chunk.samples[0]!.timestamp, trackData.timescale)), // Time (in presentation time)
|
||||
u64(chunk.moofOffset!), // moof offset
|
||||
u32(trackIndex + 1), // traf number
|
||||
u32(1), // trun number
|
||||
|
||||
+955
-40
File diff suppressed because it is too large
Load Diff
@@ -701,20 +701,22 @@ export class IsobmffMuxer extends Muxer {
|
||||
this.boxWriter.writeBox(movieBox);
|
||||
}
|
||||
|
||||
// Not all tracks need to be present in every fragment
|
||||
const tracksInFragment = this.trackDatas.filter(x => x.currentChunk);
|
||||
|
||||
// Write out an initial moof box; will be overwritten later once actual chunk offsets are known
|
||||
const moofOffset = this.writer.getPos();
|
||||
const moofBox = moof(fragmentNumber, this.trackDatas);
|
||||
const moofBox = moof(fragmentNumber, tracksInFragment);
|
||||
this.boxWriter.writeBox(moofBox);
|
||||
|
||||
// Create the mdat box
|
||||
{
|
||||
const mdatBox = mdat(false); // Initially assume no fragment is larger than 4 GiB
|
||||
const mdatBox = mdat(false); // Initially assume the fragment is not larger than 4 GiB
|
||||
let totalTrackSampleSize = 0;
|
||||
|
||||
// Compute the size of the mdat box
|
||||
for (const trackData of this.trackDatas) {
|
||||
assert(trackData.currentChunk);
|
||||
for (const sample of trackData.currentChunk.samples) {
|
||||
for (const trackData of tracksInFragment) {
|
||||
for (const sample of trackData.currentChunk!.samples) {
|
||||
totalTrackSampleSize += sample.size;
|
||||
}
|
||||
}
|
||||
@@ -731,7 +733,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
|
||||
// Write sample data
|
||||
for (const trackData of this.trackDatas) {
|
||||
for (const trackData of tracksInFragment) {
|
||||
trackData.currentChunk!.offset = this.writer.getPos();
|
||||
trackData.currentChunk!.moofOffset = moofOffset;
|
||||
|
||||
@@ -744,11 +746,11 @@ export class IsobmffMuxer extends Muxer {
|
||||
// Now that we set the actual chunk offsets, fix the moof box
|
||||
const endPos = this.writer.getPos();
|
||||
this.writer.seek(this.boxWriter.offsets.get(moofBox)!);
|
||||
const newMoofBox = moof(fragmentNumber, this.trackDatas);
|
||||
const newMoofBox = moof(fragmentNumber, tracksInFragment);
|
||||
this.boxWriter.writeBox(newMoofBox);
|
||||
this.writer.seek(endPos);
|
||||
|
||||
for (const trackData of this.trackDatas) {
|
||||
for (const trackData of tracksInFragment) {
|
||||
trackData.finalizedChunks.push(trackData.currentChunk!);
|
||||
this.finalizedChunks.push(trackData.currentChunk!);
|
||||
trackData.currentChunk = null;
|
||||
|
||||
@@ -4,6 +4,8 @@ export function assert(x: unknown): asserts x {
|
||||
}
|
||||
}
|
||||
|
||||
export type Rotation = 0 | 90 | 180 | 270;
|
||||
|
||||
/** @public */
|
||||
export type TransformationMatrix = [number, number, number, number, number, number, number, number, number];
|
||||
|
||||
|
||||
+25
-11
@@ -1,8 +1,6 @@
|
||||
import { assert, binarySearchLessOrEqual, removeItem } from './misc';
|
||||
import { Source } from './source';
|
||||
|
||||
const PAGE_SIZE = 4096;
|
||||
|
||||
type ReadSegment = {
|
||||
start: number;
|
||||
end: number;
|
||||
@@ -27,32 +25,29 @@ export class Reader {
|
||||
constructor(public source: Source, public maxStorableBytes = Infinity) {}
|
||||
|
||||
async loadRange(start: number, end: number) {
|
||||
// Read rounded to the nearest page
|
||||
const alignedStart = Math.floor(start / PAGE_SIZE) * PAGE_SIZE;
|
||||
let alignedEnd = Math.ceil(end / PAGE_SIZE) * PAGE_SIZE;
|
||||
alignedEnd = Math.min(alignedEnd, await this.source._getSize());
|
||||
end = Math.min(end, await this.source._getSize());
|
||||
|
||||
const matchingLoadingSegment = this.loadingSegments.find(x => x.start <= alignedStart && x.end >= alignedEnd);
|
||||
const matchingLoadingSegment = this.loadingSegments.find(x => x.start <= start && x.end >= end);
|
||||
if (matchingLoadingSegment) {
|
||||
// Simply wait for the existing promise to finish to avoid loading the same range twice
|
||||
await matchingLoadingSegment.promise;
|
||||
return;
|
||||
}
|
||||
|
||||
const encasingSegmentExists = this.loadedSegments.some(x => x.start <= alignedStart && x.end >= alignedEnd);
|
||||
const encasingSegmentExists = this.loadedSegments.some(x => x.start <= start && x.end >= end);
|
||||
if (encasingSegmentExists) {
|
||||
// Nothing to load
|
||||
return;
|
||||
}
|
||||
|
||||
const bytesPromise = this.source._read(alignedStart, alignedEnd);
|
||||
const loadingSegment: LoadingSegment = { start: alignedStart, end: alignedEnd, promise: bytesPromise };
|
||||
const bytesPromise = this.source._read(start, end);
|
||||
const loadingSegment: LoadingSegment = { start, end, promise: bytesPromise };
|
||||
this.loadingSegments.push(loadingSegment);
|
||||
|
||||
const bytes = await bytesPromise;
|
||||
removeItem(this.loadingSegments, loadingSegment);
|
||||
|
||||
this.insertIntoLoadedSegments(alignedStart, bytes);
|
||||
this.insertIntoLoadedSegments(start, bytes);
|
||||
}
|
||||
|
||||
private insertIntoLoadedSegments(start: number, bytes: Uint8Array) {
|
||||
@@ -136,4 +131,23 @@ export class Reader {
|
||||
offset: segment.bytes.byteOffset + start - segment.start,
|
||||
};
|
||||
}
|
||||
|
||||
forgetRange(start: number, end: number) {
|
||||
if (end <= start) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = binarySearchLessOrEqual(this.loadedSegments, start, x => x.start);
|
||||
if (startIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segment = this.loadedSegments[startIndex]!;
|
||||
if (segment.start !== start || segment.end !== end) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedSegments.splice(startIndex, 1);
|
||||
this.totalStoredBytes -= segment.bytes.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user