UNPKG

@remotion/media-parser

Version:

A pure JavaScript library for parsing video files

173 lines (172 loc) 8.38 kB
"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;