diff --git a/package-lock.json b/package-lock.json index 1f685a43..d06ed37a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hifi-spatial-audio", - "version": "1.4.1", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7166a8ba..2aa672ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hifi-spatial-audio", - "version": "1.4.1", + "version": "1.5.0", "description": "The High Fidelity Audio Client Library allows developers to integrate High Fidelity's spatial audio technology into their projects.", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/classes/HiFiCommunicator.ts b/src/classes/HiFiCommunicator.ts index cf55ef20..8d19f853 100644 --- a/src/classes/HiFiCommunicator.ts +++ b/src/classes/HiFiCommunicator.ts @@ -123,12 +123,8 @@ export interface ConnectionRetryAndTimeoutConfig { * connection is disconnected. When this is set to true, we will attempt * to reconnect if any disconnect from any cause occurs. * By default, reconnections are not automatically attempted. - * NOTE: The retrying that happens when this is set to `true` does not currently take into account - * the reason WHY a connection was disconnected. This means that if this is - * set to true, a connection that is disconnected via a purposeful server-side - * action (e.g. a "kick") will be automatically reconnected. (However, connections - * that are explicitly closed from the client side via the `disconnectFromHiFiAudioAPIServer()` - * method will stay closed.) + * The client will not attempt to reconnect when the client is 'kicked' + * or the space is forcibly shut down. */ autoRetryOnDisconnect?: boolean; /** @@ -178,7 +174,12 @@ export interface HiFiConnectionAttemptResult { * is made. It is primarily used for debugging and includes information about the server build version and * other details. This will not be set if there is an error when connecting to the server. */ - audionetInitResponse?: any + audionetInitResponse?: any, + /** + * `disableReconnect` will be present and 'true' on a failed connection attempt or disconnect if reconnects should not + * be performed. This typically happens during a kick operation. + */ + disableReconnect?: boolean; } /** @@ -458,6 +459,9 @@ export class HiFiCommunicator { signalingHostURLSafe = signalingHostURL ? signalingHostURL : HiFiConstants.DEFAULT_PROD_HIGH_FIDELITY_ENDPOINT; } + // if reconnections are enabled, attempt them if necessary. + this._mixerSession._disableReconnect = false; + signalingPort = signalingPort ? signalingPort : HiFiConstants.DEFAULT_PROD_HIGH_FIDELITY_PORT; let webRTCSignalingAddress = `wss://${signalingHostURLSafe}:${signalingPort}/?token=`; this._mixerSession.webRTCAddress = `${webRTCSignalingAddress}${hifiAuthJWT}`; @@ -513,7 +517,7 @@ export class HiFiCommunicator { // gets handled by the _manageConnection callback handler. (Note that calls to our _connectToHiFiMixer() // method get handled entirely by callback-initiated retry code, so we should never get here unless // a callback asked us to do it.) - this._mixerSession.connectToHiFiMixer({ webRTCSessionParams: this._webRTCSessionParams, customSTUNandTURNConfig: this._customSTUNandTURNConfig, timeout: timeoutPerConnectionAttempt }); + this._mixerSession.connectToHiFiMixer({ webRTCSessionParams: this._webRTCSessionParams, customSTUNandTURNConfig: this._customSTUNandTURNConfig, timeout: timeoutPerConnectionAttempt, initData: this._currentHiFiAudioAPIData }); } /** @@ -621,14 +625,15 @@ export class HiFiCommunicator { // OK, we've dealt with the situation where there's already a retry cycle going. // Now see if we need to start one. let retriesTimeoutMs = 0; - if (this._currentHiFiConnectionState === HiFiConnectionStates.Connecting && - this._connectionRetryAndTimeoutConfig.autoRetryInitialConnection) { + this._connectionRetryAndTimeoutConfig.autoRetryInitialConnection && + !message.disableReconnect) { // The user has started a connection attempt. It failed, and they want to retry. retriesTimeoutMs = 1000 * this._connectionRetryAndTimeoutConfig.maxSecondsToSpendRetryingInitialConnection; } else if (this._currentHiFiConnectionState === HiFiConnectionStates.Reconnecting && - this._connectionRetryAndTimeoutConfig.autoRetryOnDisconnect) { + this._connectionRetryAndTimeoutConfig.autoRetryOnDisconnect && + !message.disableReconnect) { // The user had previously been trying to reconnect. It failed, and they want to keep retrying. // (Note - we're not even supposed to be here; this situation should have been // caught by the "there's already a timer in play" logic above. @@ -637,7 +642,8 @@ export class HiFiCommunicator { retriesTimeoutMs = 1000 * this._connectionRetryAndTimeoutConfig.maxSecondsToSpendRetryingOnDisconnect; } else if (this._currentHiFiConnectionState === HiFiConnectionStates.Connected && - this._connectionRetryAndTimeoutConfig.autoRetryOnDisconnect) { + this._connectionRetryAndTimeoutConfig.autoRetryOnDisconnect && + !message.disableReconnect) { // The user had previously been connected. They got disconnected, and they want to retry retriesTimeoutMs = 1000 * this._connectionRetryAndTimeoutConfig.maxSecondsToSpendRetryingOnDisconnect; @@ -960,7 +966,7 @@ export class HiFiCommunicator { * @param position - The new position of the user. * @param orientationQuat - The new orientationQuat of the user. * @param orientationEuler - The new orientationEuler of the user. - * @param volumeThreshold - The new volumeThreshold of the user. + * @param volumeThreshold - The new volumeThreshold of the user. Setting this to null will use the space default volume threshold. * @param hiFiGain - This value affects how loud User A will sound to User B at a given distance in 3D space. * This value also affects the distance at which User A can be heard in 3D space. * Higher values for User A means that User A will sound louder to other users around User A, and it also means that User A will be audible from a greater distance. @@ -997,7 +1003,8 @@ export class HiFiCommunicator { this._currentHiFiAudioAPIData.orientationQuat = eulerToQuaternion(checkedEuler, ourHiFiAxisConfiguration.eulerOrder); } - if (typeof (volumeThreshold) === "number") { + if (typeof (volumeThreshold) === "number" || + volumeThreshold === null) { this._currentHiFiAudioAPIData.volumeThreshold = volumeThreshold; } if (typeof (hiFiGain) === "number") { @@ -1052,7 +1059,8 @@ export class HiFiCommunicator { this._lastTransmittedHiFiAudioAPIData.orientationQuat.z = dataJustTransmitted.orientationQuat.z ?? this._lastTransmittedHiFiAudioAPIData.orientationQuat.z; } - if (typeof (dataJustTransmitted.volumeThreshold) === "number") { + if (typeof (dataJustTransmitted.volumeThreshold) === "number" || + dataJustTransmitted.volumeThreshold === null) { this._lastTransmittedHiFiAudioAPIData["volumeThreshold"] = dataJustTransmitted.volumeThreshold; } diff --git a/src/classes/HiFiMixerSession.ts b/src/classes/HiFiMixerSession.ts index 886688b2..4b466721 100644 --- a/src/classes/HiFiMixerSession.ts +++ b/src/classes/HiFiMixerSession.ts @@ -220,6 +220,12 @@ export class HiFiMixerSession { * in the process of trying to connect. */ _tryingToConnect: boolean; + + /** + * Don't attempt a reconnect if kicked or the space is shut down. This value is + * used internally and may change depending on server activity. + */ + _disableReconnect: boolean; /** * Used for diagnostics @@ -271,6 +277,7 @@ export class HiFiMixerSession { this._lastSuccessfulInputAudioMutedValue = false; this.onMuteChanged = onMuteChanged; this._getUserFacingConnectionState = getUserFacingConnectionState; + this._disableReconnect = false; RaviUtils.setDebug(false); @@ -300,7 +307,7 @@ export class HiFiMixerSession { * @returns If this operation is successful, the Promise will resolve with `{ success: true, audionetInitResponse: }`. * If unsuccessful, the Promise will reject with `{ success: false, error: }`. */ - async promiseToRunAudioInit(): Promise { + async promiseToRunAudioInit(currentHifiAudioAPIData? : HiFiAudioAPIData): Promise { return new Promise((resolve, reject) => { let initData = { primary: true, @@ -310,6 +317,12 @@ export class HiFiMixerSession { streaming_scope: this.userDataStreamingScope, is_input_stream_stereo: this._inputAudioMediaStreamIsStereo }; + + if (currentHifiAudioAPIData) { + let initialDataToSend = this._getDataToTransmitToMixer(currentHifiAudioAPIData); + initData = { ...initData, ...initialDataToSend }; + } + let commandController = this._raviSession.getCommandController(); if (!commandController) { return reject({ @@ -322,7 +335,8 @@ export class HiFiMixerSession { let errMsg = `Couldn't connect to mixer: Call to \`init\` timed out!` return reject({ success: false, - error: errMsg + error: errMsg, + disableReconnect: this._disableReconnect }); }, INIT_TIMEOUT_MS); @@ -338,12 +352,14 @@ export class HiFiMixerSession { this.mixerInfo["visit_id_hash"] = parsedResponse.visit_id_hash; return resolve({ success: true, - audionetInitResponse: parsedResponse + audionetInitResponse: parsedResponse, + disableReconnect: this._disableReconnect }); } catch (e) { return reject({ success: false, - error: `Couldn't parse init response! Parse error:\n${e}` + error: `Couldn't parse init response! Parse error:\n${e}`, + disableReconnect: this._disableReconnect }); } }); @@ -555,6 +571,12 @@ export class HiFiMixerSession { if (shouldBeMuted !== undefined) { this._setMutedByAdmin(shouldBeMuted, MuteReason.ADMIN); } + } else if (instructionName === "terminate") { + // all reasons for termination currently should result in a disconnect + // so that the client doesn't try to automatically reconnect. Reasons + // will be either kick or user timeout. + this._disableReconnect = true; + this._disconnectFromHiFiMixer(); } } } @@ -567,7 +589,7 @@ export class HiFiMixerSession { * @param webRTCSessionParams - Parameters passed to the RAVI session when opening that session. * @returns void. Use the callback function to get information about errors upon failure, or the response from `audionet.init` when successful */ - connectToHiFiMixer({ webRTCSessionParams, customSTUNandTURNConfig, timeout }: { webRTCSessionParams?: WebRTCSessionParams, customSTUNandTURNConfig?: CustomSTUNandTURNConfig, timeout?: number }): void { + connectToHiFiMixer({ webRTCSessionParams, customSTUNandTURNConfig, timeout, initData }: { webRTCSessionParams?: WebRTCSessionParams, customSTUNandTURNConfig?: CustomSTUNandTURNConfig, timeout?: number, initData?: HiFiAudioAPIData }): void { if (this._tryingToConnect) { HiFiLogger.warn("`HiFiMixerSession.connectToHiFiMixer()` was called, but is already in the process of connecting. No action will be taken."); @@ -576,14 +598,14 @@ export class HiFiMixerSession { if (this.mixerInfo["connected"]) { let msg = `Already connected! If a reconnect is needed, please hang up and try again.`; - this._onConnectionStateChange(HiFiConnectionStates.Connected, { success: true, error: msg }); + this._onConnectionStateChange(HiFiConnectionStates.Connected, { success: true, error: msg, disableReconnect: this._disableReconnect }); return; } if (!this.webRTCAddress) { let errMsg = `Couldn't connect: \`this.webRTCAddress\` is falsey!`; // this._onConnectionStateChange will attempt a clean-up disconnect for us - this._onConnectionStateChange(HiFiConnectionStates.Failed, { success: false, error: errMsg }); + this._onConnectionStateChange(HiFiConnectionStates.Failed, { success: false, error: errMsg, disableReconnect: this._disableReconnect }); return; } @@ -592,7 +614,7 @@ export class HiFiMixerSession { if (event && event.state === RaviSignalingStates.UNAVAILABLE) { mixerIsUnavailable = true; let message = `High Fidelity server is at capacity; service is unavailable.`; - this._onConnectionStateChange(HiFiConnectionStates.Unavailable, { success: false, error: message }); + this._onConnectionStateChange(HiFiConnectionStates.Unavailable, { success: false, error: message, disableReconnect: this._disableReconnect }); this._raviSignalingConnection.removeStateChangeHandler(tempUnavailableStateHandler); this._raviSession.closeRAVISession(); } @@ -623,14 +645,14 @@ export class HiFiMixerSession { let errMsg = `Couldn't open RAVI session associated with \`${this.webRTCAddress.slice(0, this.webRTCAddress.indexOf("token="))}\`! Error:\n${RaviUtils.safelyPrintable(errorOpeningRAVISession)}`; if (mixerIsUnavailable) { errMsg = `High Fidelity server is at capacity; service is unavailable.`; - this._onConnectionStateChange(HiFiConnectionStates.Unavailable, { success: false, error: errMsg }); + this._onConnectionStateChange(HiFiConnectionStates.Unavailable, { success: false, error: errMsg, disableReconnect: this._disableReconnect }); } throw(errMsg); }); }) .then((value) => { HiFiLogger.log(`Session open; running audionet.init`); - return this.promiseToRunAudioInit() + return this.promiseToRunAudioInit(initData) .catch((errorRunningAudionetInit) => { let errMsg = `Connected, but was then unable to communicate the \`audionet.init\` message to the server. Error:\n${RaviUtils.safelyPrintable(errorRunningAudionetInit)}`; throw(errMsg); @@ -652,7 +674,7 @@ export class HiFiMixerSession { // No matter what happens up there, we want to go to a failed state // and pass along the error message. `this._onConnectionStateChange` // will disconnect and clean up for us. - this._onConnectionStateChange(HiFiConnectionStates.Failed, { success: false, error: error }); + this._onConnectionStateChange(HiFiConnectionStates.Failed, { success: false, error: error, disableReconnect: this._disableReconnect }); }) .finally(() => { this._raviSignalingConnection.removeStateChangeHandler(tempUnavailableStateHandler); @@ -703,7 +725,7 @@ export class HiFiMixerSession { await this._setMutedByAdmin(false, MuteReason.INTERNAL); - this._onConnectionStateChange(HiFiConnectionStates.Disconnected, { success: true, error: "Successfully disconnected" }); + this._onConnectionStateChange(HiFiConnectionStates.Disconnected, { success: true, error: "Successfully disconnected", disableReconnect: this._disableReconnect }); return Promise.resolve(`Successfully disconnected.`); } @@ -965,7 +987,8 @@ export class HiFiMixerSession { HiFiLogger.log(`New RAVI signaling state: \`${event.state}\``); switch (event.state) { case RaviSignalingStates.UNAVAILABLE: - this._onConnectionStateChange(HiFiConnectionStates.Unavailable, { success: false, error: `High Fidelity server is at capacity; service is unavailable.` }); + this._disableReconnect = true; + this._onConnectionStateChange(HiFiConnectionStates.Unavailable, { success: false, error: `High Fidelity server is at capacity; service is unavailable.`, disableReconnect: this._disableReconnect }); try { await this._disconnectFromHiFiMixer(); } catch (errorClosing) { @@ -1010,12 +1033,12 @@ export class HiFiMixerSession { break; case RaviSessionStates.CLOSED: message = "RaviSession has been closed; connection to High Fidelity servers has been disconnected"; - this._onConnectionStateChange(HiFiConnectionStates.Disconnected, { success: true, error: message }); + this._onConnectionStateChange(HiFiConnectionStates.Disconnected, { success: true, error: message, disableReconnect: this._disableReconnect }); break; case RaviSessionStates.DISCONNECTED: case RaviSessionStates.FAILED: message = "RaviSession has disconnected unexpectedly"; - this._onConnectionStateChange(HiFiConnectionStates.Failed, { success: false, error: message }); + this._onConnectionStateChange(HiFiConnectionStates.Failed, { success: false, error: message, disableReconnect: this._disableReconnect }); try { await this._disconnectFromHiFiMixer(); } catch (errorClosing) { @@ -1052,18 +1075,9 @@ export class HiFiMixerSession { } /** - * @param currentHifiAudioAPIData - The new user data that we want to send to the High Fidelity Audio API server. - * @returns If this operation is successful, returns `{ success: true, stringifiedDataForMixer: }`. If unsuccessful, returns - * `{ success: false, error: }`. + * This method converts the HiFiAudioAPIData structure into the format needed by the mixer. */ - _transmitHiFiAudioAPIDataToServer(currentHifiAudioAPIData: HiFiAudioAPIData, previousHifiAudioAPIData?: HiFiAudioAPIData): any { - if (!this.mixerInfo["connected"] || !this._raviSession) { - return { - success: false, - error: `Can't transmit data to mixer; not connected to mixer.` - }; - } - + _getDataToTransmitToMixer(currentHifiAudioAPIData: HiFiAudioAPIData, previousHifiAudioAPIData?: HiFiAudioAPIData): any { let dataForMixer: any = {}; // if a position is specified with valid components, let's consider adding position payload @@ -1162,7 +1176,8 @@ export class HiFiMixerSession { } } - if (typeof (currentHifiAudioAPIData.volumeThreshold) === "number") { + if (typeof (currentHifiAudioAPIData.volumeThreshold) === "number" || + currentHifiAudioAPIData.volumeThreshold === null) { dataForMixer["T"] = currentHifiAudioAPIData.volumeThreshold; } @@ -1198,6 +1213,23 @@ export class HiFiMixerSession { dataForMixer["V"] = changedUserGains; } } + return dataForMixer; + } + + /** + * @param currentHifiAudioAPIData - The new user data that we want to send to the High Fidelity Audio API server. + * @returns If this operation is successful, returns `{ success: true, stringifiedDataForMixer: }`. If unsuccessful, returns + * `{ success: false, error: }`. + */ + _transmitHiFiAudioAPIDataToServer(currentHifiAudioAPIData: HiFiAudioAPIData, previousHifiAudioAPIData?: HiFiAudioAPIData): any { + if (!this.mixerInfo["connected"] || !this._raviSession) { + return { + success: false, + error: `Can't transmit data to mixer; not connected to mixer.` + }; + } + + let dataForMixer = this._getDataToTransmitToMixer(currentHifiAudioAPIData, previousHifiAudioAPIData); if (Object.keys(dataForMixer).length === 0) { // We call this a "success" even though we didn't send anything to the mixer. diff --git a/tests/unit/src/classes/HiFiMixerSession.unit.test.ts b/tests/unit/src/classes/HiFiMixerSession.unit.test.ts index b80f5875..481beaed 100644 --- a/tests/unit/src/classes/HiFiMixerSession.unit.test.ts +++ b/tests/unit/src/classes/HiFiMixerSession.unit.test.ts @@ -6,15 +6,17 @@ test(`brand new mixer session can't connect`, async () => { const stateChangeCallback = jest.fn(); let newMixerSession = new HiFiMixerSession({ - onConnectionStateChanged: stateChangeCallback + onConnectionStateChanged: stateChangeCallback }); const failureResult:HiFiConnectionAttemptResult = { "error": "Couldn't connect: `this.webRTCAddress` is falsey!", "success": false, + disableReconnect: false, }; const disconnectResult:HiFiConnectionAttemptResult = { "error": "Successfully disconnected", "success": true, + disableReconnect: false, }; newMixerSession.connectToHiFiMixer({ webRTCSessionParams: {} }); await sleep(1000);