Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@speechly/browser-client

Package Overview
Dependencies
Maintainers
6
Versions
79
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@speechly/browser-client - npm Package Compare versions

Comparing version 1.2.0 to 1.3.0

20

core/types/speechly/client.d.ts

@@ -27,9 +27,8 @@ import { ClientOptions, StateChangeCallback, SegmentChangeCallback, TentativeTranscriptCallback, TranscriptCallback, TentativeEntitiesCallback, EntityCallback, IntentCallback } from './types';

private readonly activeContexts;
private readonly reconnectAttemptCount;
private readonly reconnectMinDelay;
private readonly maxReconnectAttemptCount;
private readonly contextStopDelay;
private connectAttempt;
private stoppedContextIdPromise?;
private initializeMicrophonePromise?;
private readonly initializeApiClientPromise;
private resolveInitialization?;
private connectPromise;
private initializePromise;
private resolveStopContext?;

@@ -40,2 +39,3 @@ private readonly deviceId;

private state;
private readonly apiUrl;
private stateChangeCb;

@@ -50,6 +50,11 @@ private segmentChangeCb;

constructor(options: ClientOptions);
private getReconnectDelayMs;
private sleep;
/**
* Esteblish websocket connection
* Connect to Speechly backend.
* This function will be called by initialize if not manually called earlier.
* Calling connect() immediately after constructor and setting callbacks allows
* prewarming the connection, resulting in less noticeable waits for the user.
*/
private connect;
connect(): Promise<void>;
/**

@@ -129,2 +134,3 @@ * Initializes the client, by initializing the microphone and establishing connection to the API.

private readonly handleWebsocketClosure;
private reconnect;
private setState;

@@ -131,0 +137,0 @@ /**

@@ -14,2 +14,6 @@ import { Microphone } from '../microphone';

/**
* Connect to Speechly upon creating the client instance. Defaults to true.
*/
connect?: boolean;
/**
* The unique identifier of a project in the dashboard.

@@ -19,2 +23,3 @@ */

/**
* @deprecated
* The language which is used by the app.

@@ -21,0 +26,0 @@ */

@@ -33,2 +33,3 @@ /**

Opened = "WEBSOCKET_OPEN",
Closed = "WEBSOCKET_CLOSED",
SourceSampleRateSetSuccess = "SOURSE_SAMPLE_RATE_SET_SUCCESS",

@@ -134,3 +135,7 @@ Started = "started",

*/
export declare type CloseCallback = (err: Error) => void;
export declare type CloseCallback = (err: {
code: number;
reason: string;
wasClean: boolean;
}) => void;
/**

@@ -137,0 +142,0 @@ * The interface for a client for Speechly SLU WebSocket API.

@@ -1,2 +0,2 @@

declare const _default: "/**\n * Known WebSocket response types.\n * @public\n */\nvar WebsocketResponseType;\n(function (WebsocketResponseType) {\n WebsocketResponseType[\"Opened\"] = \"WEBSOCKET_OPEN\";\n WebsocketResponseType[\"SourceSampleRateSetSuccess\"] = \"SOURSE_SAMPLE_RATE_SET_SUCCESS\";\n WebsocketResponseType[\"Started\"] = \"started\";\n WebsocketResponseType[\"Stopped\"] = \"stopped\";\n})(WebsocketResponseType || (WebsocketResponseType = {}));\nvar CONTROL = {\n WRITE_INDEX: 0,\n FRAMES_AVAILABLE: 1,\n LOCK: 2\n};\nvar WebsocketClient = /** @class */ (function () {\n function WebsocketClient(ctx) {\n var _this = this;\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n this.shouldResendLastFramesSent = false;\n this.buffer = new Float32Array(0);\n this.lastFramesSent = new Int16Array(0); // to re-send after switch context\n this.debug = false;\n this.initialized = false;\n this.onWebsocketClose = function (event) {\n _this.websocket = undefined;\n _this.connect(0);\n };\n this.onWebsocketOpen = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket opened');\n }\n if (_this.isContextStarted && !_this.isStartContextConfirmed) {\n _this.send(_this.outbox);\n }\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_OPEN' });\n };\n this.onWebsocketError = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket error');\n }\n _this.closeWebsocket();\n };\n this.onWebsocketMessage = function (event) {\n var response;\n try {\n response = JSON.parse(event.data);\n }\n catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e);\n return;\n }\n if (response.type === WebsocketResponseType.Started) {\n _this.isStartContextConfirmed = true;\n if (_this.shouldResendLastFramesSent) {\n _this.resendLastFrames();\n _this.shouldResendLastFramesSent = false;\n }\n }\n _this.workerCtx.postMessage(response);\n };\n this.workerCtx = ctx;\n }\n WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {\n if (this.initialized) {\n console.log('[SpeechlyClient]', 'already initialized');\n return;\n }\n this.debug = debug;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'initialize worker');\n }\n this.apiUrl = apiUrl;\n this.authToken = authToken;\n this.targetSampleRate = targetSampleRate;\n this.initialized = true;\n this.connect(0);\n };\n WebsocketClient.prototype.setSourceSampleRate = function (sourceSampleRate) {\n this.sourceSampleRate = sourceSampleRate;\n this.resampleRatio = this.sourceSampleRate / this.targetSampleRate;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'resampleRatio', this.resampleRatio);\n }\n if (this.resampleRatio > 1) {\n this.filter = generateFilter(this.sourceSampleRate, this.targetSampleRate, 127);\n }\n this.workerCtx.postMessage({ type: 'SOURSE_SAMPLE_RATE_SET_SUCCESS' });\n if (isNaN(this.resampleRatio)) {\n throw Error(\"resampleRatio is NaN source rate is \".concat(this.sourceSampleRate, \" and target rate is \").concat(this.targetSampleRate));\n }\n };\n WebsocketClient.prototype.setSharedArrayBuffers = function (controlSAB, dataSAB) {\n this.controlSAB = new Int32Array(controlSAB);\n this.dataSAB = new Float32Array(dataSAB);\n var audioHandleInterval = this.dataSAB.length / 32; // ms\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Audio handle interval', audioHandleInterval, 'ms');\n }\n setInterval(this.sendAudioFromSAB.bind(this), audioHandleInterval);\n };\n WebsocketClient.prototype.connect = function (timeout) {\n if (timeout === void 0) { timeout = 1000; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connect in ', timeout / 1000, 'sec');\n }\n setTimeout(this.initializeWebsocket.bind(this), timeout);\n };\n WebsocketClient.prototype.initializeWebsocket = function () {\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connecting to ', this.apiUrl);\n }\n this.websocket = new WebSocket(this.apiUrl, this.authToken);\n this.websocket.addEventListener('open', this.onWebsocketOpen);\n this.websocket.addEventListener('message', this.onWebsocketMessage);\n this.websocket.addEventListener('error', this.onWebsocketError);\n this.websocket.addEventListener('close', this.onWebsocketClose);\n };\n WebsocketClient.prototype.isOpen = function () {\n return this.websocket !== undefined && this.websocket.readyState === this.websocket.OPEN;\n };\n WebsocketClient.prototype.resendLastFrames = function () {\n if (this.lastFramesSent.length > 0) {\n this.send(this.lastFramesSent);\n this.lastFramesSent = new Int16Array(0);\n }\n };\n WebsocketClient.prototype.sendAudio = function (audioChunk) {\n if (!this.isContextStarted) {\n return;\n }\n if (audioChunk.length > 0) {\n if (this.resampleRatio > 1) {\n // Downsampling\n this.send(this.downsample(audioChunk));\n }\n else {\n this.send(float32ToInt16(audioChunk));\n }\n }\n };\n WebsocketClient.prototype.sendAudioFromSAB = function () {\n if (!this.isContextStarted) {\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n return;\n }\n if (this.controlSAB == undefined) {\n return;\n }\n var framesAvailable = this.controlSAB[CONTROL.FRAMES_AVAILABLE];\n var lock = this.controlSAB[CONTROL.LOCK];\n if (lock == 0 && framesAvailable > 0) {\n var data = this.dataSAB.subarray(0, framesAvailable);\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (data.length > 0) {\n var frames_1;\n if (this.resampleRatio > 1) {\n frames_1 = this.downsample(data);\n }\n else {\n frames_1 = float32ToInt16(data);\n }\n this.send(frames_1);\n // 16000 per second, 1000 in 100 ms\n // save last 250 ms\n if (this.lastFramesSent.length > 1024 * 4) {\n this.lastFramesSent = frames_1;\n }\n else {\n var concat = new Int16Array(this.lastFramesSent.length + frames_1.length);\n concat.set(this.lastFramesSent);\n concat.set(frames_1, this.lastFramesSent.length);\n this.lastFramesSent = concat;\n }\n }\n }\n };\n WebsocketClient.prototype.startContext = function (appId) {\n if (this.isContextStarted) {\n console.log('Cant start context: it has been already started');\n return;\n }\n this.isContextStarted = true;\n this.isStartContextConfirmed = false;\n if (appId !== undefined) {\n this.outbox = JSON.stringify({ event: 'start', appId: appId });\n }\n else {\n this.outbox = JSON.stringify({ event: 'start' });\n }\n this.send(this.outbox);\n };\n WebsocketClient.prototype.stopContext = function () {\n if (this.websocket == undefined) {\n throw Error('Cant start context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant stop context: it is not started');\n return;\n }\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n };\n WebsocketClient.prototype.switchContext = function (newAppId) {\n if (this.websocket == undefined) {\n throw Error('Cant switch context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant switch context: it is not started');\n return;\n }\n if (newAppId == undefined) {\n console.log('Cant switch context: new app id is undefined');\n return;\n }\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n this.shouldResendLastFramesSent = true;\n this.send(JSON.stringify({ event: 'start', appId: newAppId }));\n };\n WebsocketClient.prototype.closeWebsocket = function () {\n if (this.websocket == null) {\n throw Error('Websocket is not open');\n }\n this.websocket.removeEventListener('open', this.onWebsocketOpen);\n this.websocket.removeEventListener('message', this.onWebsocketMessage);\n this.websocket.removeEventListener('error', this.onWebsocketError);\n this.websocket.removeEventListener('close', this.onWebsocketClose);\n this.websocket.close();\n };\n WebsocketClient.prototype.downsample = function (input) {\n var inputBuffer = new Float32Array(this.buffer.length + input.length);\n inputBuffer.set(this.buffer, 0);\n inputBuffer.set(input, this.buffer.length);\n var outputLength = Math.ceil((inputBuffer.length - this.filter.length) / this.resampleRatio);\n var outputBuffer = new Int16Array(outputLength);\n for (var i = 0; i < outputLength; i++) {\n var offset = Math.round(this.resampleRatio * i);\n var val = 0.0;\n for (var j = 0; j < this.filter.length; j++) {\n val += inputBuffer[offset + j] * this.filter[j];\n }\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff);\n }\n var remainingOffset = Math.round(this.resampleRatio * outputLength);\n if (remainingOffset < inputBuffer.length) {\n this.buffer = inputBuffer.subarray(remainingOffset);\n }\n else {\n this.buffer = new Float32Array(0);\n }\n return outputBuffer;\n };\n WebsocketClient.prototype.send = function (data) {\n if (!this.isOpen()) {\n throw Error('Cant send data: websocket is inactive');\n }\n try {\n this.websocket.send(data);\n }\n catch (error) {\n console.log('[SpeechlyClient]', 'Server connection error', error);\n }\n };\n return WebsocketClient;\n}());\nvar ctx = self;\nvar websocketClient = new WebsocketClient(ctx);\nctx.onmessage = function (e) {\n switch (e.data.type) {\n case 'INIT':\n websocketClient.init(e.data.apiUrl, e.data.authToken, e.data.targetSampleRate, e.data.debug);\n break;\n case 'SET_SOURSE_SAMPLE_RATE':\n websocketClient.setSourceSampleRate(e.data.sourceSampleRate);\n break;\n case 'SET_SHARED_ARRAY_BUFFERS':\n websocketClient.setSharedArrayBuffers(e.data.controlSAB, e.data.dataSAB);\n break;\n case 'CLOSE':\n websocketClient.closeWebsocket();\n break;\n case 'START_CONTEXT':\n websocketClient.startContext(e.data.appId);\n break;\n case 'SWITCH_CONTEXT':\n websocketClient.switchContext(e.data.appId);\n break;\n case 'STOP_CONTEXT':\n websocketClient.stopContext();\n break;\n case 'AUDIO':\n websocketClient.sendAudio(e.data.payload);\n break;\n default:\n console.log('WORKER', e);\n }\n};\nfunction float32ToInt16(buffer) {\n var buf = new Int16Array(buffer.length);\n for (var l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff);\n }\n return buf;\n}\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd');\n }\n var cutoff = targetSampleRate / 2;\n var filter = new Float32Array(length);\n var sum = 0;\n for (var i = 0; i < length; i++) {\n var x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2));\n sum += x;\n filter[i] = x;\n }\n for (var i = 0; i < length; i++) {\n filter[i] = filter[i] / sum;\n }\n return filter;\n}\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0;\n }\n var piX = Math.PI * x;\n return Math.sin(piX) / piX;\n}\n";
declare const _default: "/**\n * Known WebSocket response types.\n * @public\n */\nvar WebsocketResponseType;\n(function (WebsocketResponseType) {\n WebsocketResponseType[\"Opened\"] = \"WEBSOCKET_OPEN\";\n WebsocketResponseType[\"SourceSampleRateSetSuccess\"] = \"SOURSE_SAMPLE_RATE_SET_SUCCESS\";\n WebsocketResponseType[\"Started\"] = \"started\";\n WebsocketResponseType[\"Stopped\"] = \"stopped\";\n})(WebsocketResponseType || (WebsocketResponseType = {}));\nvar CONTROL = {\n WRITE_INDEX: 0,\n FRAMES_AVAILABLE: 1,\n LOCK: 2\n};\nvar WebsocketClient = /** @class */ (function () {\n function WebsocketClient(ctx) {\n var _this = this;\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n this.shouldResendLastFramesSent = false;\n this.buffer = new Float32Array(0);\n this.lastFramesSent = new Int16Array(0); // to re-send after switch context\n this.debug = false;\n this.initialized = false;\n // WebSocket's close handler, called e.g. when\n // - normal close (code 1000)\n // - network unreachable or unable to (re)connect (code 1006)\n // List of CloseEvent.code values: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n this.onWebsocketClose = function (event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'onWebsocketClose');\n }\n _this.websocket.removeEventListener('open', _this.onWebsocketOpen);\n _this.websocket.removeEventListener('message', _this.onWebsocketMessage);\n _this.websocket.removeEventListener('error', _this.onWebsocketError);\n _this.websocket.removeEventListener('close', _this.onWebsocketClose);\n _this.websocket = undefined;\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_CLOSED', code: event.code, reason: event.reason, wasClean: event.wasClean });\n };\n this.onWebsocketOpen = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket opened');\n }\n if (_this.isContextStarted && !_this.isStartContextConfirmed) {\n _this.send(_this.outbox);\n }\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_OPEN' });\n };\n this.onWebsocketError = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket error');\n }\n };\n this.onWebsocketMessage = function (event) {\n var response;\n try {\n response = JSON.parse(event.data);\n }\n catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e);\n return;\n }\n if (response.type === WebsocketResponseType.Started) {\n _this.isStartContextConfirmed = true;\n if (_this.shouldResendLastFramesSent) {\n _this.resendLastFrames();\n _this.shouldResendLastFramesSent = false;\n }\n }\n _this.workerCtx.postMessage(response);\n };\n this.workerCtx = ctx;\n }\n WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {\n this.debug = debug;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'initialize worker');\n }\n this.apiUrl = apiUrl;\n this.authToken = authToken;\n this.targetSampleRate = targetSampleRate;\n this.initialized = true;\n this.connect(0);\n };\n WebsocketClient.prototype.setSourceSampleRate = function (sourceSampleRate) {\n this.sourceSampleRate = sourceSampleRate;\n this.resampleRatio = this.sourceSampleRate / this.targetSampleRate;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'resampleRatio', this.resampleRatio);\n }\n if (this.resampleRatio > 1) {\n this.filter = generateFilter(this.sourceSampleRate, this.targetSampleRate, 127);\n }\n this.workerCtx.postMessage({ type: 'SOURSE_SAMPLE_RATE_SET_SUCCESS' });\n if (isNaN(this.resampleRatio)) {\n throw Error(\"resampleRatio is NaN source rate is \".concat(this.sourceSampleRate, \" and target rate is \").concat(this.targetSampleRate));\n }\n };\n WebsocketClient.prototype.setSharedArrayBuffers = function (controlSAB, dataSAB) {\n this.controlSAB = new Int32Array(controlSAB);\n this.dataSAB = new Float32Array(dataSAB);\n var audioHandleInterval = this.dataSAB.length / 32; // ms\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Audio handle interval', audioHandleInterval, 'ms');\n }\n setInterval(this.sendAudioFromSAB.bind(this), audioHandleInterval);\n };\n WebsocketClient.prototype.connect = function (timeout) {\n if (timeout === void 0) { timeout = 1000; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connect in ', timeout / 1000, 'sec');\n }\n setTimeout(this.initializeWebsocket.bind(this), timeout);\n };\n WebsocketClient.prototype.initializeWebsocket = function () {\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connecting to ', this.apiUrl);\n }\n this.websocket = new WebSocket(this.apiUrl, this.authToken);\n this.websocket.addEventListener('open', this.onWebsocketOpen);\n this.websocket.addEventListener('message', this.onWebsocketMessage);\n this.websocket.addEventListener('error', this.onWebsocketError);\n this.websocket.addEventListener('close', this.onWebsocketClose);\n };\n WebsocketClient.prototype.isOpen = function () {\n return this.websocket !== undefined && this.websocket.readyState === this.websocket.OPEN;\n };\n WebsocketClient.prototype.resendLastFrames = function () {\n if (this.lastFramesSent.length > 0) {\n this.send(this.lastFramesSent);\n this.lastFramesSent = new Int16Array(0);\n }\n };\n WebsocketClient.prototype.sendAudio = function (audioChunk) {\n if (!this.isContextStarted) {\n return;\n }\n if (audioChunk.length > 0) {\n if (this.resampleRatio > 1) {\n // Downsampling\n this.send(this.downsample(audioChunk));\n }\n else {\n this.send(float32ToInt16(audioChunk));\n }\n }\n };\n WebsocketClient.prototype.sendAudioFromSAB = function () {\n if (!this.isContextStarted) {\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n return;\n }\n if (this.controlSAB == undefined) {\n return;\n }\n var framesAvailable = this.controlSAB[CONTROL.FRAMES_AVAILABLE];\n var lock = this.controlSAB[CONTROL.LOCK];\n if (lock == 0 && framesAvailable > 0) {\n var data = this.dataSAB.subarray(0, framesAvailable);\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (data.length > 0) {\n var frames_1;\n if (this.resampleRatio > 1) {\n frames_1 = this.downsample(data);\n }\n else {\n frames_1 = float32ToInt16(data);\n }\n this.send(frames_1);\n // 16000 per second, 1000 in 100 ms\n // save last 250 ms\n if (this.lastFramesSent.length > 1024 * 4) {\n this.lastFramesSent = frames_1;\n }\n else {\n var concat = new Int16Array(this.lastFramesSent.length + frames_1.length);\n concat.set(this.lastFramesSent);\n concat.set(frames_1, this.lastFramesSent.length);\n this.lastFramesSent = concat;\n }\n }\n }\n };\n WebsocketClient.prototype.startContext = function (appId) {\n if (this.isContextStarted) {\n console.log('Cant start context: it has been already started');\n return;\n }\n this.isContextStarted = true;\n this.isStartContextConfirmed = false;\n if (appId !== undefined) {\n this.outbox = JSON.stringify({ event: 'start', appId: appId });\n }\n else {\n this.outbox = JSON.stringify({ event: 'start' });\n }\n this.send(this.outbox);\n };\n WebsocketClient.prototype.stopContext = function () {\n if (!this.websocket) {\n throw Error('Cant start context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant stop context: it is not started');\n return;\n }\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n };\n WebsocketClient.prototype.switchContext = function (newAppId) {\n if (!this.websocket) {\n throw Error('Cant switch context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant switch context: it is not started');\n return;\n }\n if (newAppId == undefined) {\n console.log('Cant switch context: new app id is undefined');\n return;\n }\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n this.shouldResendLastFramesSent = true;\n this.send(JSON.stringify({ event: 'start', appId: newAppId }));\n };\n WebsocketClient.prototype.closeWebsocket = function (websocketCode, reason) {\n if (websocketCode === void 0) { websocketCode = 1005; }\n if (reason === void 0) { reason = \"No Status Received\"; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Websocket closing');\n }\n if (!this.websocket) {\n throw Error('Websocket is not open');\n }\n this.websocket.close(websocketCode, reason);\n };\n WebsocketClient.prototype.downsample = function (input) {\n var inputBuffer = new Float32Array(this.buffer.length + input.length);\n inputBuffer.set(this.buffer, 0);\n inputBuffer.set(input, this.buffer.length);\n var outputLength = Math.ceil((inputBuffer.length - this.filter.length) / this.resampleRatio);\n var outputBuffer = new Int16Array(outputLength);\n for (var i = 0; i < outputLength; i++) {\n var offset = Math.round(this.resampleRatio * i);\n var val = 0.0;\n for (var j = 0; j < this.filter.length; j++) {\n val += inputBuffer[offset + j] * this.filter[j];\n }\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff);\n }\n var remainingOffset = Math.round(this.resampleRatio * outputLength);\n if (remainingOffset < inputBuffer.length) {\n this.buffer = inputBuffer.subarray(remainingOffset);\n }\n else {\n this.buffer = new Float32Array(0);\n }\n return outputBuffer;\n };\n WebsocketClient.prototype.send = function (data) {\n if (!this.isOpen()) {\n throw Error('Cant send data: websocket is inactive');\n }\n try {\n this.websocket.send(data);\n }\n catch (error) {\n console.log('[SpeechlyClient]', 'Server connection error', error);\n }\n };\n return WebsocketClient;\n}());\nvar ctx = self;\nvar websocketClient = new WebsocketClient(ctx);\nctx.onmessage = function (e) {\n switch (e.data.type) {\n case 'INIT':\n websocketClient.init(e.data.apiUrl, e.data.authToken, e.data.targetSampleRate, e.data.debug);\n break;\n case 'SET_SOURSE_SAMPLE_RATE':\n websocketClient.setSourceSampleRate(e.data.sourceSampleRate);\n break;\n case 'SET_SHARED_ARRAY_BUFFERS':\n websocketClient.setSharedArrayBuffers(e.data.controlSAB, e.data.dataSAB);\n break;\n case 'CLOSE':\n websocketClient.closeWebsocket(1000, \"Close requested by client\");\n break;\n case 'START_CONTEXT':\n websocketClient.startContext(e.data.appId);\n break;\n case 'SWITCH_CONTEXT':\n websocketClient.switchContext(e.data.appId);\n break;\n case 'STOP_CONTEXT':\n websocketClient.stopContext();\n break;\n case 'AUDIO':\n websocketClient.sendAudio(e.data.payload);\n break;\n default:\n console.log('WORKER', e);\n }\n};\nfunction float32ToInt16(buffer) {\n var buf = new Int16Array(buffer.length);\n for (var l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff);\n }\n return buf;\n}\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd');\n }\n var cutoff = targetSampleRate / 2;\n var filter = new Float32Array(length);\n var sum = 0;\n for (var i = 0; i < length; i++) {\n var x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2));\n sum += x;\n filter[i] = x;\n }\n for (var i = 0; i < length; i++) {\n filter[i] = filter[i] / sum;\n }\n return filter;\n}\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0;\n }\n var piX = Math.PI * x;\n return Math.sin(piX) / piX;\n}\n";
export default _default;
{
"name": "@speechly/browser-client",
"version": "1.2.0",
"version": "1.3.0",
"description": "Browser client for Speechly API",

@@ -27,12 +27,9 @@ "keywords": [

"build:watch": "rm -rf ./dist/ && pnpm run buildworker && pnpx rollup -c --silent",
"buildworker": "npx tsc ./worker/worker.ts && cat ./worker/templateOpen > ./src/websocket/worker.ts && cat ./worker/worker.js >> ./src/websocket/worker.ts && cat ./worker/templateEnd >> ./src/websocket/worker.ts",
"check": "pnpm run build && npx api-extractor run --verbose",
"docs": "pnpm run prepdist && npx typedoc --readme none --includeDeclarations --excludeExternals --excludeNotExported --excludePrivate --excludeProtected --out ./docs/ --plugin typedoc-plugin-markdown ./dist/index.d.ts",
"buildworker": "pnpx tsc ./worker/worker.ts && cat ./worker/templateOpen > ./src/websocket/worker.ts && cat ./worker/worker.js >> ./src/websocket/worker.ts && cat ./worker/templateEnd >> ./src/websocket/worker.ts",
"check": "pnpm run build && pnpx api-extractor run --verbose",
"docs": "rimraf docs && pnpx typedoc --readme none --excludeExternals --excludePrivate --excludeProtected --out ./docs/ --entryPointStrategy expand --sort required-first --disableSources ./src/",
"getdeps": "pnpm install --force --frozen-lockfile",
"lint": "npx eslint --cache --max-warnings 0 'src/**/*.{ts,tsx}'",
"precommit": "npx prettier --write src/**/*.ts && pnpm run build && npx api-extractor run --local && pnpm run docs",
"prepdist": "node ./config/prepare_dist.js",
"prerelease": "pnpm run check && pnpm run prepdist",
"test": "npx jest --config ./config/jest.config.js",
"watch": "rm -rf ./dist/ && mkdir dist && pnpm run prepdist && npx tsc-watch"
"lint": "pnpx eslint --cache --max-warnings 0 'src/**/*.{ts,tsx}'",
"precommit": "pnpx prettier --write src/**/*.ts && pnpm run build && pnpx api-extractor run --local && pnpm run docs",
"test": "pnpx jest --config ./config/jest.config.js"
},

@@ -55,3 +52,2 @@ "repository": {

"base-64": "^0.1.0",
"locale-code": "^2.0.2",
"uuid": "^8.0.0"

@@ -86,5 +82,5 @@ },

"tslib": "~2.3.1",
"typedoc": "^0.21.5",
"typedoc-plugin-markdown": "^3.10.4",
"typescript": "^4.3.5"
"typescript": "^4.3.5",
"typedoc": "^0.22.6",
"typedoc-plugin-markdown": "^3.11.3"
},

@@ -91,0 +87,0 @@ "publishConfig": {

@@ -1,2 +0,1 @@

import localeCode from 'locale-code'
import { v4 as uuidv4 } from 'uuid'

@@ -48,3 +47,2 @@

const defaultLoginUrl = 'https://api.speechly.com/login'
const defaultLanguage = 'en-US'

@@ -78,9 +76,9 @@ declare global {

private readonly activeContexts = new Map<string, Map<number, SegmentState>>()
private readonly reconnectAttemptCount = 5
private readonly reconnectMinDelay = 1000
private readonly maxReconnectAttemptCount = 10
private readonly contextStopDelay = 250
private connectAttempt: number = 0
private stoppedContextIdPromise?: Promise<string>
private initializeMicrophonePromise?: Promise<void>
private readonly initializeApiClientPromise: Promise<void>
private resolveInitialization?: (value?: void) => void
private connectPromise: Promise<void> | null
private initializePromise: Promise<void> | null
private resolveStopContext?: (value?: unknown) => void

@@ -91,2 +89,3 @@ private readonly deviceId: string

private state: ClientState = ClientState.Disconnected
private readonly apiUrl: string

@@ -119,7 +118,2 @@ private stateChangeCb: StateChangeCallback = () => {}

const language = options.language ?? defaultLanguage
if (!(localeCode.validate(language) || (localeCode.validateLanguageCode(`${language.substring(0, 2)}-XX`) && /^..-\d\d\d$/.test(language)))) {
throw Error(`[SpeechlyClient] Invalid language "${language}"`)
}
this.debug = options.debug ?? false

@@ -130,4 +124,4 @@ this.logSegments = options.logSegments ?? false

this.projectId = options.projectId ?? undefined
const apiUrl = generateWsUrl(options.apiUrl ?? defaultApiUrl, language, options.sampleRate ?? DefaultSampleRate)
this.apiClient = options.apiClient ?? new WebWorkerController()
this.apiUrl = generateWsUrl(options.apiUrl ?? defaultApiUrl, options.sampleRate ?? DefaultSampleRate)

@@ -140,26 +134,3 @@ if (this.appId !== undefined && this.projectId !== undefined) {

this.deviceId = this.storage.getOrSet(deviceIdStorageKey, uuidv4)
const storedToken = this.storage.get(authTokenKey)
// 2. Fetch auth token. It doesn't matter if it's not present.
this.initializeApiClientPromise = new Promise(resolve => {
this.resolveInitialization = resolve
})
if (storedToken == null || !validateToken(storedToken, this.projectId, this.appId, this.deviceId)) {
fetchToken(this.loginUrl, this.projectId, this.appId, this.deviceId)
.then(token => {
this.authToken = token
// Cache the auth token in local storage for future use.
this.storage.set(authTokenKey, this.authToken)
this.connect(apiUrl)
})
.catch(err => {
this.setState(ClientState.Failed)
throw err
})
} else {
this.authToken = storedToken
this.connect(apiUrl)
}
if (window.AudioContext !== undefined) {

@@ -173,27 +144,62 @@ this.isWebkit = false

this.microphone = options.microphone ?? new BrowserMicrophone(this.isWebkit, this.sampleRate, this.apiClient, this.debug)
this.microphone =
options.microphone ?? new BrowserMicrophone(this.isWebkit, this.sampleRate, this.apiClient, this.debug)
this.apiClient.onResponse(this.handleWebsocketResponse)
this.apiClient.onClose(this.handleWebsocketClosure)
this.connectPromise = null
this.initializePromise = null
window.SpeechlyClient = this
if (options.connect !== false) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.connect()
}
}
private getReconnectDelayMs(attempt: number): number {
return 2 ** attempt * 100
}
private async sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* Esteblish websocket connection
* Connect to Speechly backend.
* This function will be called by initialize if not manually called earlier.
* Calling connect() immediately after constructor and setting callbacks allows
* prewarming the connection, resulting in less noticeable waits for the user.
*/
private connect(apiUrl: string): void {
if (this.authToken != null) {
this.apiClient.initialize(
apiUrl,
this.authToken,
this.sampleRate,
this.debug,
).then(() => {
if (this.resolveInitialization != null) {
this.resolveInitialization()
public async connect(): Promise<void> {
if (this.connectPromise === null) {
this.connectPromise = (async () => {
await this.sleep(this.getReconnectDelayMs(this.connectAttempt++))
// Get auth token from cache or renew it
const storedToken = this.storage.get(authTokenKey)
if (storedToken == null || !validateToken(storedToken, this.projectId, this.appId, this.deviceId)) {
try {
this.authToken = await fetchToken(this.loginUrl, this.projectId, this.appId, this.deviceId)
// Cache the auth token in local storage for future use.
this.storage.set(authTokenKey, this.authToken)
} catch (err) {
this.setState(ClientState.Failed)
throw err
}
} else {
this.authToken = storedToken
}
}).catch(err => {
throw err
})
// Establish websocket connection
try {
await this.apiClient.initialize(this.apiUrl, this.authToken, this.sampleRate, this.debug)
} catch (err) {
this.setState(ClientState.Failed)
throw err
}
})()
}
await this.connectPromise
}

@@ -211,77 +217,73 @@

async initialize(): Promise<void> {
await this.initializeApiClientPromise
if (this.state !== ClientState.Disconnected) {
throw Error('Cannot initialize client - client is not in Disconnected state')
}
// Ensure we're connected. Returns immediately if we are
await this.connect()
this.setState(ClientState.Connecting)
if (this.initializePromise === null) {
this.initializePromise = (async () => {
this.setState(ClientState.Connecting)
try {
if (this.isWebkit) {
if (window.webkitAudioContext !== undefined) {
// eslint-disable-next-line new-cap
this.audioContext = new window.webkitAudioContext()
}
} else {
const opts: AudioContextOptions = {}
if (this.nativeResamplingSupported) {
opts.sampleRate = this.sampleRate
}
try {
// 1. Initialise the storage and fetch deviceId (or generate new one and store it).
// await this.storage.initialize()
// this.deviceId = await this.storage.getOrSet(deviceIdStorageKey, uuidv4)
this.audioContext = new window.AudioContext(opts)
}
// 2. Initialise the microphone stack.
if (this.isWebkit) {
if (window.webkitAudioContext !== undefined) {
// eslint-disable-next-line new-cap
this.audioContext = new window.webkitAudioContext()
}
} else {
const opts: AudioContextOptions = {}
if (this.nativeResamplingSupported) {
opts.sampleRate = this.sampleRate
}
const mediaStreamConstraints: MediaStreamConstraints = {
video: false,
}
this.audioContext = new window.AudioContext(opts)
}
if (this.nativeResamplingSupported || this.autoGainControl) {
mediaStreamConstraints.audio = {
sampleRate: this.sampleRate,
// @ts-ignore
autoGainControl: this.autoGainControl,
}
} else {
mediaStreamConstraints.audio = true
}
const mediaStreamConstraints: MediaStreamConstraints = {
video: false,
}
if (this.audioContext != null) {
// Start audio context if we are dealing with a WebKit browser.
//
// WebKit browsers (e.g. Safari) require to resume the context first,
// before obtaining user media by calling `mediaDevices.getUserMedia`.
//
// If done in a different order, the audio context will resume successfully,
// but will emit empty audio buffers.
if (this.isWebkit) {
await this.audioContext.resume()
}
// 3. Initialise websocket.
await this.apiClient.setSourceSampleRate(this.audioContext.sampleRate)
await this.microphone.initialize(this.audioContext, mediaStreamConstraints)
} else {
throw ErrDeviceNotSupported
}
} catch (err) {
switch (err) {
case ErrDeviceNotSupported:
this.setState(ClientState.NoBrowserSupport)
break
case ErrNoAudioConsent:
this.setState(ClientState.NoAudioConsent)
break
default:
this.setState(ClientState.Failed)
}
if (this.nativeResamplingSupported || this.autoGainControl) {
mediaStreamConstraints.audio = {
sampleRate: this.sampleRate,
// @ts-ignore
autoGainControl: this.autoGainControl,
throw err
}
} else {
mediaStreamConstraints.audio = true
}
if (this.audioContext != null) {
// Start audio context if we are dealing with a WebKit browser.
//
// WebKit browsers (e.g. Safari) require to resume the context first,
// before obtaining user media by calling `mediaDevices.getUserMedia`.
//
// If done in a different order, the audio context will resume successfully,
// but will emit empty audio buffers.
if (this.isWebkit) {
await this.audioContext.resume()
}
// 3. Initialise websocket.
await this.apiClient.setSourceSampleRate(this.audioContext.sampleRate)
this.initializeMicrophonePromise = this.microphone.initialize(this.audioContext, mediaStreamConstraints)
await this.initializeMicrophonePromise
} else {
throw ErrDeviceNotSupported
}
} catch (err) {
switch (err) {
case ErrDeviceNotSupported:
this.setState(ClientState.NoBrowserSupport)
break
case ErrNoAudioConsent:
this.setState(ClientState.NoAudioConsent)
break
default:
this.setState(ClientState.Failed)
}
throw err
this.setState(ClientState.Connected)
})()
}
this.setState(ClientState.Connected)
await this.initializePromise
}

@@ -310,2 +312,4 @@

this.activeContexts.clear()
this.connectPromise = null
this.initializePromise = null
this.setState(ClientState.Disconnected)

@@ -336,2 +340,5 @@

async startContext(appId?: string): Promise<string> {
// Ensure we're initialized; returns immediately if we are
await this.initialize()
if (this.resolveStopContext != null) {

@@ -572,18 +579,34 @@ this.resolveStopContext()

private readonly handleWebsocketClosure = (err: Error): void => {
private readonly handleWebsocketClosure = (err: { code: number, reason: string, wasClean: boolean }): void => {
if (err.code === 1000) {
if (this.debug) {
console.log('[SpeechlyClient]', 'Websocket closed', err)
}
} else {
if (this.debug) {
console.error('[SpeechlyClient]', 'Websocket closed due to error', err)
}
// If for some reason deviceId is missing, there's nothing else we can do but fail completely.
if (this.deviceId === undefined) {
this.setState(ClientState.Failed)
return
}
this.reconnect()
}
}
private reconnect(): void {
if (this.debug) {
console.error('[SpeechlyClient]', 'Server connection closed', err)
console.log('[SpeechlyClient]', 'Reconnecting...', this.connectAttempt)
}
// If for some reason deviceId is missing, there's nothing else we can do but fail completely.
if (this.deviceId === undefined) {
if (this.state !== ClientState.Failed && this.connectAttempt < this.maxReconnectAttemptCount) {
this.connectPromise = null
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.connect()
} else {
console.error('[SpeechlyClient] Maximum reconnect count reached, giving up.')
this.setState(ClientState.Failed)
return
}
// Make sure we don't have concurrent reconnection procedures or attempt to reconnect from a failed state.
if (this.state === ClientState.Connecting || this.state === ClientState.Failed) {
return
}
this.setState(ClientState.Connecting)
}

@@ -612,5 +635,4 @@

function generateWsUrl(baseUrl: string, languageCode: string, sampleRate: number): string {
function generateWsUrl(baseUrl: string, sampleRate: number): string {
const params = new URLSearchParams()
params.append('languageCode', languageCode)
params.append('sampleRate', sampleRate.toString())

@@ -617,0 +639,0 @@

@@ -16,2 +16,7 @@ import { Microphone } from '../microphone'

/**
* Connect to Speechly upon creating the client instance. Defaults to true.
*/
connect?: boolean
/**
* The unique identifier of a project in the dashboard.

@@ -22,2 +27,3 @@ */

/**
* @deprecated
* The language which is used by the app.

@@ -24,0 +30,0 @@ */

@@ -37,2 +37,3 @@ /**

Opened = 'WEBSOCKET_OPEN',
Closed = 'WEBSOCKET_CLOSED',
SourceSampleRateSetSuccess = 'SOURSE_SAMPLE_RATE_SET_SUCCESS',

@@ -152,3 +153,3 @@ Started = 'started',

*/
export type CloseCallback = (err: Error) => void
export type CloseCallback = (err: {code: number, reason: string, wasClean: boolean}) => void

@@ -155,0 +156,0 @@ /**

@@ -127,2 +127,9 @@ import { APIClient, ResponseCallback, CloseCallback, WebsocketResponse, WebsocketResponseType } from './types'

break
case WebsocketResponseType.Closed:
this.onCloseCb({
code: event.data.code,
reason: event.data.reason,
wasClean: event.data.wasClean,
})
break
case WebsocketResponseType.SourceSampleRateSetSuccess:

@@ -129,0 +136,0 @@ if (this.resolveSourceSampleRateSet != null) {

@@ -27,5 +27,16 @@ export default `/**

this.initialized = false;
// WebSocket's close handler, called e.g. when
// - normal close (code 1000)
// - network unreachable or unable to (re)connect (code 1006)
// List of CloseEvent.code values: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
this.onWebsocketClose = function (event) {
if (_this.debug) {
console.log('[SpeechlyClient]', 'onWebsocketClose');
}
_this.websocket.removeEventListener('open', _this.onWebsocketOpen);
_this.websocket.removeEventListener('message', _this.onWebsocketMessage);
_this.websocket.removeEventListener('error', _this.onWebsocketError);
_this.websocket.removeEventListener('close', _this.onWebsocketClose);
_this.websocket = undefined;
_this.connect(0);
_this.workerCtx.postMessage({ type: 'WEBSOCKET_CLOSED', code: event.code, reason: event.reason, wasClean: event.wasClean });
};

@@ -45,3 +56,2 @@ this.onWebsocketOpen = function (_event) {

}
_this.closeWebsocket();
};

@@ -69,6 +79,2 @@ this.onWebsocketMessage = function (event) {

WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {
if (this.initialized) {
console.log('[SpeechlyClient]', 'already initialized');
return;
}
this.debug = debug;

@@ -201,3 +207,3 @@ if (this.debug) {

WebsocketClient.prototype.stopContext = function () {
if (this.websocket == undefined) {
if (!this.websocket) {
throw Error('Cant start context: websocket is undefined');

@@ -215,3 +221,3 @@ }

WebsocketClient.prototype.switchContext = function (newAppId) {
if (this.websocket == undefined) {
if (!this.websocket) {
throw Error('Cant switch context: websocket is undefined');

@@ -233,11 +239,12 @@ }

};
WebsocketClient.prototype.closeWebsocket = function () {
if (this.websocket == null) {
WebsocketClient.prototype.closeWebsocket = function (websocketCode, reason) {
if (websocketCode === void 0) { websocketCode = 1005; }
if (reason === void 0) { reason = "No Status Received"; }
if (this.debug) {
console.log('[SpeechlyClient]', 'Websocket closing');
}
if (!this.websocket) {
throw Error('Websocket is not open');
}
this.websocket.removeEventListener('open', this.onWebsocketOpen);
this.websocket.removeEventListener('message', this.onWebsocketMessage);
this.websocket.removeEventListener('error', this.onWebsocketError);
this.websocket.removeEventListener('close', this.onWebsocketClose);
this.websocket.close();
this.websocket.close(websocketCode, reason);
};

@@ -294,3 +301,3 @@ WebsocketClient.prototype.downsample = function (input) {

case 'CLOSE':
websocketClient.closeWebsocket();
websocketClient.closeWebsocket(1000, "Close requested by client");
break;

@@ -297,0 +304,0 @@ case 'START_CONTEXT':

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc