@remotion/media-parser
Version:
A pure JavaScript library for parsing video files
173 lines (172 loc) • 8.38 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseMdatSection = void 0;
const convert_audio_or_video_sample_1 = require("../../../convert-audio-or-video-sample");
const get_tracks_1 = require("../../../get-tracks");
const log_1 = require("../../../log");
const skip_1 = require("../../../skip");
const cached_sample_positions_1 = require("../../../state/iso-base-media/cached-sample-positions");
const last_moof_box_1 = require("../../../state/iso-base-media/last-moof-box");
const may_skip_video_data_1 = require("../../../state/may-skip-video-data");
const video_section_1 = require("../../../state/video-section");
const get_moov_atom_1 = require("../get-moov-atom");
const calculate_jump_marks_1 = require("./calculate-jump-marks");
const postprocess_bytes_1 = require("./postprocess-bytes");
const parseMdatSection = async (state) => {
const mediaSection = (0, video_section_1.getCurrentMediaSection)({
offset: state.iterator.counter.getOffset(),
mediaSections: state.mediaSection.getMediaSections(),
});
if (!mediaSection) {
throw new Error('No video section defined');
}
const endOfMdat = mediaSection.size + mediaSection.start;
// don't need mdat at all, can skip
if ((0, may_skip_video_data_1.maySkipVideoData)({ state })) {
const mfra = state.iso.mfra.getIfAlreadyLoaded();
if (mfra) {
const lastMoof = (0, last_moof_box_1.getLastMoofBox)(mfra);
if (lastMoof && lastMoof > endOfMdat) {
log_1.Log.verbose(state.logLevel, 'Skipping to last moof', lastMoof);
return (0, skip_1.makeSkip)(lastMoof);
}
}
return (0, skip_1.makeSkip)(endOfMdat);
}
// if we only need the first and last sample, we may skip over the samples in the middle
// this logic skips the samples in the middle for a fragmented mp4
if ((0, may_skip_video_data_1.maySkipOverSamplesInTheMiddle)({ state })) {
const mfra = state.iso.mfra.getIfAlreadyLoaded();
if (mfra) {
const lastMoof = (0, last_moof_box_1.getLastMoofBox)(mfra);
// we require that all moof boxes of both tracks have been processed, for correct duration calculation
const firstMax = (0, last_moof_box_1.getMaxFirstMoofOffset)(mfra);
const mediaSectionsBiggerThanMoof = state.mediaSection
.getMediaSections()
.filter((m) => m.start > firstMax).length;
if (mediaSectionsBiggerThanMoof > 1 && lastMoof && lastMoof > endOfMdat) {
log_1.Log.verbose(state.logLevel, 'Skipping to last moof because only first and last samples are needed');
return (0, skip_1.makeSkip)(lastMoof);
}
}
}
const alreadyHasMoov = (0, get_tracks_1.getHasTracks)(state, true);
if (!alreadyHasMoov) {
const moov = await (0, get_moov_atom_1.getMoovAtom)({
endOfMdat,
state,
});
state.iso.moov.setMoovBox({
moovBox: moov,
precomputed: false,
});
state.callbacks.tracks.setIsDone(state.logLevel);
state.structure.getIsoStructure().boxes.push(moov);
return (0, exports.parseMdatSection)(state);
}
if (!state.iso.flatSamples.getSamples(mediaSection.start)) {
const flattedSamples = (0, cached_sample_positions_1.calculateFlatSamples)({
state,
mediaSectionStart: mediaSection.start,
});
const calcedJumpMarks = (0, calculate_jump_marks_1.calculateJumpMarks)(flattedSamples, endOfMdat);
state.iso.flatSamples.setJumpMarks(mediaSection.start, calcedJumpMarks);
state.iso.flatSamples.setSamples(mediaSection.start, flattedSamples.flat(1));
}
const flatSamples = state.iso.flatSamples.getSamples(mediaSection.start);
const jumpMarks = state.iso.flatSamples.getJumpMarks(mediaSection.start);
const { iterator } = state;
const samplesWithIndex = flatSamples.find((sample) => {
return sample.samplePosition.offset === iterator.counter.getOffset();
});
if (!samplesWithIndex) {
// There are various reasons why in mdat we find weird stuff:
// - iphonevideo.hevc has a fake hoov atom which is not mapped
// - corrupted.mp4 has a corrupt table
const nextSample_ = flatSamples
.filter((s) => s.samplePosition.offset > iterator.counter.getOffset())
.sort((a, b) => a.samplePosition.offset - b.samplePosition.offset)[0];
if (nextSample_) {
iterator.discard(nextSample_.samplePosition.offset - iterator.counter.getOffset());
return null;
}
// guess we reached the end!
// iphonevideo.mov has extra padding here, so let's make sure to jump ahead
log_1.Log.verbose(state.logLevel, 'Could not find sample at offset', iterator.counter.getOffset(), 'skipping to end of mdat');
return (0, skip_1.makeSkip)(endOfMdat);
}
// Corrupt file: Sample is beyond the end of the file. Don't process it.
if (samplesWithIndex.samplePosition.offset +
samplesWithIndex.samplePosition.size >
state.contentLength) {
log_1.Log.verbose(state.logLevel, "Sample is beyond the end of the file. Don't process it.", samplesWithIndex.samplePosition.offset +
samplesWithIndex.samplePosition.size, endOfMdat);
return (0, skip_1.makeSkip)(endOfMdat);
}
// Need to fetch more data
if (iterator.bytesRemaining() < samplesWithIndex.samplePosition.size) {
return (0, skip_1.makeFetchMoreData)(samplesWithIndex.samplePosition.size - iterator.bytesRemaining());
}
const { timestamp: rawCts, decodingTimestamp: rawDts, duration, isKeyframe, offset, bigEndian, chunkSize, } = samplesWithIndex.samplePosition;
const { originalTimescale, startInSeconds } = samplesWithIndex.track;
const cts = rawCts + startInSeconds * originalTimescale;
const dts = rawDts + startInSeconds * originalTimescale;
const bytes = (0, postprocess_bytes_1.postprocessBytes)({
bytes: iterator.getSlice(samplesWithIndex.samplePosition.size),
bigEndian,
chunkSize,
});
if (samplesWithIndex.track.type === 'audio') {
const audioSample = (0, convert_audio_or_video_sample_1.convertAudioOrVideoSampleToWebCodecsTimestamps)({
sample: {
data: bytes,
timestamp: cts,
duration,
decodingTimestamp: dts,
type: isKeyframe ? 'key' : 'delta',
offset,
},
timescale: originalTimescale,
});
await state.callbacks.onAudioSample({
audioSample,
trackId: samplesWithIndex.track.trackId,
});
}
if (samplesWithIndex.track.type === 'video') {
// https://remotion-assets.s3.eu-central-1.amazonaws.com/example-videos/sei_checkpoint.mp4
// Position in file 0x0001aba615
// https://github.com/remotion-dev/remotion/issues/4680
// In Chrome, we may not treat recovery points as keyframes
// otherwise "a keyframe is required after flushing"
const nalUnitType = bytes[4] & 0b00011111;
let isRecoveryPoint = false;
// SEI (Supplemental enhancement information)
if (nalUnitType === 6) {
const seiType = bytes[5];
isRecoveryPoint = seiType === 6;
}
const videoSample = (0, convert_audio_or_video_sample_1.convertAudioOrVideoSampleToWebCodecsTimestamps)({
sample: {
data: bytes,
timestamp: cts,
duration,
decodingTimestamp: dts,
type: isKeyframe && !isRecoveryPoint ? 'key' : 'delta',
offset,
},
timescale: originalTimescale,
});
await state.callbacks.onVideoSample({
videoSample,
trackId: samplesWithIndex.track.trackId,
});
}
const jump = jumpMarks.find((j) => j.afterSampleWithOffset === offset);
if (jump) {
log_1.Log.verbose(state.logLevel, 'Found jump mark', jump.jumpToOffset, 'skipping to jump mark');
return (0, skip_1.makeSkip)(jump.jumpToOffset);
}
return null;
};
exports.parseMdatSection = parseMdatSection;