@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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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",
|