Skip to content

Commit 6f19978

Browse files
committed
Updates
1 parent a826af7 commit 6f19978

8 files changed

Lines changed: 199 additions & 60 deletions

File tree

callkit/.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.png filter=lfs diff=lfs merge=lfs -text

callkit/callkit-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

callkit/callkit-example.xcodeproj/project.pbxproj

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,8 @@
292292
ENABLE_USER_SELECTED_FILES = readonly;
293293
GENERATE_INFOPLIST_FILE = YES;
294294
INFOPLIST_FILE = "callkit-example/Info.plist";
295-
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Mic needed for example";
295+
INFOPLIST_KEY_NSCameraUsageDescription = "Camera access requested by SDK";
296+
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Microphone access requested by SDK";
296297
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
297298
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
298299
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -307,7 +308,7 @@
307308
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
308309
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
309310
MACOSX_DEPLOYMENT_TARGET = 26.0;
310-
MARKETING_VERSION = 1.0;
311+
MARKETING_VERSION = 1.0.0;
311312
PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.swift-sdk.callkit-example";
312313
PRODUCT_NAME = "$(TARGET_NAME)";
313314
REGISTER_APP_GROUPS = YES;
@@ -350,7 +351,8 @@
350351
ENABLE_USER_SELECTED_FILES = readonly;
351352
GENERATE_INFOPLIST_FILE = YES;
352353
INFOPLIST_FILE = "callkit-example/Info.plist";
353-
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Mic needed for example";
354+
INFOPLIST_KEY_NSCameraUsageDescription = "Camera access requested by SDK";
355+
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Microphone access requested by SDK";
354356
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
355357
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
356358
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -365,7 +367,7 @@
365367
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
366368
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
367369
MACOSX_DEPLOYMENT_TARGET = 26.0;
368-
MARKETING_VERSION = 1.0;
370+
MARKETING_VERSION = 1.0.0;
369371
PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.swift-sdk.callkit-example";
370372
PRODUCT_NAME = "$(TARGET_NAME)";
371373
REGISTER_APP_GROUPS = YES;

callkit/callkit-example/Assets.xcassets/AppIcon.appiconset/Contents.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"images" : [
33
{
4+
"filename" : "callkit-example-icon.png",
45
"idiom" : "universal",
56
"platform" : "ios",
67
"size" : "1024x1024"
Lines changed: 3 additions & 0 deletions
Loading

callkit/callkit-example/CallManager.swift

Lines changed: 88 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class CallManager: NSObject, ObservableObject {
4343
// State
4444
@Published var callState: CallState = .idle
4545
@Published var voipToken: String?
46+
@Published var activeCallUUID: UUID?
4647

4748
@AppStorage("url") var url: String = ""
4849
@AppStorage("token") var token: String = ""
@@ -68,8 +69,6 @@ class CallManager: NSObject, ObservableObject {
6869

6970
let room = Room()
7071

71-
private var activeCallUUID: UUID?
72-
7372
var hasActiveCall: Bool {
7473
switch callState {
7574
case .activeIncoming, .activeOutgoing, .connected:
@@ -86,9 +85,10 @@ class CallManager: NSObject, ObservableObject {
8685
pushRegistry.delegate = self
8786
_ = provider
8887

89-
// Set auto-config off
88+
// Set audio session auto-config off
9089
AudioManager.shared.audioSession.isAutomaticConfigurationEnabled = false
9190

91+
// Set audio engine off
9292
do {
9393
try AudioManager.shared.setEngineAvailability(.none)
9494
} catch {
@@ -98,9 +98,6 @@ class CallManager: NSObject, ObservableObject {
9898

9999
func startCall(handle: String) async {
100100
let callUUID = UUID()
101-
Task { @MainActor in
102-
activeCallUUID = callUUID
103-
}
104101

105102
let handle = CXHandle(type: .generic, value: handle)
106103
let startCallAction = CXStartCallAction(call: callUUID, handle: handle)
@@ -109,22 +106,31 @@ class CallManager: NSObject, ObservableObject {
109106
do {
110107
try await callController.request(transaction)
111108
print("Started call")
109+
110+
Task { @MainActor in
111+
activeCallUUID = callUUID
112+
}
112113
} catch {
113114
print("Failed to start call: \(error)")
114115
}
115116
}
116117

117118
func endCall() async {
118-
guard let callUUID = activeCallUUID else { return }
119-
120-
let endCallAction = CXEndCallAction(call: callUUID)
121-
let transaction = CXTransaction(action: endCallAction)
122-
123-
do {
124-
try await callController.request(transaction)
125-
print("Ended call")
126-
} catch {
127-
print("Failed to end call: \(error)")
119+
Task { @MainActor in
120+
// Read `activeCallUUID` on main thread
121+
guard let callUUID = activeCallUUID else { return }
122+
123+
Task {
124+
let endCallAction = CXEndCallAction(call: callUUID)
125+
let transaction = CXTransaction(action: endCallAction)
126+
127+
do {
128+
try await callController.request(transaction)
129+
print("Ended call")
130+
} catch {
131+
print("Failed to end call: \(error)")
132+
}
133+
}
128134
}
129135
}
130136

@@ -143,9 +149,9 @@ class CallManager: NSObject, ObservableObject {
143149

144150
// NOTE: Using sync version for background mode compatibility
145151
func reportIncomingCallSync(from callerId: String, callerName: String, completion: @escaping @Sendable ((any Error)?) -> Void) {
146-
let callUUID = UUID()
147-
activeCallUUID = callUUID
152+
logger.debug("Incoming call")
148153

154+
let callUUID = UUID()
149155
let callUpdate = CXCallUpdate()
150156
callUpdate.remoteHandle = CXHandle(type: .generic, value: callerId)
151157
callUpdate.hasVideo = false
@@ -155,6 +161,7 @@ class CallManager: NSObject, ObservableObject {
155161

156162
Task { @MainActor in
157163
self.callState = .activeIncoming
164+
self.activeCallUUID = callUUID
158165
}
159166
}
160167
}
@@ -178,16 +185,16 @@ extension CallManager {
178185

179186
extension CallManager: CXProviderDelegate {
180187
func providerDidReset(_: CXProvider) {
181-
logger.debug("CXProvider did reset")
188+
logger.debug("Did reset")
182189

183190
Task { @MainActor in
191+
self.activeCallUUID = nil
184192
self.callState = .idle
185193
}
186-
activeCallUUID = nil
187194
}
188195

189196
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
190-
logger.debug("CXProvider call start")
197+
logger.debug("Call starting...")
191198

192199
Task { @MainActor in
193200
self.callState = .activeOutgoing
@@ -206,74 +213,94 @@ extension CallManager: CXProviderDelegate {
206213
action.fulfill()
207214
} catch {
208215
logger.critical("Failed to connect to room with error: \(error)")
216+
action.fail()
209217

210218
Task { @MainActor in
211219
self.callState = .errored(error)
220+
self.activeCallUUID = nil
212221
}
213-
214-
action.fulfill()
215222
}
216223
}
217224
}
218225

219226
func provider(_: CXProvider, perform action: CXAnswerCallAction) {
220-
logger.debug("CXProvider answer call")
221-
222-
Task { @MainActor in
223-
self.callState = .connected
224-
}
227+
logger.debug("Answer call")
225228

226229
Task {
227230
do {
231+
try await connectToRoom()
228232
logger.debug("Connected to room")
233+
234+
Task { @MainActor in
235+
self.callState = .connected
236+
}
237+
229238
action.fulfill()
230-
try await connectToRoom()
231239
} catch {
232240
logger.critical("Failed to connect to room with error: \(error)")
233-
action.fulfill()
241+
242+
Task { @MainActor in
243+
self.callState = .errored(error)
244+
}
245+
246+
action.fail()
234247
}
235248
}
236249
}
237250

238251
func provider(_: CXProvider, perform action: CXEndCallAction) {
239-
logger.debug("CXProvider end call")
240-
241-
Task { @MainActor in
242-
self.activeCallUUID = nil
243-
self.callState = .idle
244-
}
252+
logger.debug("End call")
245253

246254
Task {
247255
await disconnectFromRoom()
248256
action.fulfill()
257+
258+
Task { @MainActor in
259+
self.activeCallUUID = nil
260+
261+
if case .errored = self.callState {
262+
// Keep the errored state, for failed incoming cases.
263+
} else {
264+
self.callState = .idle
265+
}
266+
}
249267
}
250268
}
251269

252270
func provider(_: CXProvider, perform action: CXSetMutedCallAction) {
253-
logger.debug("CXProvider muted call")
254-
255-
// TODO: Handle mute
256-
action.fulfill()
271+
Task {
272+
do {
273+
try await room.localParticipant.setMicrophone(enabled: !action.isMuted)
274+
logger.debug("Muted call: \(action.isMuted)")
275+
action.fulfill()
276+
} catch {
277+
logger.critical("Failed to set microphone enabled: \(!action.isMuted) with error: \(error.localizedDescription)")
278+
action.fail()
279+
}
280+
}
257281
}
258282

259-
func provider(_: CXProvider, didActivate _: AVAudioSession) {
283+
func provider(_: CXProvider, didActivate session: AVAudioSession) {
260284
// Audio session can now be activated.
261-
logger.debug("CXProvider did activate session")
285+
logger.debug("Did activate session")
262286

263287
do {
264-
let session = AVAudioSession.sharedInstance()
265288
try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers])
266-
267289
try AudioManager.shared.setEngineAvailability(.default)
268-
269290
} catch {
270-
print("Failed to set engine permissions: \(error.localizedDescription)")
291+
logger.critical("Failed to set engine availability: \(error.localizedDescription)")
271292
}
272293
}
273294

274295
func provider(_: CXProvider, didDeactivate _: AVAudioSession) {
275296
// Audio session will need to be deactivated.
276-
logger.debug("CXProvider did deactivate session")
297+
logger.debug("Did deactivate session")
298+
299+
do {
300+
try AudioManager.shared.setEngineAvailability(.none)
301+
} catch {
302+
logger.critical("Failed to set engine availability: \(error.localizedDescription)")
303+
}
277304
}
278305
}
279306

@@ -283,7 +310,8 @@ extension CallManager: PKPushRegistryDelegate {
283310
func pushRegistry(_: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
284311
guard type == .voIP else { return }
285312
let token = pushCredentials.token.map { String(format: "%02x", $0) }.joined()
286-
logger.info("token: \(token)")
313+
314+
logger.info("Push token updated: \(token)")
287315

288316
Task { @MainActor in
289317
self.voipToken = token
@@ -292,7 +320,7 @@ extension CallManager: PKPushRegistryDelegate {
292320

293321
func pushRegistry(_: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
294322
guard type == .voIP else { return }
295-
logger.info("VoIP Push Token invalidated")
323+
logger.info("Push Token invalidated")
296324

297325
Task { @MainActor in
298326
voipToken = nil
@@ -305,7 +333,17 @@ extension CallManager: PKPushRegistryDelegate {
305333
return
306334
}
307335

308-
logger.info("Received VoIP push with payload: \(payload.dictionaryPayload)")
336+
logger.info("Received push with payload: \(payload.dictionaryPayload)")
337+
338+
/// NOTE: I've observed that the mic cannot initialized if we don't set .playAndRecord here,
339+
/// which appears like a bug in my opinion, since ``CallManager/provider(_:didActivate:)``
340+
/// should be invoked at the right time instead of having to set the session category here.
341+
do {
342+
let session = AVAudioSession.sharedInstance()
343+
try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers])
344+
} catch {
345+
print("Failed to configure AVAudioSession: \(error.localizedDescription)")
346+
}
309347

310348
// Extract caller information from payload
311349
let callerId = payload.dictionaryPayload["callerId"] as? String ?? UUID().uuidString

0 commit comments

Comments
 (0)