node-srt/src/srt-stream-readable.js
Stephan Hesse bf60795889
Async API server/streams impl & functional tests + Allow to use local SRT source repo instead of remote (#9)
* 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
2020-10-21 13:09:52 +02:00

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
};