77import {
88 AnamEvent ,
99 InputAudioState ,
10+ AudioPermissionState ,
1011 InternalEvent ,
1112 SignalMessage ,
1213 SignalMessageAction ,
@@ -40,7 +41,10 @@ export class StreamingClient {
4041 private videoElement : HTMLVideoElement | null = null ;
4142 private videoStream : MediaStream | null = null ;
4243 private audioStream : MediaStream | null = null ;
43- private inputAudioState : InputAudioState = { isMuted : false } ;
44+ private inputAudioState : InputAudioState = {
45+ isMuted : false ,
46+ permissionState : AudioPermissionState . NOT_REQUESTED ,
47+ } ;
4448 private audioDeviceId : string | undefined ;
4549 private disableInputAudio : boolean ;
4650 private successMetricPoller : ReturnType < typeof setInterval > | null = null ;
@@ -307,7 +311,7 @@ export class StreamingClient {
307311 // start the connection
308312 this . signallingClient . connect ( ) ;
309313 } catch ( error ) {
310- console . log ( 'StreamingClient - startConnection: error' , error ) ;
314+ console . error ( 'StreamingClient - startConnection: error' , error ) ;
311315 this . handleWebrtcFailure ( error ) ;
312316 }
313317 }
@@ -363,6 +367,18 @@ export class StreamingClient {
363367 this . peerConnection . addTransceiver ( 'audio' , { direction : 'recvonly' } ) ;
364368 } else {
365369 this . peerConnection . addTransceiver ( 'audio' , { direction : 'sendrecv' } ) ;
370+
371+ // Handle audio setup after transceivers are configured
372+ if ( this . inputAudioStream ) {
373+ // User provided an audio stream, set it up immediately
374+ await this . setupAudioTrack ( ) ;
375+ } else {
376+ // No user stream, start microphone permission request asynchronously
377+ // Don't await - let it run in parallel with connection setup
378+ this . requestMicrophonePermissionAsync ( ) . catch ( ( error ) => {
379+ console . error ( 'Async microphone permission request failed:' , error ) ;
380+ } ) ;
381+ }
366382 }
367383 }
368384
@@ -392,7 +408,6 @@ export class StreamingClient {
392408 break ;
393409 case SignalMessageAction . END_SESSION :
394410 const reason = signalMessage . payload as string ;
395- console . log ( 'StreamingClient - onSignalMessage: reason' , reason ) ;
396411 this . publicEventEmitter . emit (
397412 AnamEvent . CONNECTION_CLOSED ,
398413 ConnectionClosedCode . SERVER_CLOSED_CONNECTION ,
@@ -548,48 +563,20 @@ export class StreamingClient {
548563 ) ;
549564 return ;
550565 }
566+
551567 /**
552- * Audio
568+ * Audio - Validate user-provided stream only
553569 *
554- * 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
555- * If input audio is disabled we don't send any audio to the peer connection
570+ * If the user provided an audio stream, validate it has audio tracks
571+ * Microphone permission request will be handled asynchronously
556572 */
557- if ( ! this . disableInputAudio ) {
558- if ( this . inputAudioStream ) {
559- // verify the user provided stream has audio tracks
560- if ( ! this . inputAudioStream . getAudioTracks ( ) . length ) {
561- throw new Error (
562- 'StreamingClient - setupDataChannels: user provided stream does not have audio tracks' ,
563- ) ;
564- }
565- } else {
566- const audioConstraints : MediaTrackConstraints = {
567- echoCancellation : true ,
568- } ;
569-
570- // If an audio device ID is provided in the options, use it
571- if ( this . audioDeviceId ) {
572- audioConstraints . deviceId = {
573- exact : this . audioDeviceId ,
574- } ;
575- }
576-
577- this . inputAudioStream = await navigator . mediaDevices . getUserMedia ( {
578- audio : audioConstraints ,
579- } ) ;
580- }
581-
582- // mute the audio tracks if the user has muted the microphone
583- if ( this . inputAudioState . isMuted ) {
584- this . muteAllAudioTracks ( ) ;
573+ if ( ! this . disableInputAudio && this . inputAudioStream ) {
574+ // verify the user provided stream has audio tracks
575+ if ( ! this . inputAudioStream . getAudioTracks ( ) . length ) {
576+ throw new Error (
577+ 'StreamingClient - setupDataChannels: user provided stream does not have audio tracks' ,
578+ ) ;
585579 }
586- const audioTrack = this . inputAudioStream . getAudioTracks ( ) [ 0 ] ;
587- this . peerConnection . addTrack ( audioTrack , this . inputAudioStream ) ;
588- // pass the stream to the callback if it exists
589- this . publicEventEmitter . emit (
590- AnamEvent . INPUT_AUDIO_STREAM_STARTED ,
591- this . inputAudioStream ,
592- ) ;
593580 }
594581
595582 /**
@@ -615,6 +602,114 @@ export class StreamingClient {
615602 } ;
616603 }
617604
605+ /**
606+ * Request microphone permission asynchronously without blocking connection
607+ */
608+ private async requestMicrophonePermissionAsync ( ) {
609+ if ( this . inputAudioState . permissionState === AudioPermissionState . PENDING ) {
610+ return ; // Already requesting
611+ }
612+
613+ this . inputAudioState = {
614+ ...this . inputAudioState ,
615+ permissionState : AudioPermissionState . PENDING ,
616+ } ;
617+
618+ this . publicEventEmitter . emit ( AnamEvent . MIC_PERMISSION_PENDING ) ;
619+
620+ try {
621+ const audioConstraints : MediaTrackConstraints = {
622+ echoCancellation : true ,
623+ } ;
624+
625+ // If an audio device ID is provided in the options, use it
626+ if ( this . audioDeviceId ) {
627+ audioConstraints . deviceId = {
628+ exact : this . audioDeviceId ,
629+ } ;
630+ }
631+
632+ this . inputAudioStream = await navigator . mediaDevices . getUserMedia ( {
633+ audio : audioConstraints ,
634+ } ) ;
635+
636+ this . inputAudioState = {
637+ ...this . inputAudioState ,
638+ permissionState : AudioPermissionState . GRANTED ,
639+ } ;
640+
641+ this . publicEventEmitter . emit ( AnamEvent . MIC_PERMISSION_GRANTED ) ;
642+
643+ // Now add the audio track to the existing connection
644+ await this . setupAudioTrack ( ) ;
645+ } catch ( error ) {
646+ console . error ( 'Failed to get microphone permission:' , error ) ;
647+ this . inputAudioState = {
648+ ...this . inputAudioState ,
649+ permissionState : AudioPermissionState . DENIED ,
650+ } ;
651+
652+ const errorMessage =
653+ error instanceof Error ? error . message : String ( error ) ;
654+ this . publicEventEmitter . emit (
655+ AnamEvent . MIC_PERMISSION_DENIED ,
656+ errorMessage ,
657+ ) ;
658+ }
659+ }
660+
661+ /**
662+ * Set up audio track and add it to the peer connection using replaceTrack
663+ */
664+ private async setupAudioTrack ( ) {
665+ if ( ! this . peerConnection || ! this . inputAudioStream ) {
666+ return ;
667+ }
668+
669+ // verify the stream has audio tracks
670+ if ( ! this . inputAudioStream . getAudioTracks ( ) . length ) {
671+ console . error (
672+ 'StreamingClient - setupAudioTrack: stream does not have audio tracks' ,
673+ ) ;
674+ return ;
675+ }
676+
677+ // mute the audio tracks if the user has muted the microphone
678+ if ( this . inputAudioState . isMuted ) {
679+ this . muteAllAudioTracks ( ) ;
680+ }
681+
682+ const audioTrack = this . inputAudioStream . getAudioTracks ( ) [ 0 ] ;
683+
684+ // Find the audio sender
685+ const existingSenders = this . peerConnection . getSenders ( ) ;
686+ const audioSender = existingSenders . find (
687+ ( sender ) =>
688+ sender . track ?. kind === 'audio' ||
689+ ( sender . track === null && sender . dtmf !== null ) , // audio sender without track
690+ ) ;
691+
692+ if ( audioSender ) {
693+ // Replace existing track (or null track) with our audio track
694+ try {
695+ await audioSender . replaceTrack ( audioTrack ) ;
696+ } catch ( error ) {
697+ console . error ( 'Failed to replace audio track:' , error ) ;
698+ // Fallback: add track normally
699+ this . peerConnection . addTrack ( audioTrack , this . inputAudioStream ) ;
700+ }
701+ } else {
702+ // No audio sender found, add track normally
703+ this . peerConnection . addTrack ( audioTrack , this . inputAudioStream ) ;
704+ }
705+
706+ // pass the stream to the callback
707+ this . publicEventEmitter . emit (
708+ AnamEvent . INPUT_AUDIO_STREAM_STARTED ,
709+ this . inputAudioStream ,
710+ ) ;
711+ }
712+
618713 private async initPeerConnectionAndSendOffer ( ) {
619714 await this . initPeerConnection ( ) ;
620715
0 commit comments