sp-streams
Version:
Streamplace Streams for Piping Video Around and Stuff
273 lines (238 loc) • 10.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.MpegMungerStream = exports.streamIsAudio = exports.streamIsVideo = undefined;
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
exports.default = mpegMungerStream;
var _stream = require("stream");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
// Most of these constants taken from http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
var PACKET_LENGTH = 188;
var SYNC_BYTE = 0x47; // 71 in decimal
var PES_START_CODE_1 = 0x0;
var PES_START_CODE_2 = 0x0;
var PES_START_CODE_3 = 0x1;
var MIN_LENGTH_PES_HEADER = 6; // Start Code (4) + PES Packet Length (2)
var START_PES_HEADER_SEARCH = 3;
var PES_INDICATOR_BYTE_OFFSET = 7;
var PES_FIRST_TIMESTAMP_BYTE_OFFSET = 9;
var PES_SECOND_TIMESTAMP_BYTE_OFFSET = 14;
var PES_INDICATOR_COMPARATOR = 192;
var PES_INDICATOR_RESULT_BOTH = 192;
var PES_INDICATOR_RESULT_PTS = 128;
var PES_INDICATOR_RESULT_NEITHER = 0;
var MPEGTS_PAYLOAD_UNIT_START_INDICATOR_MASK = 64;
var PES_TIMESTAMP_START_CODE_PTS_ONLY = 2;
var PES_TIMESTAMP_START_CODE_PTS_BOTH = 3;
var PES_TIMESTAMP_START_CODE_DTS_BOTH = 1;
var STREAM_ID_AUDIO_START = 0xc0;
var STREAM_ID_AUDIO_END = 0xdf;
var STREAM_ID_VIDEO_START = 0xe0;
var STREAM_ID_VIDEO_END = 0xef;
var streamIsVideo = exports.streamIsVideo = function streamIsVideo(streamId) {
return streamId >= STREAM_ID_VIDEO_START && streamId <= STREAM_ID_VIDEO_END;
};
var streamIsAudio = exports.streamIsAudio = function streamIsAudio(streamId) {
return streamId >= STREAM_ID_AUDIO_START && streamId <= STREAM_ID_AUDIO_END;
};
// These will need to be better someday.
var warn = function warn(str) {
/*eslint-disable no-console */
console.error(str);
};
var zeroPad = function zeroPad(str, len) {
str = "" + str;
while (str.length < len) {
str = "0" + str;
}
return str;
};
// Debugging tool. Not a good way to preform bitwise math. Obviously.
var dumpByte = function dumpByte(byte) {
// Convert to a twos-complement string
var str = (byte >>> 0).toString(2);
// Zero-pad to eight
str = zeroPad(str, 8);
// console.log(str);
return str;
};
var MpegMungerStream = exports.MpegMungerStream = function (_Transform) {
_inherits(MpegMungerStream, _Transform);
function MpegMungerStream(params) {
_classCallCheck(this, MpegMungerStream);
// 188-length buffer containing remainder data while we wait for the next chunk.
var _this = _possibleConstructorReturn(this, (MpegMungerStream.__proto__ || Object.getPrototypeOf(MpegMungerStream)).call(this, params));
_this.remainder = null;
// What's the length of the data in the remainder?
_this.remainderLength = null;
return _this;
}
_createClass(MpegMungerStream, [{
key: "_rewrite",
value: function _rewrite(chunk, startIdx) {
var sync = chunk.readUInt8(startIdx);
if (sync !== SYNC_BYTE) {
this.end();
// throw new Error("MPEGTS appears to be out of sync.");
}
var payload = chunk.readUInt8(startIdx + 1) & MPEGTS_PAYLOAD_UNIT_START_INDICATOR_MASK;
if (payload === 0) {
// No payload in this packet, we can leave!
return;
}
var searchStartIdx = startIdx + START_PES_HEADER_SEARCH;
var endIdx = startIdx + PACKET_LENGTH - MIN_LENGTH_PES_HEADER;
for (var idx = searchStartIdx; idx < endIdx; idx += 1) {
if (chunk.readUInt8(idx) !== PES_START_CODE_1) {
continue;
}
if (chunk.readUInt8(idx + 1) !== PES_START_CODE_2) {
idx += 1; // We already know next byte ain't a zero.
continue;
}
if (chunk.readUInt8(idx + 2) !== PES_START_CODE_3) {
continue;
}
var streamId = chunk.readUInt8(idx + 3);
var isVideo = streamId >= STREAM_ID_VIDEO_START && streamId <= STREAM_ID_VIDEO_END;
var isAudio = streamId >= STREAM_ID_AUDIO_START && streamId <= STREAM_ID_AUDIO_END;
if (!isVideo && !isAudio) {
idx += 3; // We can skip this whole dang sequence now
continue;
}
this._rewriteHeader(chunk, idx, streamId);
return;
}
}
}, {
key: "_rewriteHeader",
value: function _rewriteHeader(chunk, startIdx, streamId) {
var indicator = chunk.readUInt8(startIdx + PES_INDICATOR_BYTE_OFFSET);
var result = indicator & PES_INDICATOR_COMPARATOR;
var pts = void 0;
var dts = void 0;
if (result === PES_INDICATOR_RESULT_BOTH) {
var ptsIdx = startIdx + PES_FIRST_TIMESTAMP_BYTE_OFFSET;
var dtsIdx = startIdx + PES_SECOND_TIMESTAMP_BYTE_OFFSET;
pts = this._readTimestamp(chunk, ptsIdx);
dts = this._readTimestamp(chunk, dtsIdx);
var newPTS = this.transformPTS(pts, dts);
this.notifyPTS(newPTS);
var newDTS = this.transformDTS(dts, pts);
this._writeTimestamp(chunk, newPTS, ptsIdx, PES_TIMESTAMP_START_CODE_PTS_BOTH);
this._writeTimestamp(chunk, newDTS, dtsIdx, PES_TIMESTAMP_START_CODE_DTS_BOTH);
} else if (result === PES_INDICATOR_RESULT_PTS) {
var _ptsIdx = startIdx + PES_FIRST_TIMESTAMP_BYTE_OFFSET;
pts = this._readTimestamp(chunk, _ptsIdx);
var _newPTS = this.transformPTS(pts, null);
this.notifyPTS(_newPTS);
this._writeTimestamp(chunk, _newPTS, _ptsIdx, PES_TIMESTAMP_START_CODE_PTS_ONLY);
} else if (result === PES_INDICATOR_RESULT_NEITHER) {
// This doesn't happen in my use case, so far as I can tell.
} else {
throw new Error("Unknown indicator result:" + dumpByte(result));
}
this.currentPTS = pts;
this.emit("pts", { pts: pts, streamId: streamId });
}
}, {
key: "_readTimestamp",
value: function _readTimestamp(chunk, startIdx) {
var raw = chunk.readUIntBE(startIdx, 5);
var result = 0;
// Credit: http://stackoverflow.com/questions/13606023/mpeg2-presentation-time-stamps-pts-calculation
result = result | raw >> 3 & 0x0007 << 30;
result = result | raw >> 2 & 0x7fff << 15;
result = result | raw >> 1 & 0x7fff << 0;
// const str = zeroPad(raw.toString(2), 40);
// console.log(`${str.slice(0, 8)} ${str.slice(8, 24)} ${str.slice(24, 40)}`)
return result;
}
}, {
key: "_writeTimestamp",
value: function _writeTimestamp(chunk, value, idx, startCode) {
// Tricky because Node is limited to 32-bit bitwise operations. Yuck!
var hiPart = value >>> 30 | startCode << 4 | 1;
var midPart = (value >>> 15 & ~(-1 << 15)) << 1 | 1;
var loPart = (value & ~(-1 << 15)) << 1 | 1;
// const hiStr = zeroPad(hiPart.toString(2), 8);
// const midStr = zeroPad(midPart.toString(2), 16);
// const loStr = zeroPad(loPart.toString(2), 16);
// console.log(`${hiStr} ${midStr} ${loStr}`);
chunk.writeUIntBE(hiPart, idx, 1);
chunk.writeUIntBE(midPart, idx + 1, 2);
chunk.writeUIntBE(loPart, idx + 3, 2);
}
/**
* Replace this function if you want to learn about the PTS when it happens
*/
}, {
key: "notifyPTS",
value: function notifyPTS(pts) {}
/**
* Default function -- no-op
*/
}, {
key: "transformPTS",
value: function transformPTS(oldPTS, oldDTS) {
return oldPTS;
}
/**
* Default function -- no-op
*/
}, {
key: "transformDTS",
value: function transformDTS(oldDTS, oldPTS) {
return oldDTS;
}
}, {
key: "_transform",
value: function _transform(chunk, enc, next) {
var chunkLength = chunk.length;
var dataStart = 0;
// Index where our data starts.
var startIdx = 0;
// Are we carrying around the remainder of the last chunk? If so, resolve that first.
if (this.remainder !== null) {
var lengthOfRemainderNeeded = PACKET_LENGTH - this.remainderLength;
if (chunk.length < lengthOfRemainderNeeded) {
// We didn't even get enough to copmlete a packet! Add what we got to the remainder and
// move on.
chunk.copy(this.remainder, this.remainderLength, 0, chunk.length);
this.remainderLength = this.remainderLength + chunk.length;
return next();
} else {
chunk.copy(this.remainder, this.remainderLength, 0, lengthOfRemainderNeeded);
this._rewrite(this.remainder, 0);
this.push(this.remainder);
startIdx = lengthOfRemainderNeeded;
}
this.remainder = null;
this.remainderLength = null;
}
var idx = startIdx;
// Main loop.
while (idx + PACKET_LENGTH <= chunkLength) {
// If the remainder of the chunk is < 188 bytes, save it to be combined with the next
// incoming chunk.
this._rewrite(chunk, idx);
idx += PACKET_LENGTH;
}
var endIdx = idx;
if (endIdx < chunkLength) {
this.remainder = new Buffer(PACKET_LENGTH);
this.remainderLength = chunk.length - endIdx;
var bytesCopied = chunk.copy(this.remainder, 0, endIdx);
}
this.push(chunk.slice(startIdx, endIdx));
next();
}
}]);
return MpegMungerStream;
}(_stream.Transform);
function mpegMungerStream() {
return new MpegMungerStream();
}