gkozlenko/node-video-lib

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

Summary

Maintainability
B
4 hrs
Test Coverage
A
98%
'use strict';

const AmfParser = require('./amf-parser');
const Utils = require('./utils');
const Movie = require('../movie');
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');

class ParserImpl {

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

    parse() {
        // Parse header
        let pos = this._parseHeader();

        // Create movie
        this._createMovie();

        // Parse body
        this._parseBody(pos);

        // Complete movie object
        [this.videoTrack, this.audioTrack].forEach((track) => {
            if (track.extraData && track.samples.length > 0) {
                track.sortSamples();
                track.ensureDuration();
                this.movie.addTrack(track);
            }
        });
        this.movie.ensureDuration();

        // Return movie object
        return this.movie;
    }

    _parseHeader() {
        let buffer = Buffer.allocUnsafe(Utils.HEADER_SIZE);
        let size = this.reader.read(buffer, 0);
        if (size !== Utils.HEADER_SIZE ||
                buffer.toString('ascii', 0, 3) !== Utils.HEADER_PREFIX ||
                buffer[3] !== Utils.HEADER_VERSION) {
            throw new Error('FLV header not found');
        }
        return buffer[8];
    }

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

        // Create video track
        this.videoTrack = new VideoTrack();
        this.videoTrack.timescale = Utils.MOVIE_TIMESCALE;

        // Create audio track
        this.audioTrack = new AudioTrack();
        this.audioTrack.timescale = Utils.MOVIE_TIMESCALE;
    }

    _parseBody(pos) {
        let buffer = Buffer.allocUnsafe(15);
        let size = this.reader.size();
        while (pos < size) {
            pos += this.reader.read(buffer, pos);
            let type = buffer[4];
            if (undefined === type) {
                break;
            }

            let dataSize = buffer.readUIntBE(5, 3);
            let timestamp = (buffer[11] << 24) + buffer.readUIntBE(8, 3);

            // check if we can read this tag
            if (pos + dataSize >= size) {
                break;
            }

            if (Utils.TYPE_SCRIPT === type) {
                this._parseScript(pos, dataSize);
            } else if (Utils.TYPE_AUDIO === type) {
                this._parseAudio(pos, dataSize, timestamp);
            } else if (Utils.TYPE_VIDEO === type) {
                this._parseVideo(pos, dataSize, timestamp);
            }

            pos += dataSize;
        }
    }

    _parseScript(pos, dataSize) {
        let buffer = Buffer.allocUnsafe(dataSize);
        this.reader.read(buffer, pos);

        let data = AmfParser.parse(buffer);
        if (data && data.length > 1) {
            let metaData = data[1];
            if (metaData['duration'] !== undefined) {
                let duration = metaData['duration'] * Utils.MOVIE_TIMESCALE;
                this.movie.duration = duration;
                this.videoTrack.duration = duration;
                this.audioTrack.duration = duration;
            }
            if (metaData['width'] !== undefined) {
                this.videoTrack.width = metaData['width'];
            }
            if (metaData['height'] !== undefined) {
                this.videoTrack.height = metaData['height'];
            }
        }
    }

    _parseAudio(pos, dataSize, timestamp) {
        // Read header
        let headerSize = 2;
        let buffer = Buffer.allocUnsafe(headerSize);
        this.reader.read(buffer, pos);

        // Metadata
        let flags = buffer[0];
        let soundType = flags & 0x01;
        let soundSize = (flags & 0x02) >> 1;
        let soundRate = (flags & 0x0c) >> 2;
        let soundFormat = (flags & 0xf0) >> 4;

        if (Utils.AUDIO_FORMAT_AAC !== soundFormat) {
            return;
        }

        if (0 === buffer[1]) {
            // Update audio track
            this.audioTrack.channels = 1 === soundType ? 2 : 1;
            this.audioTrack.sampleRate = (5512.5 * (1 << soundRate)) << 0;
            this.audioTrack.sampleSize = 1 === soundSize ? 16 : 8;

            // Get codec info
            let extraData = Buffer.allocUnsafe(4 + dataSize - headerSize);
            extraData.write('mp4a');
            this.reader.read(extraData, pos + headerSize, 4);
            this.audioTrack.extraData = extraData;
            this.audioTrack.codec = CodecParser.parse(extraData).codec();
        } else {
            // Get sample info
            let sample = Object.create(AudioSample.prototype);
            sample.timestamp = timestamp;
            sample.timescale = this.audioTrack.timescale;
            sample.size = dataSize - headerSize;
            sample.offset = pos + headerSize;
            if (0 < sample.size) {
                this.audioTrack.samples.push(sample);
            }
        }
    }

    _parseVideo(pos, dataSize, timestamp) {
        // Read header
        let headerSize = 5;
        let buffer = Buffer.allocUnsafe(headerSize);
        this.reader.read(buffer, pos);

        // Metadata
        let flags = buffer[0];
        let videoFormat = flags & 0x0f;
        let frameType = (flags & 0xf0) >> 4;
        let compTime = buffer.readUIntBE(2, 3);

        if (Utils.VIDEO_FORMAT_H264 !== videoFormat) {
            return;
        }

        if (0 === buffer[1]) {
            // Get codec info
            let extraData = Buffer.allocUnsafe(4 + dataSize - headerSize);
            extraData.write('avcC');
            this.reader.read(extraData, pos + headerSize, 4);
            this.videoTrack.extraData = extraData;
            this.videoTrack.codec = CodecParser.parse(extraData).codec();
        } else {
            // Get sample info
            let sample = Object.create(VideoSample.prototype);
            sample.timestamp = timestamp;
            sample.timescale = this.videoTrack.timescale;
            sample.size = dataSize - headerSize;
            sample.offset = pos + headerSize;
            sample.compositionOffset = compTime;
            sample.keyframe = 1 === frameType;
            if (0 < sample.size) {
                this.videoTrack.samples.push(sample);
            }
        }
    }

}

module.exports = ParserImpl;