@@ -130,6 +130,20 @@ export class DailyTransport extends Transport {
130130 private _audioQueue : ArrayBuffer [ ] = [ ] ;
131131 declare private _mediaStreamRecorder : MediaStreamRecorder ;
132132
133+ // NOTE: deliberately not read live off `this._daily.localAudio()` /
134+ // `localVideo()` in the isMicEnabled/isCamEnabled getters below. Daily
135+ // fires 'track-started'/'track-stopped' *before* it commits the new state
136+ // to the participant object those calls read from (daily-js emits the
137+ // track event from within its participant-updated handling, then
138+ // reassigns its internal participant cache afterwards). Code that reacts
139+ // to TrackStarted/TrackStopped by reading isMicEnabled/isCamEnabled to
140+ // resync UI state — e.g. PipecatClientState's mic/cam sync — would
141+ // otherwise always observe the pre-toggle value. These fields are updated
142+ // synchronously in enableMic/enableCam and in handleTrackStarted/
143+ // handleTrackStopped, before those callbacks fire.
144+ private _micEnabled : boolean = true ;
145+ private _camEnabled : boolean = false ;
146+
133147 constructor ( opts : DailyTransportConstructorOptions = { } ) {
134148 super ( ) ;
135149
@@ -236,6 +250,8 @@ export class DailyTransport extends Transport {
236250 // Default is mic on
237251 this . _dailyFactoryOptions . startAudioOff = ! ( options . enableMic ?? true ) ;
238252 }
253+ this . _camEnabled = ! this . _dailyFactoryOptions . startVideoOff ;
254+ this . _micEnabled = ! this . _dailyFactoryOptions . startAudioOff ;
239255
240256 this . attachEventListeners ( ) ;
241257
@@ -330,24 +346,26 @@ export class DailyTransport extends Transport {
330346 // joined — pre-session, daily-js can't act on the request because the
331347 // call object isn't part of a meeting yet.
332348 this . _dailyFactoryOptions . startAudioOff = ! enable ;
349+ this . _micEnabled = enable ;
333350 if ( this . _daily . participants ( ) ?. local ) {
334351 this . _daily . setLocalAudio ( enable ) ;
335352 }
336353 }
337354
338355 get isMicEnabled ( ) {
339- return this . _daily . localAudio ( ) ;
356+ return this . _micEnabled ;
340357 }
341358
342359 enableCam ( enable : boolean ) {
343360 this . _dailyFactoryOptions . startVideoOff = ! enable ;
361+ this . _camEnabled = enable ;
344362 if ( this . _daily . participants ( ) ?. local ) {
345363 this . _daily . setLocalVideo ( enable ) ;
346364 }
347365 }
348366
349367 get isCamEnabled ( ) {
350- return this . _daily . localVideo ( ) ;
368+ return this . _camEnabled ;
351369 }
352370
353371 public enableScreenShare ( enable : boolean ) {
@@ -719,7 +737,22 @@ export class DailyTransport extends Transport {
719737 }
720738 }
721739 } ;
722- this . _callbacks . onDeviceError ?.( generateDeviceError ( ev . error ) ) ;
740+ const deviceError = generateDeviceError ( ev . error ) ;
741+ // enableMic()/enableCam() set _micEnabled/_camEnabled optimistically,
742+ // but setLocalAudio()/setLocalVideo() are fire-and-forget — no promise,
743+ // no synchronous failure signal — so a failed request (blocked
744+ // permission, device in use, etc.) leaves that optimistic value wrong
745+ // with nothing to correct it: no track-started/stopped ever fires for
746+ // an operation that didn't actually take effect. This is the only
747+ // signal we get that the request failed, so resync from Daily's actual
748+ // state for whichever device(s) it implicates.
749+ if ( deviceError . devices . includes ( "mic" ) ) {
750+ this . _micEnabled = this . _daily . localAudio ( ) ;
751+ }
752+ if ( deviceError . devices . includes ( "cam" ) ) {
753+ this . _camEnabled = this . _daily . localVideo ( ) ;
754+ }
755+ this . _callbacks . onDeviceError ?.( deviceError ) ;
723756 }
724757
725758 private async handleLocalAudioTrack ( track : MediaStreamTrack ) {
@@ -768,8 +801,13 @@ export class DailyTransport extends Transport {
768801 : undefined
769802 ) ;
770803 } else {
771- if ( ev . participant ?. local && ev . track . kind === "audio" ) {
772- void this . handleLocalAudioTrack ( ev . track ) ;
804+ if ( ev . participant ?. local ) {
805+ if ( ev . track . kind === "audio" ) {
806+ this . _micEnabled = true ;
807+ void this . handleLocalAudioTrack ( ev . track ) ;
808+ } else if ( ev . track . kind === "video" ) {
809+ this . _camEnabled = true ;
810+ }
773811 }
774812 this . _callbacks . onTrackStarted ?.(
775813 ev . track ,
@@ -789,6 +827,13 @@ export class DailyTransport extends Transport {
789827 : undefined
790828 ) ;
791829 } else {
830+ if ( ev . participant ?. local ) {
831+ if ( ev . track . kind === "audio" ) {
832+ this . _micEnabled = false ;
833+ } else if ( ev . track . kind === "video" ) {
834+ this . _camEnabled = false ;
835+ }
836+ }
792837 this . _callbacks . onTrackStopped ?.(
793838 ev . track ,
794839 ev . participant
0 commit comments