@@ -6,7 +6,6 @@ Please see LICENSE in the repository root for full details.
6
6
*/
7
7
8
8
import {
9
- type AudioCaptureOptions ,
10
9
ConnectionError ,
11
10
ConnectionState ,
12
11
type LocalTrack ,
@@ -25,6 +24,7 @@ import {
25
24
InsufficientCapacityError ,
26
25
UnknownCallError ,
27
26
} from "../utils/errors.ts" ;
27
+ import { AbortHandle } from "../utils/abortHandle.ts" ;
28
28
29
29
declare global {
30
30
interface Window {
@@ -59,7 +59,8 @@ async function doConnect(
59
59
livekitRoom : Room ,
60
60
sfuConfig : SFUConfig ,
61
61
audioEnabled : boolean ,
62
- audioOptions : AudioCaptureOptions ,
62
+ initialDeviceId : string | undefined ,
63
+ abortHandle : AbortHandle ,
63
64
) : Promise < void > {
64
65
// Always create an audio track manually.
65
66
// livekit (by default) keeps the mic track open when you mute, but if you start muted,
@@ -82,19 +83,40 @@ async function doConnect(
82
83
let preCreatedAudioTrack : LocalTrack | undefined ;
83
84
try {
84
85
const audioTracks = await livekitRoom ! . localParticipant . createTracks ( {
85
- audio : audioOptions ,
86
+ audio : { deviceId : initialDeviceId } ,
86
87
} ) ;
88
+
87
89
if ( audioTracks . length < 1 ) {
88
90
logger . info ( "Tried to pre-create local audio track but got no tracks" ) ;
89
91
} else {
90
92
preCreatedAudioTrack = audioTracks [ 0 ] ;
91
93
}
94
+ // There was a yield point previously (awaiting for the track to be created) so we need to check
95
+ // if the operation was cancelled and stop connecting if needed.
96
+ if ( abortHandle . isAborted ( ) ) {
97
+ logger . info (
98
+ "[Lifecycle] Signal Aborted: Pre-created audio track but connection aborted" ,
99
+ ) ;
100
+ preCreatedAudioTrack ?. stop ( ) ;
101
+ return ;
102
+ }
103
+
92
104
logger . info ( "Pre-created microphone track" ) ;
93
105
} catch ( e ) {
94
106
logger . error ( "Failed to pre-create microphone track" , e ) ;
95
107
}
96
108
97
- if ( ! audioEnabled ) await preCreatedAudioTrack ?. mute ( ) ;
109
+ if ( ! audioEnabled ) {
110
+ await preCreatedAudioTrack ?. mute ( ) ;
111
+ // There was a yield point. Check if the operation was cancelled and stop connecting.
112
+ if ( abortHandle . isAborted ( ) ) {
113
+ logger . info (
114
+ "[Lifecycle] Signal Aborted: Pre-created audio track but connection aborted" ,
115
+ ) ;
116
+ preCreatedAudioTrack ?. stop ( ) ;
117
+ return ;
118
+ }
119
+ }
98
120
99
121
// check again having awaited for the track to create
100
122
if (
@@ -107,9 +129,18 @@ async function doConnect(
107
129
return ;
108
130
}
109
131
110
- logger . info ( "Connecting & publishing" ) ;
132
+ logger . info ( "[Lifecycle] Connecting & publishing" ) ;
111
133
try {
112
134
await connectAndPublish ( livekitRoom , sfuConfig , preCreatedAudioTrack , [ ] ) ;
135
+ if ( abortHandle . isAborted ( ) ) {
136
+ logger . info (
137
+ "[Lifecycle] Signal Aborted: Connected but operation was cancelled. Force disconnect" ,
138
+ ) ;
139
+ livekitRoom ?. disconnect ( ) . catch ( ( err ) => {
140
+ logger . error ( "Failed to disconnect from SFU" , err ) ;
141
+ } ) ;
142
+ return ;
143
+ }
113
144
} catch ( e ) {
114
145
preCreatedAudioTrack ?. stop ( ) ;
115
146
logger . debug ( "Stopped precreated audio tracks." ) ;
@@ -137,13 +168,16 @@ async function connectAndPublish(
137
168
livekitRoom . once ( RoomEvent . SignalConnected , tracker . cacheWsConnect ) ;
138
169
139
170
try {
171
+ logger . info ( `[Lifecycle] Connecting to livekit room ${ sfuConfig ! . url } ...` ) ;
140
172
await livekitRoom ! . connect ( sfuConfig ! . url , sfuConfig ! . jwt , {
141
173
// Due to stability issues on Firefox we are testing the effect of different
142
174
// timeouts, and allow these values to be set through the console
143
175
peerConnectionTimeout : window . peerConnectionTimeout ?? 45000 ,
144
176
websocketTimeout : window . websocketTimeout ?? 45000 ,
145
177
} ) ;
178
+ logger . info ( `[Lifecycle] ... connected to livekit room` ) ;
146
179
} catch ( e ) {
180
+ logger . error ( "[Lifecycle] Failed to connect" , e ) ;
147
181
// LiveKit uses 503 to indicate that the server has hit its track limits.
148
182
// https://github.com/livekit/livekit/blob/fcb05e97c5a31812ecf0ca6f7efa57c485cea9fb/pkg/service/rtcservice.go#L171
149
183
// It also errors with a status code of 200 (yes, really) for room
@@ -184,7 +218,7 @@ async function connectAndPublish(
184
218
}
185
219
186
220
export function useECConnectionState (
187
- initialAudioOptions : AudioCaptureOptions ,
221
+ initialDeviceId : string | undefined ,
188
222
initialAudioEnabled : boolean ,
189
223
livekitRoom ?: Room ,
190
224
sfuConfig ?: SFUConfig ,
@@ -247,6 +281,22 @@ export function useECConnectionState(
247
281
248
282
const currentSFUConfig = useRef ( Object . assign ( { } , sfuConfig ) ) ;
249
283
284
+ // Protection against potential leaks, where the component to be unmounted and there is
285
+ // still a pending doConnect promise. This would lead the user to still be in the call even
286
+ // if the component is unmounted.
287
+ const abortHandlesBag = useRef ( new Set < AbortHandle > ( ) ) ;
288
+
289
+ // This is a cleanup function that will be called when the component is about to be unmounted.
290
+ // It will cancel all abortHandles in the bag
291
+ useEffect ( ( ) => {
292
+ const bag = abortHandlesBag . current ;
293
+ return ( ) : void => {
294
+ bag . forEach ( ( handle ) => {
295
+ handle . abort ( ) ;
296
+ } ) ;
297
+ } ;
298
+ } , [ ] ) ;
299
+
250
300
// Id we are transitioning from a valid config to another valid one, we need
251
301
// to explicitly switch focus
252
302
useEffect ( ( ) => {
@@ -273,11 +323,14 @@ export function useECConnectionState(
273
323
// always capturing audio: it helps keep bluetooth headsets in the right mode and
274
324
// mobile browsers to know we're doing a call.
275
325
setIsInDoConnect ( true ) ;
326
+ const abortHandle = new AbortHandle ( ) ;
327
+ abortHandlesBag . current . add ( abortHandle ) ;
276
328
doConnect (
277
329
livekitRoom ! ,
278
330
sfuConfig ! ,
279
331
initialAudioEnabled ,
280
- initialAudioOptions ,
332
+ initialDeviceId ,
333
+ abortHandle ,
281
334
)
282
335
. catch ( ( e ) => {
283
336
if ( e instanceof ElementCallError ) {
@@ -286,14 +339,17 @@ export function useECConnectionState(
286
339
setError ( new UnknownCallError ( e ) ) ;
287
340
} else logger . error ( "Failed to connect to SFU" , e ) ;
288
341
} )
289
- . finally ( ( ) => setIsInDoConnect ( false ) ) ;
342
+ . finally ( ( ) => {
343
+ abortHandlesBag . current . delete ( abortHandle ) ;
344
+ setIsInDoConnect ( false ) ;
345
+ } ) ;
290
346
}
291
347
292
348
currentSFUConfig . current = Object . assign ( { } , sfuConfig ) ;
293
349
} , [
294
350
sfuConfig ,
295
351
livekitRoom ,
296
- initialAudioOptions ,
352
+ initialDeviceId ,
297
353
initialAudioEnabled ,
298
354
doFocusSwitch ,
299
355
] ) ;
0 commit comments