@ntrip/caster
Version:
NTRIP caster
365 lines (364 loc) • 14.7 kB
JavaScript
"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;