@poolse/react-native-calls 0.4.1 → 0.4.2

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.
@@ -168,6 +168,9 @@ final class CallCore {
168
168
  eventsAdapter.core = self
169
169
  surfaceAdapter.core = self
170
170
  callKit.actions = self
171
+ // LiveKit's audio stays off until CallKit activates the session —
172
+ // configured now, at launch, before any activation can happen.
173
+ mediaPort.prepareAudio()
171
174
  }
172
175
 
173
176
  // MARK: - native entry points
@@ -61,13 +61,24 @@ final class LiveKitMediaPort: NSObject, MediaPort {
61
61
  super.init()
62
62
  NotificationCenter.default.addObserver(self, selector: #selector(routeChanged),
63
63
  name: AVAudioSession.routeChangeNotification, object: nil)
64
+ NotificationCenter.default.addObserver(self, selector: #selector(interrupted),
65
+ name: AVAudioSession.interruptionNotification, object: nil)
66
+ NotificationCenter.default.addObserver(self, selector: #selector(mediaServicesReset),
67
+ name: AVAudioSession.mediaServicesWereResetNotification,
68
+ object: nil)
64
69
  }
65
70
 
66
71
  // MARK: - audio session (CallKit-driven)
67
72
 
68
73
  /// Before any room exists: LiveKit must not configure the session nor
69
74
  /// start its engine outside CallKit's activation window.
70
- private func prepareAudio() {
75
+ ///
76
+ /// Called at app LAUNCH (CallCore.init, from the app-delegate
77
+ /// subscriber), as LiveKit's CallKit integration requires — before any
78
+ /// audio session can be activated. Doing it inside didActivate meant a
79
+ /// call answered with the app closed created and reconfigured LiveKit's
80
+ /// audio manager mid-activation, and came up with no audio.
81
+ func prepareAudio() {
71
82
  guard !audioPrepared else { return }
72
83
  audioPrepared = true
73
84
  AudioManager.shared.audioSession.isAutomaticConfigurationEnabled = false
@@ -80,17 +91,31 @@ final class LiveKitMediaPort: NSObject, MediaPort {
80
91
  func audioActivated(_ session: AVAudioSession, media: String) {
81
92
  prepareAudio()
82
93
  audioActive = true
83
- let video = media == "video"
84
- do {
85
- try session.setCategory(.playAndRecord, mode: video && speakerWanted != false ? .videoChat : .voiceChat,
86
- options: [.allowBluetooth, .allowBluetoothA2DP])
87
- } catch {
88
- NSLog("[PoolseCalls] audio session category failed: %@", "\(error)")
94
+ if media != self.media && room == nil { self.media = media }
95
+ reassertAudio(session)
96
+ }
97
+
98
+ /// Put the call's audio back the way the call needs it: play-and-record
99
+ /// in the voice/video chat mode, LiveKit's engine available, the chosen
100
+ /// route. Idempotent — only touches what differs — so it is safe to run
101
+ /// whenever anything may have changed the session behind the call's
102
+ /// back (another audio library reconfiguring it as JS boots mid-call,
103
+ /// an interruption, a media-services reset). Only while CallKit (or the
104
+ /// no-CallKit fallback) holds the session active.
105
+ func reassertAudio(_ session: AVAudioSession = .sharedInstance()) {
106
+ guard audioActive else { return }
107
+ let mode: AVAudioSession.Mode = media == "video" && speakerWanted != false ? .videoChat : .voiceChat
108
+ if session.category != .playAndRecord || session.mode != mode {
109
+ do {
110
+ try session.setCategory(.playAndRecord, mode: mode,
111
+ options: [.allowBluetooth, .allowBluetoothA2DP])
112
+ } catch {
113
+ NSLog("[PoolseCalls] audio session category failed: %@", "\(error)")
114
+ }
89
115
  }
90
116
  do { try AudioManager.shared.setEngineAvailability(.default) } catch {
91
117
  NSLog("[PoolseCalls] setEngineAvailability(.default) failed: %@", "\(error)")
92
118
  }
93
- if media != self.media && room == nil { self.media = media }
94
119
  applyRoute()
95
120
  }
96
121
 
@@ -159,6 +184,9 @@ final class LiveKitMediaPort: NSObject, MediaPort {
159
184
  do { try await r.localParticipant.setMicrophone(enabled: !muted) } catch {
160
185
  NSLog("[PoolseCalls] microphone publish failed: %@", "\(error)")
161
186
  }
187
+ // Connected and publishing: make sure nothing changed the session
188
+ // while we connected (a cold answer boots JS in exactly this window).
189
+ await MainActor.run { self.reassertAudio() }
162
190
  let camera = await MainActor.run { self.cameraWanted ?? wantsVideo }
163
191
  if camera { await self.enableCamera(r, true) }
164
192
  await MainActor.run {
@@ -297,9 +325,28 @@ final class LiveKitMediaPort: NSObject, MediaPort {
297
325
  events?.onAudioRoute(currentRoute())
298
326
  }
299
327
 
328
+ /// An interruption (another app's audio, Siri) ended: the session may
329
+ /// have been reconfigured; restore the call's.
330
+ @objc private func interrupted(_ note: Notification) {
331
+ guard let raw = note.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
332
+ AVAudioSession.InterruptionType(rawValue: raw) == .ended else { return }
333
+ DispatchQueue.main.async { [weak self] in self?.reassertAudio() }
334
+ }
335
+
336
+ @objc private func mediaServicesReset(_ note: Notification) {
337
+ DispatchQueue.main.async { [weak self] in self?.reassertAudio() }
338
+ }
339
+
300
340
  @objc private func routeChanged(_ note: Notification) {
341
+ let reason = (note.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt)
342
+ .flatMap(AVAudioSession.RouteChangeReason.init(rawValue:))
301
343
  DispatchQueue.main.async { [weak self] in
302
344
  guard let self = self, self.audioActive else { return }
345
+ // Something reconfigured the shared session mid-call: put it back.
346
+ if reason == .categoryChange {
347
+ self.reassertAudio()
348
+ return
349
+ }
303
350
  // A user's speaker choice survives route changes, except that a
304
351
  // newly connected headset/Bluetooth device always wins.
305
352
  let route = self.currentRoute()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poolse/react-native-calls",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Native call core for poolse — Kotlin/Swift calling engine with a thin React Native bridge. Ring, answer and carry audio with the app killed and the phone locked.",
5
5
  "license": "MIT",
6
6
  "type": "module",