diff --git a/src/AnamClient.ts b/src/AnamClient.ts index e9b7607..89a61e8 100644 --- a/src/AnamClient.ts +++ b/src/AnamClient.ts @@ -45,7 +45,11 @@ export default class AnamClient { options, ); if (configError) { - throw new Error(configError); + throw new ClientError( + configError, + ErrorCode.CLIENT_ERROR_CODE_CONFIGURATION_ERROR, + 400, + ); } this.personaConfig = personaConfig; @@ -114,6 +118,9 @@ export default class AnamClient { // Validate voice detection configuration if (options?.voiceDetection) { + if (options.disableInputAudio) { + return 'Voice detection is disabled because input audio is disabled. Please set disableInputAudio to false to enable voice detection.'; + } // End of speech sensitivity must be a number between 0 and 1 if (options.voiceDetection.endOfSpeechSensitivity !== undefined) { if (typeof options.voiceDetection.endOfSpeechSensitivity !== 'number') { @@ -183,8 +190,11 @@ export default class AnamClient { iceServers, inputAudio: { inputAudioState: this.inputAudioState, - userProvidedMediaStream: userProvidedAudioStream, + userProvidedMediaStream: this.clientOptions?.disableInputAudio + ? undefined + : userProvidedAudioStream, audioDeviceId: this.clientOptions?.audioDeviceId, + disableInputAudio: this.clientOptions?.disableInputAudio, }, }, this.publicEventEmitter, @@ -203,9 +213,9 @@ export default class AnamClient { return sessionId; } - private async startSessionIfNeeded(userProvidedMediaStream?: MediaStream) { + private async startSessionIfNeeded(userProvidedAudioStream?: MediaStream) { if (!this.sessionId || !this.streamingClient) { - await this.startSession(userProvidedMediaStream); + await this.startSession(userProvidedAudioStream); if (!this.sessionId || !this.streamingClient) { throw new ClientError( @@ -223,6 +233,11 @@ export default class AnamClient { public async stream( userProvidedAudioStream?: MediaStream, ): Promise { + if (this.clientOptions?.disableInputAudio && userProvidedAudioStream) { + console.warn( + 'AnamClient:Input audio is disabled. User provided audio stream will be ignored.', + ); + } await this.startSessionIfNeeded(userProvidedAudioStream); if (this._isStreaming) { throw new Error('Already streaming'); @@ -261,10 +276,15 @@ export default class AnamClient { public async streamToVideoAndAudioElements( videoElementId: string, audioElementId: string, - userProvidedMediaStream?: MediaStream, + userProvidedAudioStream?: MediaStream, ): Promise { + if (this.clientOptions?.disableInputAudio && userProvidedAudioStream) { + console.warn( + 'AnamClient:Input audio is disabled. User provided audio stream will be ignored.', + ); + } try { - await this.startSessionIfNeeded(userProvidedMediaStream); + await this.startSessionIfNeeded(userProvidedAudioStream); } catch (error) { if (error instanceof ClientError) { throw error; @@ -340,6 +360,11 @@ export default class AnamClient { } public getInputAudioState(): InputAudioState { + if (this.clientOptions?.disableInputAudio) { + console.warn( + 'AnamClient: Audio state will not be used because input audio is disabled.', + ); + } // if streaming client is available, make sure our state is up to date if (this.streamingClient) { this.inputAudioState = this.streamingClient.getInputAudioState(); @@ -347,7 +372,12 @@ export default class AnamClient { return this.inputAudioState; } public muteInputAudio(): InputAudioState { - if (this.streamingClient) { + if (this.clientOptions?.disableInputAudio) { + console.warn( + 'AnamClient: Input audio is disabled. Muting input audio will have no effect.', + ); + } + if (this.streamingClient && !this.clientOptions?.disableInputAudio) { this.inputAudioState = this.streamingClient.muteInputAudio(); } else { this.inputAudioState = { @@ -359,7 +389,12 @@ export default class AnamClient { } public unmuteInputAudio(): InputAudioState { - if (this.streamingClient) { + if (this.clientOptions?.disableInputAudio) { + console.warn( + 'AnamClient: Input audio is disabled. Unmuting input audio will have no effect.', + ); + } + if (this.streamingClient && !this.clientOptions?.disableInputAudio) { this.inputAudioState = this.streamingClient.unmuteInputAudio(); } else { this.inputAudioState = { diff --git a/src/lib/ClientError.ts b/src/lib/ClientError.ts index ed0ac4a..4531a2a 100644 --- a/src/lib/ClientError.ts +++ b/src/lib/ClientError.ts @@ -7,6 +7,7 @@ export enum ErrorCode { CLIENT_ERROR_CODE_SERVICE_BUSY = 'CLIENT_ERROR_CODE_SERVICE_BUSY', CLIENT_ERROR_CODE_NO_PLAN_FOUND = 'CLIENT_ERROR_CODE_NO_PLAN_FOUND', CLIENT_ERROR_CODE_UNKNOWN_ERROR = 'CLIENT_ERROR_CODE_UNKNOWN_ERROR', + CLIENT_ERROR_CODE_CONFIGURATION_ERROR = 'CLIENT_ERROR_CODE_CONFIGURATION_ERROR', } // TODO: Move to CoreApiRestClient if we have a pattern for not exposing this diff --git a/src/modules/StreamingClient.ts b/src/modules/StreamingClient.ts index b7afc6f..75a70c2 100644 --- a/src/modules/StreamingClient.ts +++ b/src/modules/StreamingClient.ts @@ -38,6 +38,7 @@ export class StreamingClient { private audioStream: MediaStream | null = null; private inputAudioState: InputAudioState = { isMuted: false }; private audioDeviceId: string | undefined; + private disableInputAudio: boolean; constructor( sessionId: string, @@ -53,6 +54,7 @@ export class StreamingClient { if (options.inputAudio.userProvidedMediaStream) { this.inputAudioStream = options.inputAudio.userProvidedMediaStream; } + this.disableInputAudio = options.inputAudio.disableInputAudio === true; // register event handlers this.internalEventEmitter.addListener( InternalEvent.WEB_SOCKET_OPEN, @@ -413,42 +415,45 @@ export class StreamingClient { * Audio * * If the user hasn't provided an audio stream, capture the audio stream from the user's microphone and send it to the peer connection + * If input audio is disabled we don't send any audio to the peer connection */ - if (this.inputAudioStream) { - // verify the user provided stream has audio tracks - if (!this.inputAudioStream.getAudioTracks().length) { - throw new Error( - 'StreamingClient - setupDataChannels: user provided stream does not have audio tracks', - ); - } - } else { - const audioConstraints: MediaTrackConstraints = { - echoCancellation: true, - }; - - // If an audio device ID is provided in the options, use it - if (this.audioDeviceId) { - audioConstraints.deviceId = { - exact: this.audioDeviceId, + if (!this.disableInputAudio) { + if (this.inputAudioStream) { + // verify the user provided stream has audio tracks + if (!this.inputAudioStream.getAudioTracks().length) { + throw new Error( + 'StreamingClient - setupDataChannels: user provided stream does not have audio tracks', + ); + } + } else { + const audioConstraints: MediaTrackConstraints = { + echoCancellation: true, }; - } - this.inputAudioStream = await navigator.mediaDevices.getUserMedia({ - audio: audioConstraints, - }); - } + // If an audio device ID is provided in the options, use it + if (this.audioDeviceId) { + audioConstraints.deviceId = { + exact: this.audioDeviceId, + }; + } - // mute the audio tracks if the user has muted the microphone - if (this.inputAudioState.isMuted) { - this.muteAllAudioTracks(); + this.inputAudioStream = await navigator.mediaDevices.getUserMedia({ + audio: audioConstraints, + }); + } + + // mute the audio tracks if the user has muted the microphone + if (this.inputAudioState.isMuted) { + this.muteAllAudioTracks(); + } + const audioTrack = this.inputAudioStream.getAudioTracks()[0]; + this.peerConnection.addTrack(audioTrack, this.inputAudioStream); + // pass the stream to the callback if it exists + this.publicEventEmitter.emit( + AnamEvent.INPUT_AUDIO_STREAM_STARTED, + this.inputAudioStream, + ); } - const audioTrack = this.inputAudioStream.getAudioTracks()[0]; - this.peerConnection.addTrack(audioTrack, this.inputAudioStream); - // pass the stream to the callback if it exists - this.publicEventEmitter.emit( - AnamEvent.INPUT_AUDIO_STREAM_STARTED, - this.inputAudioStream, - ); /** * Text @@ -462,9 +467,7 @@ export class StreamingClient { dataChannel.onopen = () => { this.dataChannel = dataChannel ?? null; }; - dataChannel.onclose = () => { - // TODO: should we set the data channel to null here? - }; + dataChannel.onclose = () => {}; // pass text message to the message history client dataChannel.onmessage = (event) => { const messageEvent = JSON.parse(event.data) as WebRtcTextMessageEvent; diff --git a/src/types/AnamPublicClientOptions.ts b/src/types/AnamPublicClientOptions.ts index 90617f2..49f5759 100644 --- a/src/types/AnamPublicClientOptions.ts +++ b/src/types/AnamPublicClientOptions.ts @@ -5,4 +5,5 @@ export interface AnamPublicClientOptions { api?: CoreApiRestClientOptions; voiceDetection?: VoiceDetectionOptions; audioDeviceId?: string; + disableInputAudio?: boolean; } diff --git a/src/types/streaming/InputAudioOptions.ts b/src/types/streaming/InputAudioOptions.ts index 989a8ee..ee1842e 100644 --- a/src/types/streaming/InputAudioOptions.ts +++ b/src/types/streaming/InputAudioOptions.ts @@ -4,4 +4,5 @@ export interface InputAudioOptions { inputAudioState: InputAudioState; userProvidedMediaStream?: MediaStream; audioDeviceId?: string; + disableInputAudio?: boolean; }