* replace stream module by improved version of readable/writable impl * rm server.js * async api improvments: - better tracing of calls from worker back and forth - fix transferrable handling to avoid copying buffers for r/w - optional debug logs - completed jsdocs annotations - add dispose method - add setLogLevel method (analoguous to added binding) * node-srt C bindings: - add SetLogLevel to get libSRT log output if desired - add OK static member - add #define EPOLL_EVENTS_NUM_MAX 1024 - improve error string thrown in Read (add that it comes from srt_recvmsg) - improve error string thrown in Write (add that it comes from srt_sendmsg2) - misc isofunctional improvements (var names) and comments * add SRT logging related JS-side helper * rewrite flat TypeScript decl files without "module" keyword * add ts enum decl for all libSRT enums * async-worker: enable using transferrable for zero-copy + allow better debugging (like in api/dispatcher side) + misc improvements on code quality * add async-helpers: various functions to help dealing with transferrables + tracing calls to native bindings in debug output * add async read/write modes functions + async-reader-writer class - these will allow for performing high-level r/w operations conveniently at optimum throughput for larger pieces of payload i.e list of packets. * add srt-server and srt-connection (can manage multiple clients), - based on async-api - can be used with reader/writer (i.e the underlying modes) * srt-server/connection typings * async srt spec: add dispose method usage (but commented out as crashing atm) * async srt spec: rm redundant checks on SRT static members (they are done in other spec already) * promises api spec: formal fixes * stream spec: add dummy test * package.json: - put gyp toolchain in runtime deps (since the build happens on install) - add JEST test runner - shorten check-tsc script - rebuild script: check & use all CPU cores available - run rebuild actually on install, not preinstall (fixes deps not being there) - remove preinstall and thus "npm install git-clone" in the package scripts * update package lock * update typings index not to need triple-slashs anymore * in srt.ts example: check for read return value type * build-srt-sdk script: - allow to use any local libSRT code repo - when using make: use all amount of cores available for build - isolate better code running on different platforms * update package main index with new things * add enum typings index * add jest config * add "use strict" on async-srt-await example * add integration/smoke testing for client-to-server one-way burst write * readme: add note on build prerequisites * readme: add infos on new components SRTServer/Connection & AsyncReaderWriter
191 lines
5 KiB
JavaScript
191 lines
5 KiB
JavaScript
const { Readable } = require('stream');
|
|
const { SRT } = require('../build/Release/node_srt.node');
|
|
const debug = require('debug')('srt-read-stream');
|
|
|
|
const CONNECTION_ACCEPT_POLLING_INTERVAL_MS = 50;
|
|
const READ_WAIT_INTERVAL_MS = 50;
|
|
|
|
const EPOLLUWAIT_TIMEOUT_MS = 1000;
|
|
const SOCKET_LISTEN_BACKLOG = 10;
|
|
|
|
/**
|
|
* Example:
|
|
*
|
|
* const dest = fs.createWritableStream('./output');
|
|
*
|
|
* const srt = new SRTReadStream('0.0.0.0', 1234);
|
|
* srt.listen(readStream => {
|
|
* readStream.pipe(dest);
|
|
* })
|
|
*
|
|
*/
|
|
class SRTReadStream extends Readable {
|
|
// Q: opts not used ?
|
|
// Q: not better if port (mandatory) is before, and address is optional (default to "0.0.0.0")?
|
|
constructor(address, port, opts) {
|
|
super();
|
|
|
|
/**
|
|
* @member {SRT}
|
|
*/
|
|
this.srt = new SRT();
|
|
|
|
/**
|
|
* @member {number}
|
|
*/
|
|
this.socket = this.srt.createSocket();
|
|
|
|
/**
|
|
* @member {string}
|
|
*/
|
|
this.address = address;
|
|
|
|
/**
|
|
* @member {number}
|
|
*/
|
|
this.port = port;
|
|
|
|
/**
|
|
* @member {number | null}
|
|
*/
|
|
this.fd = null;
|
|
|
|
this._eventPollInterval = null;
|
|
this._readTimer = null;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Function} onData Passes this stream instance as first arg to callback
|
|
*/
|
|
listen(onData) {
|
|
|
|
if (this.fd !== null) {
|
|
throw new Error('listen() called but stream file-descriptor already initialized');
|
|
}
|
|
|
|
this.srt.bind(this.socket, this.address, this.port);
|
|
this.srt.listen(this.socket, SOCKET_LISTEN_BACKLOG);
|
|
|
|
const epid = this.srt.epollCreate();
|
|
this.srt.epollAddUsock(epid, this.socket, SRT.EPOLL_IN | SRT.EPOLL_ERR);
|
|
|
|
const interval = this._eventPollInterval = setInterval(() => {
|
|
const events = this.srt.epollUWait(epid, EPOLLUWAIT_TIMEOUT_MS);
|
|
events.forEach(event => {
|
|
const status = this.srt.getSockState(event.socket);
|
|
if (status === SRT.SRTS_BROKEN || status === SRT.SRTS_NONEXIST || status === SRT.SRTS_CLOSED) {
|
|
debug("Client disconnected with socket:", event.socket);
|
|
this.srt.close(event.socket);
|
|
this.push(null);
|
|
this.emit('end');
|
|
} else if (event.socket === this.socket) {
|
|
const fhandle = this.srt.accept(this.socket);
|
|
debug("Accepted client connection with file-descriptor:", fhandle);
|
|
this.srt.epollAddUsock(epid, fhandle, SRT.EPOLL_IN | SRT.EPOLL_ERR);
|
|
this.emit('readable');
|
|
} else {
|
|
debug("Got data from connection on fd:", event.socket);
|
|
this.fd = event.socket;
|
|
clearInterval(interval);
|
|
onData(this);
|
|
this.emit('readable');
|
|
}
|
|
});
|
|
}, CONNECTION_ACCEPT_POLLING_INTERVAL_MS);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Function} onConnect
|
|
*/
|
|
connect(onConnect) {
|
|
|
|
if (this.fd !== null) {
|
|
throw new Error('connect() called but stream file-descriptor already initialized');
|
|
}
|
|
|
|
this.srt.connect(this.socket, this.address, this.port);
|
|
this.fd = this.socket;
|
|
if (this.fd) {
|
|
onConnect(this);
|
|
}
|
|
}
|
|
|
|
close() {
|
|
this.destroy();
|
|
}
|
|
|
|
_scheduleNextRead(requestedBytes, timeoutMs) {
|
|
this._readTimer = setTimeout(this._readSocketAndPush.bind(this, requestedBytes), timeoutMs);
|
|
}
|
|
|
|
_clearScheduledRead() {
|
|
clearTimeout(this._readTimer);
|
|
this._readTimer = null;
|
|
}
|
|
|
|
_readSocketAndPush(bytes) {
|
|
this._clearScheduledRead();
|
|
if (this.fd === null) {
|
|
this._scheduleNextRead(bytes, READ_WAIT_INTERVAL_MS);
|
|
return;
|
|
}
|
|
let remainingBytes = bytes;
|
|
while(true) {
|
|
const buffer = this.srt.read(this.fd, bytes);
|
|
if (buffer === null) { // connection likely died
|
|
debug("Socket read call returned 'null'");
|
|
this.close();
|
|
break;
|
|
}
|
|
// we expect a Buffer object here, but
|
|
// -1 is the SRT_ERROR value that would get returned
|
|
// if there is no data to read yet/anymore
|
|
if (buffer === -1) {
|
|
this._scheduleNextRead(remainingBytes, READ_WAIT_INTERVAL_MS);
|
|
break;
|
|
}
|
|
//debug(`Read ${buffer.length} bytes from fd`);
|
|
|
|
// @see https://nodejs.org/api/stream.html#stream_readable_push_chunk_encoding
|
|
if (this.push(buffer)) {
|
|
remainingBytes -= buffer.length;
|
|
if (remainingBytes <= 0) {
|
|
// 0 as a timer value acts as setImmediate task (next tick ideally)
|
|
break;
|
|
}
|
|
} else {
|
|
debug("Readable.push returned 'false' at remaining bytes:", remainingBytes);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see https://nodejs.org/api/stream.html#stream_readable_read_size_1
|
|
* @param {number} bytes
|
|
*/
|
|
_read(bytes) {
|
|
debug('Readable._read(): requested bytes:', bytes);
|
|
this._readSocketAndPush(bytes);
|
|
}
|
|
|
|
/**
|
|
* @see https://nodejs.org/api/stream.html#stream_readable_destroy_err_callback
|
|
* @param {Error} err
|
|
* @param {Function} cb
|
|
*/
|
|
_destroy(err, cb) {
|
|
// guard from closing multiple times
|
|
if (this.fd === null) return;
|
|
this.srt.close(this.socket);
|
|
this.fd = null;
|
|
this._clearScheduledRead();
|
|
if (cb) cb(err);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
SRTReadStream
|
|
};
|