Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions src/AnamClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -223,6 +233,11 @@ export default class AnamClient {
public async stream(
userProvidedAudioStream?: MediaStream,
): Promise<MediaStream[]> {
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');
Expand Down Expand Up @@ -261,10 +276,15 @@ export default class AnamClient {
public async streamToVideoAndAudioElements(
videoElementId: string,
audioElementId: string,
userProvidedMediaStream?: MediaStream,
userProvidedAudioStream?: MediaStream,
): Promise<void> {
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;
Expand Down Expand Up @@ -340,14 +360,24 @@ 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();
}
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 = {
Expand All @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions src/lib/ClientError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 37 additions & 34 deletions src/modules/StreamingClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/types/AnamPublicClientOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface AnamPublicClientOptions {
api?: CoreApiRestClientOptions;
voiceDetection?: VoiceDetectionOptions;
audioDeviceId?: string;
disableInputAudio?: boolean;
}
1 change: 1 addition & 0 deletions src/types/streaming/InputAudioOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface InputAudioOptions {
inputAudioState: InputAudioState;
userProvidedMediaStream?: MediaStream;
audioDeviceId?: string;
disableInputAudio?: boolean;
}