@robotical/ricjs
Version:
Javascript/TS library for Robotical RIC
257 lines (251 loc) • 10.8 kB
JavaScript
"use strict";
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// RICJS
// Communications Library
//
// Rob Dobson & Chris Greening 2020-2022
// (C) 2020-2022
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const RICLog_1 = tslib_1.__importDefault(require("./RICLog"));
const RICMsgHandler_1 = require("./RICMsgHandler");
const RICTypes_1 = require("./RICTypes");
const RICConnEvents_1 = require("./RICConnEvents");
class RICStreamHandler {
constructor(msgHandler, commsStats, ricConnector) {
// Queue of audio stream requests
/*
private _streamAudioQueue: {
streamContents: Uint8Array;
audioDuration: number;
}[] = [];
*/
// Stream state
this._streamID = null;
this.DEFAULT_MAX_BLOCK_SIZE = 475;
this._maxBlockSize = this.DEFAULT_MAX_BLOCK_SIZE;
// Flow control
this._soktoReceived = false;
this._soktoPos = 0;
this._streamIsStarting = false;
this._lastStreamStartTime = 0;
this._isStreaming = false;
this._isPaused = false;
this._streamBuffer = new Uint8Array();
this._audioDuration = 0;
this._audioByteRate = 0;
this._streamPos = 0;
this._numBlocksWithoutPause = 15;
this._legacySoktoMode = false;
// soundFinishPoint timer
this.soundFinishPoint = null;
this._ricConnector = ricConnector;
this._msgHandler = msgHandler;
this._commsStats = commsStats;
this.onSoktoMsg = this.onSoktoMsg.bind(this);
}
setNumBlocksWithoutPause(numBlocks) {
this._numBlocksWithoutPause = numBlocks;
}
setLegacySoktoMode(legacyMode) {
RICLog_1.default.debug(`Setting legacy sokto mode to ${legacyMode}`);
this._legacySoktoMode = legacyMode;
}
// Start streaming audio
streamAudio(streamContents, clearExisting, audioDuration) {
if (!clearExisting)
RICLog_1.default.debug(`only clearExisting = true is supported right now.`);
// TODO - if clearExisting is not set, form a queue
if (this._streamIsStarting || this._lastStreamStartTime > (Date.now() - 500)) {
RICLog_1.default.error(`Unable to start sound, too soon since last request`);
return;
}
this._isPaused = true;
this._streamIsStarting = true;
this._lastStreamStartTime = Date.now();
this._soktoReceived = false;
this._soktoPos = 0;
this._streamPos = 0;
this._streamBuffer = streamContents;
this._audioDuration = audioDuration;
this._audioByteRate = (streamContents.length / audioDuration) * 1000;
this.clearFinishPointTimeout();
this._sendStreamStartMsg("audio.mp3", "streamaudio", RICTypes_1.RICStreamType.RIC_REAL_TIME_STREAM, streamContents).then((result) => {
this._isPaused = false;
this._streamIsStarting = false;
if (!result) {
RICLog_1.default.error(`Unable to start stream. ufStart message send failed`);
return;
}
//this.streamingPerformanceChecker();
if (!this._isStreaming) {
this._isStreaming = true;
this._sendStreamBuffer();
}
});
}
async streamCancel() {
this._streamBuffer = new Uint8Array();
this.clearFinishPointTimeout();
}
isStreamStarting() {
return this._streamIsStarting;
}
clearFinishPointTimeout() {
if (this.soundFinishPoint) {
clearTimeout(this.soundFinishPoint);
this.soundFinishPoint = null;
}
}
streamingPerformanceChecker() {
if (this._audioDuration) {
this.clearFinishPointTimeout();
this.soundFinishPoint = setTimeout(() => {
// if the streaming hasn't finished before the end of the audio
// we can assume we are having streaming issues
// publish event in case we are having issues
this._ricConnector.onConnEvent(RICConnEvents_1.RICConnEvent.CONN_STREAMING_ISSUE);
this.clearFinishPointTimeout();
}, this._audioDuration + 500);
}
}
// Send the start message
async _sendStreamStartMsg(streamName, targetEndpoint, streamTypeEnum, streamContents) {
// Stream start command message
const streamType = 'rtstream';
const cmdMsg = `{"cmdName":"ufStart","reqStr":"ufStart","fileType":"${streamType}","fileName":"${streamName}","endpoint":"${targetEndpoint}","fileLen":${streamContents.length}}`;
// Debug
RICLog_1.default.debug(`sendStreamStartMsg ${cmdMsg}`);
// Send
let streamStartResp = null;
try {
streamStartResp = await this._msgHandler.sendRICREST(cmdMsg, RICMsgHandler_1.RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME);
}
catch (err) {
RICLog_1.default.error(`sendStreamStartMsg error ${err}`);
return false;
}
// Extract params
if (streamStartResp && (streamStartResp.rslt === 'ok')) {
this._streamID = streamStartResp.streamID;
this._maxBlockSize = streamStartResp.maxBlockSize || this.DEFAULT_MAX_BLOCK_SIZE;
this.streamingPerformanceChecker();
RICLog_1.default.verbose(`sendStreamStartMsg streamID ${this._streamID} maxBlockSize ${this._maxBlockSize} streamType ${streamTypeEnum}`);
}
else {
RICLog_1.default.warn(`sendStreamStartMsg failed ${streamStartResp ? streamStartResp.rslt : 'no response'}`);
return false;
}
return true;
}
get maxBlockSize() {
return this._maxBlockSize;
}
set maxBlockSize(maxBlockSize) {
this._maxBlockSize = maxBlockSize;
this.DEFAULT_MAX_BLOCK_SIZE = maxBlockSize;
}
async _sendStreamEndMsg(streamID) {
if (streamID === null) {
return false;
}
// Stram end command message
const cmdMsg = `{"cmdName":"ufEnd","reqStr":"ufEnd","streamID":${streamID}}`;
// Debug
RICLog_1.default.debug(`sendStreamEndMsg ${cmdMsg}`);
// Send
let streamEndResp = null;
try {
streamEndResp = await this._msgHandler.sendRICREST(cmdMsg, RICMsgHandler_1.RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME);
}
catch (err) {
RICLog_1.default.error(`sendStreamEndMsg error ${err}`);
return false;
}
return streamEndResp.rslt === 'ok';
}
/*
private async _sendAudioStopMsg(): Promise<RICOKFail> {
const cmdMsg = `{"cmdName":"audio/stop"}`;
// Debug
RICLog.debug(`sendAudioStopMsg ${cmdMsg}`);
// Send
return this._msgHandler.sendRICREST<RICOKFail>(
cmdMsg,
RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME,
);
}
private async _sendStreamCancelMsg(): Promise<RICOKFail> {
// File cancel command message
const cmdMsg = `{"cmdName":"ufCancel","reqStr":"ufCancel","streamID":${this._streamID}}`;
// Debug
RICLog.debug(`sendStreamCancelMsg ${cmdMsg}`);
// Send
return this._msgHandler.sendRICREST<RICOKFail>(
cmdMsg,
RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME,
);
}
*/
async _sendStreamBuffer() {
const streamStartTime = Date.now();
// Check streamID is valid
if (this._streamID === null) {
return false;
}
let blockNum = 0;
// Send stream blocks
while (this._soktoPos < this._streamBuffer.length || this._isPaused) {
if (this._isPaused) {
await new Promise((resolve) => setTimeout(resolve, 5));
continue;
}
// Check for new sokto
if (this._soktoReceived) {
if (this._legacySoktoMode)
this._streamPos = this._soktoPos;
// apart from when in legacy mode, the sokto message is now informational only,
// to allow the central to slow down sending of data if it is swamping the peripheral
RICLog_1.default.verbose(`sendStreamContents ${Date.now() - streamStartTime}ms soktoReceived for ${this._streamPos}`);
this._soktoReceived = false;
// receiving an sokto message before the completion of the stream means that the streaming is not keeping up
this._ricConnector.onConnEvent(RICConnEvents_1.RICConnEvent.CONN_STREAMING_ISSUE);
}
// Send stream block
const blockSize = Math.min(this._streamBuffer.length - this._streamPos, this._maxBlockSize);
const block = this._streamBuffer.slice(this._streamPos, this._streamPos + blockSize);
if (block.length > 0) {
const sentOk = await this._msgHandler.sendStreamBlock(block, this._streamPos, this._streamID);
this._commsStats.recordStreamBytes(block.length);
RICLog_1.default.verbose(`sendStreamContents ${sentOk ? "OK" : "FAILED"} ${Date.now() - streamStartTime}ms pos ${this._streamPos} ${blockSize} ${block.length} ${this._soktoPos}`);
if (!sentOk) {
return false;
}
this._streamPos += blockSize;
blockNum += 1;
if (this._audioByteRate && blockNum > this._numBlocksWithoutPause) {
const pauseTime = ((blockSize / this._audioByteRate) * 1000) - 10;
RICLog_1.default.verbose(`Pausing for ${pauseTime} ms between audio packets. Bit rate ${this._audioByteRate * 8}`);
await new Promise((resolve) => setTimeout(resolve, pauseTime));
}
}
// Wait to ensure we don't hog the CPU
await new Promise((resolve) => setTimeout(resolve, 1));
}
this._isStreaming = false;
this.clearFinishPointTimeout();
await this._sendStreamEndMsg(this._streamID);
return true;
}
onSoktoMsg(soktoPos) {
// Get how far we've progressed in file
this._soktoPos = soktoPos;
this._soktoReceived = true;
RICLog_1.default.debug(`onSoktoMsg received file up to ${this._soktoPos}`);
}
}
exports.default = RICStreamHandler;
//# sourceMappingURL=RICStreamHandler.js.map