matroska
Version:
Matroska node parser
503 lines (402 loc) • 10.8 kB
JavaScript
/*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 + "]";
}