UNPKG

sp-streams

Version:

Streamplace Streams for Piping Video Around and Stuff

273 lines (238 loc) 10.6 kB
"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(); }