gkozlenko/node-video-lib

View on GitHub
lib/mp4/parser-impl.js

Summary

Maintainability
D
2 days
Test Coverage
A
93%
'use strict';

const Utils = require('./utils');
const Movie = require('../movie');
const AtomMoov = require('./atoms/atom-moov');
const AudioTrack = require('../audio-track');
const VideoTrack = require('../video-track');
const AudioSample = require('../audio-sample');
const VideoSample = require('../video-sample');
const SourceReader = require('../readers/source-reader');
const CodecParser = require('../codecs/parser');
const BufferUtils = require('../buffer-utils');

class ParserImpl {

    constructor(source) {
        this.source = source;
        this.reader = SourceReader.create(this.source);
    }

    parse() {
        // Get moov atom
        this._findMoovAtom();

        // Create movie
        this._createMovie();

        // Create tracks
        let trakAtoms = this.moovAtom.getAtoms(Utils.ATOM_TRAK);
        for (let trakAtom of trakAtoms) {
            this._createTrack(trakAtom);
        }

        // Complete movie object
        this.movie.tracks.forEach((track) => {
            track.sortSamples();
            track.ensureDuration();
        });
        this.movie.ensureDuration();

        // Return movie object
        return this.movie;
    }

    _findMoovAtom() {
        this.moovAtom = null;

        let pos = 0;
        let size = this.reader.size();
        let buffer = Buffer.allocUnsafe(8);
        while (pos < size) {
            this.reader.read(buffer, pos);
            let headerSize = 8;
            let atomSize = buffer.readUInt32BE(0);
            let atomType = buffer.toString('ascii', 4);
            if (atomSize === 0) {
                atomSize = size - pos;
            } else if (atomSize === 1) {
                this.reader.read(buffer, pos + buffer.length);
                atomSize = BufferUtils.readUInt64BE(buffer, 0);
                headerSize += 8;
            }
            if (Utils.ATOM_MOOV === atomType) {
                let buffer = Buffer.allocUnsafe(atomSize - headerSize);
                if (this.reader.read(buffer, pos + headerSize) === buffer.length) {
                    this.moovAtom = new AtomMoov();
                    this.moovAtom.parse(buffer);
                    break;
                }
            } else {
                pos += atomSize;
            }
        }

        if (!this.moovAtom) {
            throw new Error('MOOV atom not found');
        }
    }

    _createMovie() {
        // Create movie
        this.movie = new Movie();

        // Add meta information
        let mvhdAtom = this.moovAtom.getAtom(Utils.ATOM_MVHD);
        if (mvhdAtom) {
            this.movie.timescale = mvhdAtom.timescale;
            this.movie.duration = mvhdAtom.duration;
        } else {
            throw new Error('MVHD atom not found');
        }
    }

    _createTrack(trakAtom) {
        let mdiaAtom = trakAtom.getAtom(Utils.ATOM_MDIA);
        if (mdiaAtom === null) {
            return;
        }

        let hdlrAtom = mdiaAtom.getAtom(Utils.ATOM_HDLR);
        let mdhdAtom = mdiaAtom.getAtom(Utils.ATOM_MDHD);
        let minfAtom = mdiaAtom.getAtom(Utils.ATOM_MINF);
        if (hdlrAtom === null || mdhdAtom === null || minfAtom === null) {
            return;
        }

        let stblAtom = minfAtom.getAtom(Utils.ATOM_STBL);
        if (stblAtom === null) {
            return;
        }

        let stsdAtom = stblAtom.getAtom(Utils.ATOM_STSD);
        let track = null;
        let samplePrototype = null;

        if (Utils.TRACK_TYPE_AUDIO === hdlrAtom.handlerType) {
            let audioAtom = stsdAtom.getAudioAtom();
            if (audioAtom !== null && audioAtom.extraData !== null) {
                track = new AudioTrack();
                samplePrototype = AudioSample.prototype;
                track.channels = audioAtom.channels;
                track.sampleRate = audioAtom.sampleRate;
                track.sampleSize = audioAtom.sampleSize;
                track.extraData = audioAtom.extraData;
            }
        } else if (Utils.TRACK_TYPE_VIDEO === hdlrAtom.handlerType) {
            let videoAtom = stsdAtom.getVideoAtom();
            if (videoAtom !== null && videoAtom.extraData !== null) {
                track = new VideoTrack();
                samplePrototype = VideoSample.prototype;
                track.width = videoAtom.width;
                track.height = videoAtom.height;
                track.extraData = videoAtom.extraData;
            }
        }

        if (track === null) {
            return;
        }

        track.duration = mdhdAtom.duration;
        track.timescale = mdhdAtom.timescale;
        let codecInfo = CodecParser.parse(track.extraData);
        if (codecInfo !== null) {
            track.codec = codecInfo.codec();
        }

        // Get needed data to build samples
        let compositions = ParserImpl._getEntries(stblAtom, Utils.ATOM_CTTS);
        let sampleSizes = ParserImpl._getEntries(stblAtom, Utils.ATOM_STSZ);
        let samplesToChunk = ParserImpl._getEntries(stblAtom, Utils.ATOM_STSC);
        let syncSamples = ParserImpl._getEntries(stblAtom, Utils.ATOM_STSS);
        let timeSamples = ParserImpl._getEntries(stblAtom, Utils.ATOM_STTS);
        let chunkOffsets = ParserImpl._getEntries(stblAtom, Utils.ATOM_STCO);
        if (chunkOffsets.length === 0) {
            chunkOffsets = ParserImpl._getEntries(stblAtom, Utils.ATOM_CO64);
        }

        let currentTimestamp = 0;
        let currentChunk = 0;
        let currentChunkOffset = 0;
        let currentChunkNumbers = 0;
        let currentSampleChunk = 0;
        let currentCompositionIndex = 0;
        let currentCompositionCount = 0;
        let index = 0;
        let indexKeyframe = 0;
        let samplesPerChunk = 0;
        if (samplesToChunk.length > 0) {
            currentSampleChunk = samplesToChunk[0];
            samplesPerChunk = samplesToChunk[1];
        }

        // check edit list table
        let edtsAtom = trakAtom.getAtom(Utils.ATOM_EDTS);
        if (edtsAtom !== null) {
            let editEntries = ParserImpl._getEntries(edtsAtom, Utils.ATOM_ELST);
            if (editEntries.length >= 3 && editEntries[1] === -1) {
                // apply the first empty edit
                currentTimestamp = (editEntries[0] * track.timescale / this.movie.timescale) >>> 0;
            }
        }

        // Build samples
        let samples = new Array(sampleSizes.length);
        let pos = 0;
        for (let i = 0, l = timeSamples.length; i < l; i += 2) {
            let sampleDuration = timeSamples[i + 1] || 0;
            for (let j = 0; j < timeSamples[i]; j++) {
                let sample = Object.create(samplePrototype);
                sample.timestamp = currentTimestamp;
                sample.timescale = track.timescale;
                sample.size = sampleSizes[index];
                sample.offset = chunkOffsets[currentChunk] + currentChunkOffset;
                if (track instanceof VideoTrack) {
                    let compositionOffset = 0;
                    if (2 * currentCompositionIndex + 1 < compositions.length) {
                        compositionOffset = compositions[2 * currentCompositionIndex + 1] || 0;
                        currentCompositionCount++;
                        if (currentCompositionCount >= compositions[2 * currentCompositionIndex]) {
                            currentCompositionIndex++;
                            currentCompositionCount = 0;
                        }
                    }
                    sample.compositionOffset = compositionOffset;
                    if (indexKeyframe < syncSamples.length && syncSamples[indexKeyframe] === index + 1) {
                        sample.keyframe = true;
                        indexKeyframe++;
                    } else {
                        sample.keyframe = false;
                    }
                }
                if (sample.size > 0) {
                    samples[pos++] = sample;
                }

                currentChunkNumbers++;
                if (currentChunkNumbers < samplesPerChunk) {
                    currentChunkOffset += sampleSizes[index];
                } else {
                    currentChunkNumbers = 0;
                    currentChunkOffset = 0;
                    currentChunk++;
                    if (currentSampleChunk * 3 + 1 < samplesToChunk.length) {
                        if (currentChunk + 1 >= samplesToChunk[3 * currentSampleChunk]) {
                            samplesPerChunk = samplesToChunk[3 * currentSampleChunk + 1];
                            currentSampleChunk++;
                        }
                    }
                }

                currentTimestamp += sampleDuration;
                index++;
            }
        }

        if (pos < samples.length) {
            track.samples = samples.slice(0, pos);
        } else {
            track.samples = samples;
        }

        if (track.extraData && track.samples.length > 0) {
            this.movie.addTrack(track);
        }
    }

    static _getEntries(stblAtom, type) {
        let entries = [];
        let atom = stblAtom.getAtom(type);
        if (atom !== null) {
            entries = atom.entries;
        }
        return entries;
    }

}

module.exports = ParserImpl;