UNPKG

@ntrip/caster

Version:
365 lines (364 loc) 14.7 kB
"use strict"; /* * This file is part of the @ntrip/caster distribution (https://github.com/node-ntrip/caster). * Copyright (c) 2020 Nebojsa Cvetkovic. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.NtripRtpSession = exports.NtripRtpMessageType = exports.RtpPacketPayloadStream = exports.RtpSession = exports.RtpPacket = void 0; const crypto = require("crypto"); const stream = require("stream"); /** * RTP packet * * Read only class representing an RTP packet, with helper functions for parsing/serialization. */ class RtpPacket { constructor({ payloadType, padding = false, marker = false, sequenceNumber, timestamp, ssrc, csrc = [], extension, payload }) { this.padding = padding; this.marker = marker; this.payloadType = payloadType; this.sequenceNumber = sequenceNumber; this.timestamp = timestamp; this.ssrc = ssrc; this.csrc = csrc; this.extension = extension; this.payload = payload; } get length() { var _a, _b; return 0x0c + (this.csrc.length * 4) + (this.extension !== undefined ? this.extension.length + 4 : 0) + ((_b = (_a = this.payload) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); } /** * Reads an RtpPacket from the provided buffer * * @param buffer Buffer to read packet from */ static fromBuffer(buffer) { RtpPacket.fromBufferAssertLength(buffer, RtpPacket.RTP_PACKET_MIN_HEADER_SIZE); // Version (V) const v = (buffer[0] & 192) >>> 6; if (v != RtpPacket.RTP_VERSION) throw new Error(`Invalid RTP packet provided: Version must be equal to 2, ${v} found`); // Padding (P) const p = (buffer[0] & 32) != 0; // Extension (X) const x = (buffer[0] & 16) != 0; // CSRC count (CC) const cc = (buffer[0] & 15); // Marker (M) const m = (buffer[1] & 128) != 0; // Payload type (PT) const pt = (buffer[1] & 127); const sequenceNumber = buffer.readUInt16BE(0x02); const timestamp = buffer.readUInt32BE(0x04); const ssrc = buffer.readUInt32BE(0x08); RtpPacket.fromBufferAssertLength(buffer, RtpPacket.RTP_PACKET_MIN_HEADER_SIZE + (cc * 4), "with CSRC"); const csrc = []; for (let c = 0; c < cc; c++) csrc.push(buffer.readUInt32BE(0x0c + (c * 4))); let offset = 0x0c + (cc * 4); let extension; if (x) { RtpPacket.fromBufferAssertLength(buffer, RtpPacket.RTP_PACKET_MIN_HEADER_SIZE + (cc * 4) + 4, "with extension header"); const extensionHeader = buffer.readUInt16BE(offset); const extensionLength = buffer.readUInt16BE(offset + 2); RtpPacket.fromBufferAssertLength(buffer, RtpPacket.RTP_PACKET_MIN_HEADER_SIZE + (cc * 4) + 4 + extensionLength, "with extension content"); const extensionContent = buffer.slice(offset + 4, offset + 4 + extensionLength); extension = { header: extensionHeader, length: extensionLength, content: extensionContent }; offset += 4 + extensionLength; } const payload = buffer.slice(offset); return new RtpPacket({ padding: p, marker: m, payloadType: pt, sequenceNumber: sequenceNumber, timestamp: timestamp, ssrc: ssrc, csrc: csrc, extension: extension, payload: payload }); } static fromBufferAssertLength(buffer, length, content) { if (buffer.length < length) { throw new Error(`Invalid RTP packet: Buffer not large enough to contain ${length} byte header` + (content === undefined ? '' : " " + content)); } } /** * Writes an RtpPacket to a (provided) buffer * * @param packet Packet to write to buffer * @param buffer Optional buffer to write packet to * * If a buffer is not provided, a new buffer is allocated with the appropriate length. */ static toBuffer(packet, buffer) { var _a; buffer = buffer !== null && buffer !== void 0 ? buffer : Buffer.allocUnsafe(packet.length); if (buffer.length < packet.length) throw new Error('Provided buffer is not large enough to write packet'); // Version (V) buffer[0] = RtpPacket.RTP_VERSION << 6; // Padding (P) if (packet.padding) buffer[0] |= 32; // Extension (X) if (packet.extension != null) buffer[0] |= 16; // CSRC count (CC) buffer[0] |= packet.csrc.length & 15; buffer[1] = 0; // Marker (M) if (packet.marker) buffer[1] |= 128; // Payload type (PT) buffer[1] |= packet.payloadType & 127; buffer.writeUInt16BE(packet.sequenceNumber, 0x02); buffer.writeUInt32BE(packet.timestamp, 0x04); buffer.writeUInt32BE(packet.ssrc, 0x08); const cc = Math.min(16, packet.csrc.length); for (let c = 0; c < cc; c++) buffer.writeUInt32BE(packet.csrc[c], 0x0c + (c * 4)); let offset = 0x0c + (cc * 4); if (packet.extension != null) { buffer.writeUInt16BE(packet.extension.header, offset); buffer.writeUInt16BE(packet.extension.length, offset + 2); packet.extension.content.copy(buffer, offset + 4, 0, packet.extension.length); offset += 4 + packet.extension.length; } (_a = packet.payload) === null || _a === void 0 ? void 0 : _a.copy(buffer, offset); return buffer; } } exports.RtpPacket = RtpPacket; RtpPacket.RTP_VERSION = 2; RtpPacket.RTP_PACKET_MIN_HEADER_SIZE = 12; /** * RTP Session * * Class representing a unique RTP stream, with a single SSRC. Manages sequence number and timestamp. */ class RtpSession extends stream.Duplex { constructor(socket, { timestampPeriod = RtpSession.DEFAULT_TIMESTAMP_PERIOD_NS, bufferSize = RtpSession.DEFAULT_BUFFER_SIZE }) { super({ readableObjectMode: true, writableObjectMode: true }); this.socket = socket; this.sequenceNumber = crypto.randomBytes(2).readUInt16BE(0); this.timestamp = crypto.randomBytes(4).readUInt32BE(0); this.ssrc = crypto.randomBytes(4).readUInt32BE(0); this.time = process.hrtime.bigint(); this.timestampPeriod = timestampPeriod; this.buffer = Buffer.allocUnsafe(bufferSize); socket.on('message', message => this.receive(message)); socket.once('error', err => this.destroy(err)); } newSequenceNumber() { this.sequenceNumber++; this.sequenceNumber &= 0xffff; return this.sequenceNumber; } newTimestamp() { const time = process.hrtime.bigint(); this.timestamp += Number(time - this.time) / this.timestampPeriod; this.timestamp &= 0xffffffff; this.timestamp >>>= 0; this.time = time; return this.timestamp; } regenerateSsrc() { this.ssrc = crypto.randomBytes(4).readUInt32BE(0); } receive(message) { try { this.push(RtpPacket.fromBuffer(message)); } catch (err) { this.destroy(err); } } _read(size) { } _write(packet, encoding, callback) { if (!(packet instanceof RtpPacket)) return callback(new Error("Can only accept RtpPackets")); try { RtpPacket.toBuffer(packet, this.buffer); } catch (err) { return callback(err); } this.socket.send(this.buffer, 0, packet.length, callback); } _destroy(error, callback) { this.socket.close(() => callback(null)); } } exports.RtpSession = RtpSession; RtpSession.DEFAULT_TIMESTAMP_PERIOD_NS = 125000; RtpSession.DEFAULT_BUFFER_SIZE = 1446; /** * RTP Packet Payload Data Stream * * Helper stream for reading/writing raw data to/from a specific payload type in an RTP session. */ class RtpPacketPayloadStream extends stream.Duplex { constructor(session, payloadType, bufferOptions) { super(); this.session = session; this.payloadType = payloadType; this.bufferTimeout = null; if (bufferOptions !== undefined) { this.buffer = Buffer.allocUnsafe(bufferOptions.bufferSize); this.bufferOffset = 0; this.bufferHighWaterMark = bufferOptions.bufferHighWaterMark; this.bufferTimeoutMs = bufferOptions.bufferTimeout; } session.once('close', () => this.destroy()); session.once('error', err => this.destroy(err)); session.pipe(new stream.Writable({ objectMode: true, write: (packet, encoding, callback) => { // Push packet payload contents to stream if correct type if (packet.payloadType == this.payloadType) this.push(packet.payload); callback(); } })); } get bufferRemaining() { return this.buffer.length - this.bufferOffset; } send(payload, callback) { this.session.write(new RtpPacket({ payloadType: this.payloadType, sequenceNumber: this.session.newSequenceNumber(), timestamp: this.session.newTimestamp(), ssrc: this.session.ssrc, payload: payload }), callback); } async writeBuffer(chunk) { // Set timeout to flush unless buffer gets filled in meantime if (this.bufferTimeout === undefined) this.bufferTimeout = setTimeout(() => { this.flushBuffer().catch(err => this.destroy(err)); }, this.bufferTimeoutMs); // Always avoid splitting chunk, unless splitting will happen anyway if (chunk.length <= this.buffer.length) { // Chunk could fit in single packet, but too long with existing buffer contents, so flush first if (chunk.length > this.bufferRemaining) await this.flushBuffer(); chunk.copy(this.buffer, this.bufferOffset); this.bufferOffset += chunk.length; } else { // Write entire chunk as efficiently as possible let chunkOffset = 0; while (chunkOffset < chunk.length) { const write = Math.min(chunk.length - chunkOffset, this.bufferRemaining); chunk.copy(this.buffer, this.bufferOffset, chunkOffset, chunkOffset + write); this.bufferOffset += write; chunkOffset += write; // Flush along the way if buffer is full if (this.bufferRemaining === 0) await this.flushBuffer(); } } // Flush if buffer is filled above high water mark if (this.bufferOffset > this.bufferHighWaterMark) await this.flushBuffer(); } async flushBuffer() { if (this.bufferTimeout !== null) { clearTimeout(this.bufferTimeout); this.bufferTimeout = null; } await new Promise((resolve, reject) => { this.send(this.buffer.slice(0, this.bufferOffset), err => { if (err instanceof Error) reject(err); else resolve(); }); }); this.bufferOffset = 0; } _read(size) { } _write(chunk, encoding, callback) { if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding); if (this.buffer === undefined) return this.send(chunk, callback); else this.writeBuffer(chunk).then(callback).catch(callback); } _final(callback) { if (this.buffer !== undefined && this.bufferOffset != 0) { this.flushBuffer().then(callback).catch(callback); } else { callback(); } } } exports.RtpPacketPayloadStream = RtpPacketPayloadStream; var NtripRtpMessageType; (function (NtripRtpMessageType) { NtripRtpMessageType[NtripRtpMessageType["DATA_STREAM"] = 96] = "DATA_STREAM"; NtripRtpMessageType[NtripRtpMessageType["HTTP"] = 97] = "HTTP"; NtripRtpMessageType[NtripRtpMessageType["CONNECTION_END"] = 98] = "CONNECTION_END"; })(NtripRtpMessageType = exports.NtripRtpMessageType || (exports.NtripRtpMessageType = {})); class NtripRtpSession extends RtpSession { constructor(socket) { super(socket, { timestampPeriod: NtripRtpSession.TIMESTAMP_PERIOD_NS, bufferSize: NtripRtpSession.PACKET_MAX_SIZE }); this.dataStream = new RtpPacketPayloadStream(this, NtripRtpMessageType.DATA_STREAM, { bufferSize: NtripRtpSession.DATA_PACKET_MAX_CONTENT_SIZE, bufferHighWaterMark: NtripRtpSession.DATA_PACKET_HIGH_WATER_MARK, bufferTimeout: NtripRtpSession.DATA_PACKET_TIMEOUT_MS }); this.httpStream = new RtpPacketPayloadStream(this, NtripRtpMessageType.HTTP); this.dataStream.once('close', () => this.end()); } push(packet, encoding) { if (packet.payloadType === NtripRtpMessageType.CONNECTION_END) this.end(); return super.push(packet, encoding); } _final(callback) { this.write(new RtpPacket({ payloadType: NtripRtpMessageType.CONNECTION_END, sequenceNumber: this.newSequenceNumber(), timestamp: this.newTimestamp(), ssrc: this.ssrc }), callback); } } exports.NtripRtpSession = NtripRtpSession; NtripRtpSession.PACKET_MAX_SIZE = 1526; NtripRtpSession.TIMESTAMP_PERIOD_NS = 125000; NtripRtpSession.DATA_PACKET_MAX_CONTENT_SIZE = 1514; NtripRtpSession.DATA_PACKET_HIGH_WATER_MARK = NtripRtpSession.DATA_PACKET_MAX_CONTENT_SIZE * 0.75; NtripRtpSession.DATA_PACKET_TIMEOUT_MS = 50;