@ethora/chat-component-rn 26.10.4 → 26.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/lib/commonjs/components/VideoCalls/ActiveCallBar.js +61 -16
  2. package/lib/commonjs/components/VideoCalls/ActiveCallBar.js.map +1 -1
  3. package/lib/commonjs/components/VideoCalls/CallIcons.js +41 -16
  4. package/lib/commonjs/components/VideoCalls/CallIcons.js.map +1 -1
  5. package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js +183 -9
  6. package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js.map +1 -1
  7. package/lib/commonjs/components/VideoCalls/VideoCallSession.js +40 -15
  8. package/lib/commonjs/components/VideoCalls/VideoCallSession.js.map +1 -1
  9. package/lib/commonjs/components/VideoCalls/callSessionController.js +49 -26
  10. package/lib/commonjs/components/VideoCalls/callSessionController.js.map +1 -1
  11. package/lib/commonjs/components/VideoCalls/callUiStore.js +33 -4
  12. package/lib/commonjs/components/VideoCalls/callUiStore.js.map +1 -1
  13. package/lib/commonjs/components/VideoCalls/livekitRuntime.js +9 -1
  14. package/lib/commonjs/components/VideoCalls/livekitRuntime.js.map +1 -1
  15. package/lib/commonjs/components/VideoCalls/useCallKeep.js +197 -28
  16. package/lib/commonjs/components/VideoCalls/useCallKeep.js.map +1 -1
  17. package/lib/commonjs/components/VideoCalls/useOngoingCallNotification.js +88 -0
  18. package/lib/commonjs/components/VideoCalls/useOngoingCallNotification.js.map +1 -0
  19. package/lib/commonjs/components/VideoCalls/usePipTile.js +232 -0
  20. package/lib/commonjs/components/VideoCalls/usePipTile.js.map +1 -0
  21. package/lib/commonjs/helpers/updateMessagesTillLast.js +3 -1
  22. package/lib/commonjs/helpers/updateMessagesTillLast.js.map +1 -1
  23. package/lib/commonjs/i18n/strings.js +48 -0
  24. package/lib/commonjs/i18n/strings.js.map +1 -1
  25. package/lib/commonjs/main.js.map +1 -1
  26. package/lib/commonjs/version.js +1 -1
  27. package/lib/module/components/VideoCalls/ActiveCallBar.js +63 -18
  28. package/lib/module/components/VideoCalls/ActiveCallBar.js.map +1 -1
  29. package/lib/module/components/VideoCalls/CallIcons.js +38 -15
  30. package/lib/module/components/VideoCalls/CallIcons.js.map +1 -1
  31. package/lib/module/components/VideoCalls/VideoCallOverlay.js +185 -11
  32. package/lib/module/components/VideoCalls/VideoCallOverlay.js.map +1 -1
  33. package/lib/module/components/VideoCalls/VideoCallSession.js +41 -16
  34. package/lib/module/components/VideoCalls/VideoCallSession.js.map +1 -1
  35. package/lib/module/components/VideoCalls/callSessionController.js +46 -24
  36. package/lib/module/components/VideoCalls/callSessionController.js.map +1 -1
  37. package/lib/module/components/VideoCalls/callUiStore.js +30 -3
  38. package/lib/module/components/VideoCalls/callUiStore.js.map +1 -1
  39. package/lib/module/components/VideoCalls/livekitRuntime.js +9 -1
  40. package/lib/module/components/VideoCalls/livekitRuntime.js.map +1 -1
  41. package/lib/module/components/VideoCalls/useCallKeep.js +198 -29
  42. package/lib/module/components/VideoCalls/useCallKeep.js.map +1 -1
  43. package/lib/module/components/VideoCalls/useOngoingCallNotification.js +84 -0
  44. package/lib/module/components/VideoCalls/useOngoingCallNotification.js.map +1 -0
  45. package/lib/module/components/VideoCalls/usePipTile.js +228 -0
  46. package/lib/module/components/VideoCalls/usePipTile.js.map +1 -0
  47. package/lib/module/helpers/updateMessagesTillLast.js +3 -1
  48. package/lib/module/helpers/updateMessagesTillLast.js.map +1 -1
  49. package/lib/module/i18n/strings.js +48 -0
  50. package/lib/module/i18n/strings.js.map +1 -1
  51. package/lib/module/main.js.map +1 -1
  52. package/lib/module/version.js +1 -1
  53. package/lib/typescript/components/VideoCalls/ActiveCallBar.d.ts +9 -3
  54. package/lib/typescript/components/VideoCalls/ActiveCallBar.d.ts.map +1 -1
  55. package/lib/typescript/components/VideoCalls/CallIcons.d.ts +9 -0
  56. package/lib/typescript/components/VideoCalls/CallIcons.d.ts.map +1 -1
  57. package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts.map +1 -1
  58. package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -1
  59. package/lib/typescript/components/VideoCalls/callSessionController.d.ts +7 -0
  60. package/lib/typescript/components/VideoCalls/callSessionController.d.ts.map +1 -1
  61. package/lib/typescript/components/VideoCalls/callUiStore.d.ts +13 -0
  62. package/lib/typescript/components/VideoCalls/callUiStore.d.ts.map +1 -1
  63. package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts +5 -0
  64. package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts.map +1 -1
  65. package/lib/typescript/components/VideoCalls/useCallKeep.d.ts +12 -0
  66. package/lib/typescript/components/VideoCalls/useCallKeep.d.ts.map +1 -1
  67. package/lib/typescript/components/VideoCalls/useOngoingCallNotification.d.ts +10 -0
  68. package/lib/typescript/components/VideoCalls/useOngoingCallNotification.d.ts.map +1 -0
  69. package/lib/typescript/components/VideoCalls/usePipTile.d.ts +46 -0
  70. package/lib/typescript/components/VideoCalls/usePipTile.d.ts.map +1 -0
  71. package/lib/typescript/helpers/updateMessagesTillLast.d.ts.map +1 -1
  72. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  73. package/lib/typescript/main.d.ts +1 -1
  74. package/lib/typescript/main.d.ts.map +1 -1
  75. package/lib/typescript/types/types.d.ts +40 -0
  76. package/lib/typescript/types/types.d.ts.map +1 -1
  77. package/lib/typescript/version.d.ts +1 -1
  78. package/package.json +1 -1
  79. package/src/components/VideoCalls/ActiveCallBar.tsx +56 -10
  80. package/src/components/VideoCalls/CallIcons.tsx +36 -12
  81. package/src/components/VideoCalls/VideoCallOverlay.tsx +205 -8
  82. package/src/components/VideoCalls/VideoCallSession.tsx +45 -13
  83. package/src/components/VideoCalls/callSessionController.ts +51 -20
  84. package/src/components/VideoCalls/callUiStore.ts +33 -3
  85. package/src/components/VideoCalls/livekitRuntime.ts +13 -0
  86. package/src/components/VideoCalls/useCallKeep.ts +204 -33
  87. package/src/components/VideoCalls/useOngoingCallNotification.ts +113 -0
  88. package/src/components/VideoCalls/usePipTile.ts +214 -0
  89. package/src/helpers/updateMessagesTillLast.tsx +2 -1
  90. package/src/i18n/strings.ts +48 -0
  91. package/src/main.ts +3 -0
  92. package/src/types/types.ts +48 -0
  93. package/src/version.ts +1 -1
  94. package/lib/commonjs/components/VideoCalls/useDraggable.js +0 -59
  95. package/lib/commonjs/components/VideoCalls/useDraggable.js.map +0 -1
  96. package/lib/module/components/VideoCalls/useDraggable.js +0 -55
  97. package/lib/module/components/VideoCalls/useDraggable.js.map +0 -1
  98. package/lib/typescript/components/VideoCalls/useDraggable.d.ts +0 -19
  99. package/lib/typescript/components/VideoCalls/useDraggable.d.ts.map +0 -1
  100. package/src/components/VideoCalls/useDraggable.ts +0 -46
@@ -14,6 +14,8 @@ import { ProfileImagePlaceholder } from '../MainComponents/ProfileImagePlacehold
14
14
  import {
15
15
  CameraOffIcon,
16
16
  CameraOnIcon,
17
+ FillVideoIcon,
18
+ FitVideoIcon,
17
19
  HangUpIcon,
18
20
  MicOffIcon,
19
21
  MicOnIcon,
@@ -31,7 +33,7 @@ import {
31
33
  toggleCallSpeaker,
32
34
  useCallSession,
33
35
  } from './callSessionController';
34
- import { useDraggable } from './useDraggable';
36
+ import { usePipTile } from './usePipTile';
35
37
  import { useT } from '../../i18n/useT';
36
38
 
37
39
  const DEFAULT_PRIMARY = '#0052CD';
@@ -99,10 +101,14 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
99
101
  const insets = useSafeAreaInsets();
100
102
  const runtime = loadLiveKit();
101
103
  const session = useCallSession();
102
- const selfView = useDraggable();
104
+ const pipTile = usePipTile({ topInset: insets.top });
103
105
  const elapsed = useSharedCallElapsed(session.anchorAt);
104
106
  // Telegram-style: tap the small tile to swap it with the big one.
105
107
  const [swapped, setSwapped] = useState(false);
108
+ // The peer's video is shown whole by default: a web camera is landscape,
109
+ // and filling a portrait phone crops most of it away. The button next to
110
+ // minimize stretches it to fill (the old behaviour) and back.
111
+ const [fillRemote, setFillRemote] = useState(false);
106
112
 
107
113
  if (!runtime) {
108
114
  return (
@@ -124,6 +130,7 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
124
130
  deviceError,
125
131
  remoteMicMuted,
126
132
  facingMode,
133
+ localVideoEpoch,
127
134
  } = session;
128
135
  // The call slice's name (the chat title) first, as web does: LiveKit's
129
136
  // participant name is often just the opaque xmpp identity.
@@ -137,8 +144,14 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
137
144
  const isSwapped = swapped && canSwap;
138
145
  const mainVideo = isSwapped ? localVideo : showRemoteVideo ? remoteVideo : null;
139
146
  const pipVideo = isSwapped ? remoteVideo : localLive ? localVideo : null;
147
+ const mainIsRemote = !!mainVideo && mainVideo === remoteVideo;
148
+ const mainFit = mainIsRemote && !fillRemote ? 'contain' : 'cover';
140
149
  // Only the front camera is mirrored, as in every camera app.
141
150
  const selfMirror = facingMode === 'user';
151
+ // The local view remounts when the camera is re-acquired (switching
152
+ // front/back), so it never keeps rendering the stopped stream.
153
+ const videoKey = (ref: any) =>
154
+ ref === localVideo ? `local-${localVideoEpoch}` : 'remote';
142
155
 
143
156
  const statusText =
144
157
  elapsed !== null
@@ -154,12 +167,13 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
154
167
  return (
155
168
  <View style={styles.canvas}>
156
169
  {/* Stage */}
157
- <View style={styles.stage}>
170
+ <View style={styles.stage} onLayout={pipTile.onStageLayout}>
158
171
  {mainVideo ? (
159
172
  <VideoTrack
173
+ key={videoKey(mainVideo)}
160
174
  trackRef={mainVideo}
161
175
  style={styles.remoteVideo}
162
- objectFit="cover"
176
+ objectFit={mainFit}
163
177
  mirror={isSwapped && selfMirror}
164
178
  zOrder={0}
165
179
  />
@@ -204,12 +218,12 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
204
218
  </View>
205
219
  )}
206
220
 
207
- {/* Picture-in-picture: draggable like web's self-view, tap to swap
208
- it with the main video. */}
221
+ {/* Picture-in-picture: drag it and it snaps to a corner, pinch it
222
+ for the large size, tap it to swap it with the main video. */}
209
223
  {pipVideo && (
210
224
  <Animated.View
211
- {...selfView.panHandlers}
212
- style={[styles.pip, { top: 16 + insets.top }, selfView.style]}
225
+ {...pipTile.panHandlers}
226
+ style={[styles.pip, pipTile.style]}
213
227
  >
214
228
  <Pressable
215
229
  style={styles.pipVideo}
@@ -219,6 +233,7 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
219
233
  accessibilityLabel={t('call.action.swapVideo')}
220
234
  >
221
235
  <VideoTrack
236
+ key={videoKey(pipVideo)}
222
237
  trackRef={pipVideo}
223
238
  style={styles.pipVideo}
224
239
  objectFit="cover"
@@ -240,6 +255,23 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
240
255
  </TouchableOpacity>
241
256
  )}
242
257
 
258
+ {mainIsRemote && (
259
+ <TouchableOpacity
260
+ accessibilityRole="button"
261
+ accessibilityLabel={
262
+ fillRemote ? t('call.action.fitVideo') : t('call.action.fillVideo')
263
+ }
264
+ onPress={() => setFillRemote((value) => !value)}
265
+ style={[styles.minimizeButton, styles.fitButton, { top: 16 + insets.top }]}
266
+ >
267
+ {fillRemote ? (
268
+ <FitVideoIcon color={TEXT_ON_DARK} size={20} />
269
+ ) : (
270
+ <FillVideoIcon color={TEXT_ON_DARK} size={20} />
271
+ )}
272
+ </TouchableOpacity>
273
+ )}
274
+
243
275
  {!!deviceError && (
244
276
  <TouchableOpacity
245
277
  style={[styles.deviceErrorBanner, { top: 64 + insets.top }]}
@@ -354,7 +386,7 @@ const styles = StyleSheet.create({
354
386
  top: 0,
355
387
  left: 0,
356
388
  right: 0,
357
- paddingHorizontal: 64,
389
+ paddingHorizontal: 108,
358
390
  paddingBottom: 14,
359
391
  alignItems: 'center',
360
392
  backgroundColor: 'rgba(0,0,0,0.35)',
@@ -397,10 +429,6 @@ const styles = StyleSheet.create({
397
429
  fontSize: 12,
398
430
  },
399
431
  pip: {
400
- position: 'absolute',
401
- right: 16,
402
- width: 104,
403
- height: 148,
404
432
  borderRadius: 12,
405
433
  overflow: 'hidden',
406
434
  backgroundColor: '#000',
@@ -422,6 +450,10 @@ const styles = StyleSheet.create({
422
450
  backgroundColor: 'rgba(0,0,0,0.45)',
423
451
  zIndex: 6,
424
452
  },
453
+ // Next to minimize, top-left: the top-right corner belongs to the tile.
454
+ fitButton: {
455
+ left: 60,
456
+ },
425
457
  controls: {
426
458
  flexDirection: 'row',
427
459
  alignItems: 'center',
@@ -33,6 +33,11 @@ export interface CallSessionSnapshot {
33
33
  speakerOn: boolean;
34
34
  /** Which camera the local video comes from; only the front one is mirrored. */
35
35
  facingMode: 'user' | 'environment';
36
+ /**
37
+ * Bumped whenever the local camera track is re-acquired, so the self view
38
+ * can remount onto the new stream instead of holding the stopped one.
39
+ */
40
+ localVideoEpoch: number;
36
41
  /** `{ participant, publication, source }` for `<VideoTrack />`, live and unmuted only. */
37
42
  remoteVideo: any;
38
43
  localVideo: any;
@@ -56,6 +61,7 @@ const IDLE: CallSessionSnapshot = {
56
61
  cameraOn: false,
57
62
  speakerOn: false,
58
63
  facingMode: 'user',
64
+ localVideoEpoch: 0,
59
65
  remoteVideo: null,
60
66
  localVideo: null,
61
67
  remoteName: null,
@@ -299,7 +305,14 @@ export const startCallSession = ({
299
305
  // Claims audio focus and sets the right native audio mode. Must
300
306
  // start BEFORE connecting or the first seconds route to the earpiece
301
307
  // on Android even for a video call.
302
- await AudioSession.startAudioSession();
308
+ try {
309
+ await AudioSession.startAudioSession();
310
+ } catch {
311
+ // A call answered from the CallKit banner: CallKit owns the iOS
312
+ // audio session and activates it itself (useCallKeep forwards that
313
+ // to WebRTC), so our own activation is refused. Not a reason to
314
+ // fail the call.
315
+ }
303
316
  await room.connect(livekitUrl, token);
304
317
  } catch (error: any) {
305
318
  if (current.disposed) {return;}
@@ -350,10 +363,12 @@ export const startCallSession = ({
350
363
 
351
364
  // ---- controls -----------------------------------------------------------
352
365
 
353
- export const toggleCallMic = async (): Promise<void> => {
366
+ export const toggleCallMic = (): Promise<void> => setCallMicEnabled(!snapshot.micOn);
367
+
368
+ /** Set the mic to a given state (system call UI reports absolute values). */
369
+ export const setCallMicEnabled = async (next: boolean): Promise<void> => {
354
370
  const current = session;
355
- if (!current) {return;}
356
- const next = !snapshot.micOn;
371
+ if (!current || snapshot.micOn === next) {return;}
357
372
  emit({ micOn: next });
358
373
  try {
359
374
  await current.room.localParticipant.setMicrophoneEnabled(next);
@@ -379,29 +394,45 @@ export const toggleCallCamera = async (): Promise<void> => {
379
394
  }
380
395
  };
381
396
 
397
+ let switchingCamera = false;
398
+
382
399
  export const switchCallCamera = async (): Promise<void> => {
383
400
  const current = session;
384
401
  const runtime = loadLiveKit();
385
- if (!current || !runtime) {return;}
386
- const mediaTrack = current.room.localParticipant.getTrackPublication?.(
402
+ if (!current || !runtime || switchingCamera) {return;}
403
+ const track = current.room.localParticipant.getTrackPublication?.(
387
404
  runtime.Track.Source.Camera
388
- )?.track?.mediaStreamTrack;
389
- if (!mediaTrack?.applyConstraints) {return;}
405
+ )?.track;
406
+ if (!track?.restartTrack) {return;}
390
407
  const next = snapshot.facingMode === 'user' ? 'environment' : 'user';
408
+ switchingCamera = true;
391
409
  try {
392
- // Swap the capture device inside the SAME track (react-native-webrtc's
393
- // own camera switch). `restartTrack` stopped the published track and
394
- // made a new one, and the native view kept showing the stopped stream:
395
- // the picture froze on the switch.
396
- // Keep the current capture settings (resolution, frame rate) and only
397
- // flip the camera, exactly like the library's `_switchCamera`.
398
- const constraints = { ...(mediaTrack.getSettings?.() || {}) };
399
- delete constraints.deviceId;
400
- await mediaTrack.applyConstraints({ ...constraints, facingMode: next });
401
- if (!current.disposed) {emit({ facingMode: next });}
410
+ // A real device change needs a new capture: react-native-webrtc's
411
+ // native applyConstraints only applies width/height/frameRate and
412
+ // ignores facingMode on both platforms, so the old approach kept the
413
+ // front camera and merely flipped our mirror flag (the picture looked
414
+ // swapped left to right). restartTrack re-acquires the camera with
415
+ // the new facing mode and swaps it into the published sender; the
416
+ // <VideoTrack /> view follows it through TrackEvent.Restarted.
417
+ await track.restartTrack({
418
+ facingMode: next,
419
+ ...(runtime.VideoPresets
420
+ ? { resolution: runtime.VideoPresets.h720.resolution }
421
+ : {}),
422
+ });
423
+ if (current.disposed) {return;}
424
+ // Trust what the camera reports: a device with a single camera stays
425
+ // on it, and the mirror must follow the real camera, not the request.
426
+ const actual = track.mediaStreamTrack?.getSettings?.()?.facingMode;
427
+ emit({
428
+ facingMode: actual === 'user' || actual === 'environment' ? actual : next,
429
+ localVideoEpoch: snapshot.localVideoEpoch + 1,
430
+ });
402
431
  resync();
403
- } catch {
404
- // Single camera, or the switch is still in flight.
432
+ } catch (error) {
433
+ if (!current.disposed) {emit({ deviceError: humanizeDeviceError(error) });}
434
+ } finally {
435
+ switchingCamera = false;
405
436
  }
406
437
  };
407
438
 
@@ -16,11 +16,26 @@ import { store } from '../../roomStore';
16
16
 
17
17
  interface CallUiState {
18
18
  minimized: boolean;
19
+ /**
20
+ * iOS shows this incoming call in its own CallKit UI (banner / full
21
+ * screen), so the in-app ring card stays hidden instead of doubling it.
22
+ */
23
+ systemRinging: boolean;
19
24
  /** Id of the header that shows the strip, or null when none is mounted. */
20
25
  barHost: number | null;
26
+ /**
27
+ * Window y of the top of the strip slot under the hosting header, so the
28
+ * collapse animation can land exactly on the strip. Null: no header.
29
+ */
30
+ barAnchorY: number | null;
21
31
  }
22
32
 
23
- let state: CallUiState = { minimized: false, barHost: null };
33
+ let state: CallUiState = {
34
+ minimized: false,
35
+ systemRinging: false,
36
+ barHost: null,
37
+ barAnchorY: null,
38
+ };
24
39
  const hosts: number[] = [];
25
40
  let nextHostId = 1;
26
41
  const listeners = new Set<() => void>();
@@ -35,9 +50,13 @@ const emit = (patch: Partial<CallUiState>) => {
35
50
  const ensureStoreWatcher = () => {
36
51
  if (storeUnsubscribe) {return;}
37
52
  storeUnsubscribe = store.subscribe(() => {
38
- if (state.minimized && store.getState().call.phase === 'idle') {
53
+ const phase = store.getState().call.phase;
54
+ if (state.minimized && phase === 'idle') {
39
55
  emit({ minimized: false });
40
56
  }
57
+ if (state.systemRinging && phase !== 'ringing-incoming') {
58
+ emit({ systemRinging: false });
59
+ }
41
60
  });
42
61
  };
43
62
 
@@ -61,6 +80,10 @@ export const setCallMinimized = (minimized: boolean): void => {
61
80
  if (state.minimized !== minimized) {emit({ minimized });}
62
81
  };
63
82
 
83
+ export const setCallSystemRinging = (systemRinging: boolean): void => {
84
+ if (state.systemRinging !== systemRinging) {emit({ systemRinging });}
85
+ };
86
+
64
87
  export const registerCallBarHost = (
65
88
  onId?: (id: number) => void
66
89
  ): (() => void) => {
@@ -71,6 +94,13 @@ export const registerCallBarHost = (
71
94
  return () => {
72
95
  const index = hosts.indexOf(id);
73
96
  if (index >= 0) {hosts.splice(index, 1);}
74
- emit({ barHost: hosts.length ? hosts[hosts.length - 1] : null });
97
+ const next = hosts.length ? hosts[hosts.length - 1] : null;
98
+ emit({ barHost: next, barAnchorY: next === null ? null : state.barAnchorY });
75
99
  };
76
100
  };
101
+
102
+ /** The hosting header reports where its strip slot is on screen. */
103
+ export const setCallBarAnchor = (hostId: number, y: number): void => {
104
+ if (state.barHost !== hostId || state.barAnchorY === y) {return;}
105
+ emit({ barAnchorY: y });
106
+ };
@@ -34,6 +34,11 @@ export interface LiveKitRuntime {
34
34
  AudioSession: any;
35
35
  /** Installs the WebRTC globals livekit-client expects. */
36
36
  registerGlobals: () => void;
37
+ /**
38
+ * `@livekit/react-native-webrtc` RTCAudioSession: tells WebRTC when
39
+ * CallKit activates/deactivates the iOS audio session.
40
+ */
41
+ RTCAudioSession: any;
37
42
  }
38
43
 
39
44
  let cached: LiveKitRuntime | null | undefined;
@@ -59,6 +64,13 @@ export const loadLiveKit = (): LiveKitRuntime | null => {
59
64
  // moving subset across versions, the core package does not.
60
65
  // eslint-disable-next-line @typescript-eslint/no-require-imports
61
66
  const core = require('livekit-client');
67
+ let webrtc: any = null;
68
+ try {
69
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
70
+ webrtc = require('@livekit/react-native-webrtc');
71
+ } catch {
72
+ // Only needed for CallKit; calls work without it.
73
+ }
62
74
 
63
75
  cached = {
64
76
  Room: core.Room,
@@ -69,6 +81,7 @@ export const loadLiveKit = (): LiveKitRuntime | null => {
69
81
  VideoTrack: rn.VideoTrack,
70
82
  AudioSession: rn.AudioSession,
71
83
  registerGlobals: rn.registerGlobals,
84
+ RTCAudioSession: webrtc?.RTCAudioSession ?? null,
72
85
  };
73
86
  } catch {
74
87
  cached = null;