UNPKG

matroska

Version:
503 lines (402 loc) 10.8 kB
/*jslint node: true, vars: true, nomen: true */ 'use strict'; var async = require('async'); var fs = require('fs'); var util = require('util'); var crc32 = require('crc').crc32; var debug = require('debug')('matroska:document'); var schema = require('./schema'); var Element = require('./element'); var tools = require('./tools'); // we have to require all of them to be browserify/webpack compatible // find dom | grep ".js" | grep -v _proto | while read line; do echo "require('./$line');"; done require('./dom/attachedFile.js'); require('./dom/attachments.js'); require('./dom/audio.js'); require('./dom/crc-32.js'); require('./dom/cuePoint.js'); require('./dom/cueReference.js'); require('./dom/cues.js'); require('./dom/cueTrackPositions.js'); require('./dom/element1.js'); require('./dom/info.js'); require('./dom/masterElement.js'); require('./dom/seek.js'); require('./dom/seekHead.js'); require('./dom/segment.js'); require('./dom/segment1.js'); require('./dom/segment2.js'); require('./dom/segment3.js'); require('./dom/simpleTag.js'); require('./dom/tag.js'); require('./dom/tags.js'); require('./dom/targets.js'); require('./dom/trackEntry.js'); require('./dom/tracks.js'); require('./dom/video.js'); var tagClasses = {}; tagClasses[schema.byName.Segment] = "segment3"; tagClasses[schema.byName.Attachments] = "attachments"; tagClasses[schema.byName.AttachedFile] = "attachedFile"; tagClasses[schema.byName.Tags] = "tags"; tagClasses[schema.byName.Tag] = "tag"; tagClasses[schema.byName.Targets] = "targets"; tagClasses[schema.byName.SimpleTag] = "simpleTag"; tagClasses[schema.byName.SeekHead] = "seekHead"; tagClasses[schema.byName.Seek] = "seek"; tagClasses[schema.byName.Info] = "info"; tagClasses[schema.byName.Tracks] = "tracks"; tagClasses[schema.byName.TrackEntry] = "trackEntry"; tagClasses[schema.byName.Video] = "video"; tagClasses[schema.byName.Audio] = "audio"; tagClasses[schema.byName.CRC_32] = "crc-32"; tagClasses[schema.byName.Cues] = "cues"; tagClasses[schema.byName.CuePoint] = "cuePoint"; tagClasses[schema.byName.CueReference] = "cueReference"; tagClasses[schema.byName.CueTrackPositions] = "cueTrackPositions"; /** * @constructs * @private */ function Document() { this.type = "D"; this._name = "Document"; this.tagId = 0; this._nextTagId = 1; this.ownerDocument = this; this.masterType = true; } util.inherits(Document, Element); module.exports = Document; Document.prototype.createElement = function(ebmlID, start, length) { var element; var tagClass = tagClasses[ebmlID]; if (tagClass) { if (typeof (tagClass) === "string") { tagClass = require('./dom/' + tagClass); tagClasses[ebmlID] = tagClass; } element = new tagClass(this, this._nextTagId++, start, length); } else { element = new Element(this, this._nextTagId++, ebmlID, start, length); } return element; }; /** * @private */ Document.prototype._registerPosition = function(tag) { if (!this._positions) { this._positions = []; } this._positions.push(tag); if (this._modified) { tag._markModified(); } }; function removeList(list, tag) { if (!list || !list.length) { return false; } var idx = list.indexOf(tag); if (idx < 0) { return false; } list.splice(idx, 1); return true; } /** * @private */ Document.prototype._unregisterPosition = function(tag) { return removeList(this._positions, tag); }; /** * @private */ Document.prototype._registerCRC = function(tag) { if (!this._crcs) { this._crcs = []; } // console.log("Register tag #" + tag.tagId); this._crcs.push(tag); }; /** * @private */ Document.prototype._unregisterCRC = function(tag) { return removeList(this._crcs, tag); }; /** * @private */ Document.prototype._markModified = function() { if (this._modified) { return; } if (this._partial) { throw new Error("Can not modify a partial parsed document"); } this._modified = true; if (!this._positions) { return; } this._positions.forEach(function(child) { child._markModified(); }); }; /** * @private */ Document.prototype._buildLinks = function() { if (this._linksBuilt) { return; } this._linksBuilt = true; if (!this._positions) { return; } var self = this; this._positions.forEach(function(child) { var offset = child.getValue(); var parent1 = child.getLevel1(); var targetType = child._positionTargetType; switch (targetType) { case "segment": break; case "clusterRelative": var cp = child.parent.cueClusterPosition; if (!cp) { console.log("parent=", child.parent); throw new Error("Invalid cluster relative without a cueClusterPosition"); } offset += cp._positionTarget ? cp._positionTarget.getContentPosition() : cp; // this assumed _positionTarget, but usually cp is simply an integer break; case "cluster": default: throw new Error("Not supported ! (" + targetType + ")"); } var target = parent1.getTagByPosition(offset, true); if (debug.enabled) { debug("Position #" + parent1.tagId + " " + offset + "=> " + (target ? ("#" + target.tagID) : "null")); } if (!target || target.position !== "start") { // In partial mode, some nodes might not be loaded ! if (!self._partial) { debug("Can not find target for offset=" + offset + " doc=" + self); } return; } child._positionTarget = target.target; }); }; /** * Write MKV file * * @param {(Stream|string)} * stream Stream or path of the destination file * @param {Object} * [options] Options object * @param {function} * callback Called when done */ Document.prototype.write = function(stream, options, callback) { if (typeof (options) === "function" && arguments.length === 2) { callback = options; options = null; } options = options || {}; var self = this; if (this._partial) { return callback(new Error("The document is not complete")) } this._prepareDocument(options, function(error) { if (error) { return callback(error); } // console.log("Computing positions ..."); self._computePositions(function(error) { if (error) { return callback(error); } var source = self.source; self._updateCRC32(function(error) { if (error) { return callback(error); } var closeStream = false; if (typeof (stream) === "string") { console.log("Write to file ", stream); stream = fs.createWriteStream(stream); closeStream = true; } var writeSession = { stream: stream, options: options }; self._write(writeSession, source, function(error) { if (error) { return callback(error); } source.end(writeSession, function(error) { if (error) { return callback(error); } if (!closeStream) { return callback(); } // console.error("Close stream"); stream.end(callback); }); }); }); }); }); }; /** * @private */ Document.prototype._prepareDocument = function(options, callback) { return callback(); }; /** * @private */ Document.prototype._write = function(output, source, callback) { async.eachSeries(this.children, function(child, callback) { child._write(output, source, callback); }, callback); }; /** * @private */ Document.prototype._computePositions = function(estimatedFileSize, callback) { if (!this._positions || !this._positions.length) { return callback(); } if (typeof (estimatedFileSize) === 'function') { callback = estimatedFileSize; estimatedFileSize = null; } if (!estimatedFileSize) { estimatedFileSize = 0; this.children.forEach(function(child) { var s = child._getSize(); estimatedFileSize += s; }); } var bits = tools.sizeUInt(estimatedFileSize) * 8; var max = Math.pow(2, bits) - 1; // console.log("Estimated size=" + estimatedFileSize + " max=" + max + " // bits=" + bits); var error = false; this._positions.forEach(function(child) { child.setUInt(max); var target = child._positionTarget; if (!target) { console.error("No target for tag '" + child._name + "' #" + child.tagId); error = true; return; } if (false) { console.error("Target has been removed target=" + target._name + "#" + target.tagId); error = true; return; } var level1s = child.getLevel1(); var level1t = target.getLevel1(); if (level1s !== level1t) { console.error("Target and source have not the same Level1 target=" + target._name + "#" + target.tagId); error = true; return; } }); if (error) { return callback(error); } var tx = 1; var times = 5; var changes = 1; for (; changes && times; times--, tx++) { changes = 0; this._positions.forEach(function(child) { var target = child._positionTarget; var level1 = target.getLevel1(); var newPosition = target.getPosition() - level1.getContentPosition(); var oldPosition = child.getUInt(); if (newPosition === oldPosition) { return; } // console.log("Change position old=" + child.getUInt() + " new=" + // newPosition); child.setUInt(newPosition); changes++; }); console.log("Position pass #" + tx + ": " + changes + " modified positions"); } return callback(); }; Document.prototype._updateCRC32 = function(callback) { var crcs = this._crcs; if (!crcs || !crcs.length) { console.log("No crc to compute ", crcs); return callback(); } console.log(crcs.length + " crc to compute ..."); function depth(t) { var ret = 0; for (; t; t = t.parent) { ret++; } return ret; } crcs.sort(function(t1, t2) { return depth(t2) - depth(t1); }); async.eachSeries(crcs, function(crcTag, callback) { var parent = crcTag.parent; // console.log("Compute crc #" + crcTag.tagId + " parent #" + parent.tagId); if (false && !parent._modified) { // console.log("Compute crc #" + parent.tagId + " => not modified"); return callback(); } parent._computeChildrenCRC(true, null, function(error, crc) { if (error) { return callback(error); } if (crcTag.data) { var oldCRC = crcTag.getCRCValue(); if (crc == oldCRC) { // console.log("No change keep old CRC #" + crcTag.tagId + " 0x", // crc.toString(16)); return callback(); } // console.log("Change CRC #" + crcTag.tagId + " new=", crc, " old=", // oldCRC); } crcTag.setCRCValue(crc); callback(); }); }, callback); }; /** * Get tag by its tag identifier (number) * * @param {number} * tagId Tag identifier * @returns {Tag} The found tag or null */ Document.prototype.getTagById = function(tagId) { if (isNaN(tagId)) { throw new Error("Tag identifier is not supported (" + tagId + ")"); } var ret = this.deepWalk(function(child) { if (child.tagId === tagId) { return child; } }); return ret; }; Document.prototype.toString = function() { return "[Document source=" + this.source + "]"; }