@capgo/capacitor-stream-call 7.1.31 → 7.7.7
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.
- package/README.md +102 -12
- package/android/build.gradle +2 -2
- package/android/src/main/AndroidManifest.xml +0 -6
- package/android/src/main/java/ee/forgr/capacitor/streamcall/AcceptCallReceiver.kt +1 -1
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomStreamIntentResolver.kt +2 -2
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +2946 -2462
- package/android/src/main/res/raw/outgoing.mp3 +0 -0
- package/dist/docs.json +204 -1
- package/dist/esm/definitions.d.ts +64 -2
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -0
- package/dist/esm/web.js +3 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +3 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +443 -154
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +35 -30
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import WebKit
|
|
|
12
12
|
*/
|
|
13
13
|
@objc(StreamCallPlugin)
|
|
14
14
|
public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
15
|
+
private let pluginVersion: String = "7.7.7"
|
|
15
16
|
public let identifier = "StreamCallPlugin"
|
|
16
17
|
public let jsName = "StreamCall"
|
|
17
18
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -19,6 +20,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
19
20
|
CAPPluginMethod(name: "login", returnType: CAPPluginReturnPromise),
|
|
20
21
|
CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
|
|
21
22
|
CAPPluginMethod(name: "call", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "joinCall", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "toggleViews", returnType: CAPPluginReturnPromise),
|
|
22
25
|
CAPPluginMethod(name: "endCall", returnType: CAPPluginReturnPromise),
|
|
23
26
|
CAPPluginMethod(name: "setMicrophoneEnabled", returnType: CAPPluginReturnPromise),
|
|
24
27
|
CAPPluginMethod(name: "setCameraEnabled", returnType: CAPPluginReturnPromise),
|
|
@@ -30,7 +33,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
30
33
|
CAPPluginMethod(name: "getCallInfo", returnType: CAPPluginReturnPromise),
|
|
31
34
|
CAPPluginMethod(name: "setDynamicStreamVideoApikey", returnType: CAPPluginReturnPromise),
|
|
32
35
|
CAPPluginMethod(name: "getDynamicStreamVideoApikey", returnType: CAPPluginReturnPromise),
|
|
33
|
-
CAPPluginMethod(name: "getCurrentUser", returnType: CAPPluginReturnPromise)
|
|
36
|
+
CAPPluginMethod(name: "getCurrentUser", returnType: CAPPluginReturnPromise),
|
|
37
|
+
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
|
|
34
38
|
]
|
|
35
39
|
|
|
36
40
|
private enum State {
|
|
@@ -47,6 +51,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
47
51
|
private var hostingController: UIHostingController<CallOverlayView>?
|
|
48
52
|
private var tokenSubscription: AnyCancellable?
|
|
49
53
|
private var activeCallSubscription: AnyCancellable?
|
|
54
|
+
private var speakerSubscription: AnyCancellable?
|
|
50
55
|
private var lastVoIPToken: String?
|
|
51
56
|
private var touchInterceptView: TouchInterceptView?
|
|
52
57
|
private var needsTouchInterceptorSetup: Bool = false
|
|
@@ -59,9 +64,12 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
59
64
|
private var currentCallId: String = ""
|
|
60
65
|
private var currentCallState: String = ""
|
|
61
66
|
private var hasNotifiedCallJoined: Bool = false
|
|
67
|
+
private var previousCallingState: CallingState?
|
|
62
68
|
|
|
63
69
|
@Injected(\.callKitAdapter) var callKitAdapter
|
|
64
70
|
@Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
|
|
71
|
+
@Injected(\.utils) var utils
|
|
72
|
+
|
|
65
73
|
private var webviewDelegate: WebviewNavigationDelegate?
|
|
66
74
|
|
|
67
75
|
// Declare as optional and initialize in load() method
|
|
@@ -188,9 +196,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
188
196
|
DispatchQueue.main.async { [weak self] in
|
|
189
197
|
guard let self = self else { return }
|
|
190
198
|
|
|
191
|
-
// Cancel existing
|
|
199
|
+
// Cancel existing subscriptions if any
|
|
192
200
|
self.activeCallSubscription?.cancel()
|
|
193
201
|
self.activeCallSubscription = nil
|
|
202
|
+
self.speakerSubscription?.cancel()
|
|
203
|
+
self.speakerSubscription = nil
|
|
194
204
|
|
|
195
205
|
// Verify callViewModel exists
|
|
196
206
|
guard let callViewModel = self.callViewModel, let streamVideo = self.streamVideo else {
|
|
@@ -208,6 +218,52 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
208
218
|
// while the subscription is active
|
|
209
219
|
let viewModel = callViewModel
|
|
210
220
|
|
|
221
|
+
let callEvents = streamVideo
|
|
222
|
+
.eventPublisher()
|
|
223
|
+
.sink { [weak self] event in
|
|
224
|
+
guard let self else { return }
|
|
225
|
+
switch event {
|
|
226
|
+
case let .typeCallRejectedEvent(response):
|
|
227
|
+
let data: [String: Any] = [
|
|
228
|
+
"callId": response.callCid,
|
|
229
|
+
"state": "rejected"
|
|
230
|
+
]
|
|
231
|
+
notifyListeners("callEvent", data: data)
|
|
232
|
+
case let .typeCallEndedEvent(response):
|
|
233
|
+
let data: [String: Any] = [
|
|
234
|
+
"callId": response.callCid,
|
|
235
|
+
"state": "left"
|
|
236
|
+
]
|
|
237
|
+
notifyListeners("callEvent", data: data)
|
|
238
|
+
|
|
239
|
+
case let .typeCallSessionParticipantCountsUpdatedEvent(response):
|
|
240
|
+
let activeCall = streamVideo.state.activeCall
|
|
241
|
+
let callDropped = self.currentCallId == response.callCid && activeCall == nil
|
|
242
|
+
let onlyOneParticipant = activeCall?.cId == response.callCid && activeCall?.state.participantCount == 1
|
|
243
|
+
if onlyOneParticipant || callDropped {
|
|
244
|
+
self.endCallInternal()
|
|
245
|
+
} else {
|
|
246
|
+
print("""
|
|
247
|
+
onlyOneParticipant check:
|
|
248
|
+
- activeCall?.cId: \(String(describing: activeCall?.cId))
|
|
249
|
+
- response.callCid: \(response.callCid)
|
|
250
|
+
- activeCall?.state.participantCount: \(String(describing: activeCall?.state.participantCount))
|
|
251
|
+
- Result (onlyOneParticipant): \(onlyOneParticipant)
|
|
252
|
+
""")
|
|
253
|
+
}
|
|
254
|
+
if let count = activeCall?.state.participantCount {
|
|
255
|
+
let data: [String: Any] = [
|
|
256
|
+
"callId": response.callCid,
|
|
257
|
+
"state": "participant_counts",
|
|
258
|
+
"count": count
|
|
259
|
+
]
|
|
260
|
+
notifyListeners("callEvent", data: data)
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
211
267
|
// Subscribe to streamVideo.state.$activeCall to handle CallKit integration
|
|
212
268
|
let callPublisher = streamVideo.state.$activeCall
|
|
213
269
|
.receive(on: DispatchQueue.main)
|
|
@@ -220,6 +276,58 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
220
276
|
// Sync callViewModel with activeCall from streamVideo state
|
|
221
277
|
// This ensures CallKit integration works properly
|
|
222
278
|
viewModel.setActiveCall(activeCall)
|
|
279
|
+
viewModel.update(participantsLayout: .grid)
|
|
280
|
+
|
|
281
|
+
if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
|
|
282
|
+
print("Notifying call joined: \(callId)")
|
|
283
|
+
self.updateCallStatusAndNotify(callId: callId, state: "joined")
|
|
284
|
+
self.hasNotifiedCallJoined = true
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Subscribe to speaker status for this active call
|
|
288
|
+
self.speakerSubscription = activeCall.speaker.$status
|
|
289
|
+
.receive(on: DispatchQueue.main)
|
|
290
|
+
.sink { [weak self] speakerStatus in
|
|
291
|
+
guard let self = self else { return }
|
|
292
|
+
|
|
293
|
+
// Only emit if the current active call matches our current call ID
|
|
294
|
+
guard activeCall.cId == self.currentCallId else {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
print("Speaker status update: \(speakerStatus)")
|
|
299
|
+
|
|
300
|
+
let state: String
|
|
301
|
+
switch speakerStatus {
|
|
302
|
+
case .enabled:
|
|
303
|
+
state = "speaker_enabled"
|
|
304
|
+
case .disabled:
|
|
305
|
+
state = "speaker_disabled"
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let data: [String: Any] = [
|
|
309
|
+
"callId": self.currentCallId,
|
|
310
|
+
"state": state
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
self.notifyListeners("callEvent", data: data)
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// Clean up speaker subscription when activeCall becomes nil
|
|
317
|
+
print("Active call became nil, cleaning up speaker subscription")
|
|
318
|
+
self.speakerSubscription?.cancel()
|
|
319
|
+
self.speakerSubscription = nil
|
|
320
|
+
|
|
321
|
+
print("Call actually ending: \(self.currentCallId)")
|
|
322
|
+
|
|
323
|
+
// Notify that call has ended - use the stored call ID
|
|
324
|
+
self.updateCallStatusAndNotify(callId: self.currentCallId, state: "left")
|
|
325
|
+
|
|
326
|
+
// Reset notification flag when call ends
|
|
327
|
+
self.hasNotifiedCallJoined = false
|
|
328
|
+
|
|
329
|
+
// Remove the call overlay view and touch intercept view when not in a call
|
|
330
|
+
self.ensureViewRemoved()
|
|
223
331
|
}
|
|
224
332
|
}
|
|
225
333
|
|
|
@@ -238,6 +346,53 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
238
346
|
do {
|
|
239
347
|
try self.requireInitialized()
|
|
240
348
|
print("Call State Update: \(newState)")
|
|
349
|
+
// Check if transitioning from outgoing to idle
|
|
350
|
+
if newState == .idle,
|
|
351
|
+
let previousState = self.previousCallingState,
|
|
352
|
+
case .outgoing = previousState {
|
|
353
|
+
print("Call state changed from outgoing to idle. Ending call with ID: \(self.currentCallId)")
|
|
354
|
+
|
|
355
|
+
// Stop ongoing sound when call is cancelled
|
|
356
|
+
self.utils.callSoundsPlayer.stopOngoingSound()
|
|
357
|
+
|
|
358
|
+
// End the call using the stored call ID and type
|
|
359
|
+
if !self.currentCallId.isEmpty {
|
|
360
|
+
Task {
|
|
361
|
+
do {
|
|
362
|
+
// Parse call type and id from currentCallId format "type:id"
|
|
363
|
+
let components = self.currentCallId.components(separatedBy: ":")
|
|
364
|
+
if components.count >= 2 {
|
|
365
|
+
let callType = components[0]
|
|
366
|
+
let callId = components[1]
|
|
367
|
+
|
|
368
|
+
// Try to get the call and end it using the parsed call type
|
|
369
|
+
if let streamVideo = self.streamVideo {
|
|
370
|
+
let call = streamVideo.call(callType: callType, callId: callId)
|
|
371
|
+
try await call.end()
|
|
372
|
+
print("Successfully ended outgoing call: \(callId) with type: \(callType)")
|
|
373
|
+
|
|
374
|
+
let data: [String: Any] = [
|
|
375
|
+
"callId": call.cId,
|
|
376
|
+
"state": "outgoing_call_ended"
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
self.notifyListeners("callEvent", data: data)
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
print("Invalid call ID format: \(self.currentCallId)")
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
print("Error ending outgoing call \(self.currentCallId): \(error)")
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Clean up stored call information
|
|
389
|
+
DispatchQueue.main.async {
|
|
390
|
+
self.currentCallId = ""
|
|
391
|
+
print("Cleaned up call information after ending outgoing call")
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
241
396
|
|
|
242
397
|
if newState == .inCall {
|
|
243
398
|
print("- In call state detected")
|
|
@@ -249,85 +404,32 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
249
404
|
// Create/update overlay and make visible when there's an active call
|
|
250
405
|
self.createCallOverlayView()
|
|
251
406
|
|
|
407
|
+
// Stop ongoing sound when call is joined
|
|
408
|
+
self.utils.callSoundsPlayer.stopOngoingSound()
|
|
409
|
+
|
|
252
410
|
// Notify that a call has started - but only if we haven't notified for this call yet
|
|
253
411
|
if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
|
|
254
412
|
print("Notifying call joined: \(callId)")
|
|
255
413
|
self.updateCallStatusAndNotify(callId: callId, state: "joined")
|
|
256
414
|
self.hasNotifiedCallJoined = true
|
|
257
415
|
}
|
|
258
|
-
} else if case .incoming(let incomingCall) = newState {
|
|
259
|
-
// Extract caller information
|
|
260
|
-
Task {
|
|
261
|
-
var caller: [String: Any]?
|
|
262
|
-
var members: [[String: Any]]?
|
|
263
|
-
|
|
264
|
-
do {
|
|
265
|
-
// Get the call from StreamVideo to access detailed information
|
|
266
|
-
if let streamVideo = self.streamVideo {
|
|
267
|
-
let call = streamVideo.call(callType: incomingCall.type, callId: incomingCall.id)
|
|
268
|
-
let callInfo = try await call.get()
|
|
269
|
-
|
|
270
|
-
// Extract caller information
|
|
271
|
-
let createdBy = callInfo.call.createdBy
|
|
272
|
-
var callerData: [String: Any] = [:]
|
|
273
|
-
callerData["userId"] = createdBy.id
|
|
274
|
-
callerData["name"] = createdBy.name
|
|
275
|
-
callerData["imageURL"] = createdBy.image
|
|
276
|
-
callerData["role"] = createdBy.role
|
|
277
|
-
caller = callerData
|
|
278
|
-
|
|
279
|
-
// Extract members information from current participants if available
|
|
280
|
-
var membersArray: [[String: Any]] = []
|
|
281
|
-
if let activeCall = streamVideo.state.activeCall {
|
|
282
|
-
let participants = activeCall.state.participants
|
|
283
|
-
for participant in participants {
|
|
284
|
-
var memberData: [String: Any] = [:]
|
|
285
|
-
memberData["userId"] = participant.userId
|
|
286
|
-
memberData["name"] = participant.name
|
|
287
|
-
memberData["imageURL"] = participant.profileImageURL?.absoluteString ?? ""
|
|
288
|
-
memberData["role"] = participant.roles.first ?? ""
|
|
289
|
-
membersArray.append(memberData)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
members = membersArray.isEmpty ? nil : membersArray
|
|
293
|
-
}
|
|
294
|
-
} catch {
|
|
295
|
-
print("Failed to get call info for caller details: \(error)")
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Notify with caller information
|
|
299
|
-
let fullCallId = "\(incomingCall.type):\(incomingCall.id)"
|
|
300
|
-
self.updateCallStatusAndNotify(callId: fullCallId, state: "ringing", caller: caller, members: members)
|
|
301
|
-
}
|
|
302
|
-
} else if newState == .idle {
|
|
303
|
-
print("Call state changed to idle. CurrentCallId: \(self.currentCallId), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
|
|
304
|
-
|
|
305
|
-
// Only notify about call ending if we have a valid stored call ID and there's truly no active call
|
|
306
|
-
// This prevents false "left" events during normal state transitions
|
|
307
|
-
if !self.currentCallId.isEmpty && self.streamVideo?.state.activeCall == nil {
|
|
308
|
-
print("Call actually ending: \(self.currentCallId)")
|
|
309
|
-
|
|
310
|
-
// Notify that call has ended - use the stored call ID
|
|
311
|
-
self.updateCallStatusAndNotify(callId: self.currentCallId, state: "left")
|
|
312
416
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Remove the call overlay view when not in a call
|
|
317
|
-
self.ensureViewRemoved()
|
|
318
|
-
} else {
|
|
319
|
-
print("Not sending left event - CurrentCallId: \(self.currentCallId), ActiveCall exists: \(self.streamVideo?.state.activeCall != nil)")
|
|
320
|
-
}
|
|
417
|
+
} else if case .incoming(let incomingCall) = newState {
|
|
418
|
+
self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing")
|
|
419
|
+
// }
|
|
321
420
|
}
|
|
421
|
+
// Update the previous state for next comparison
|
|
422
|
+
self.previousCallingState = newState
|
|
322
423
|
} catch {
|
|
323
424
|
log.error("Error handling call state update: \(String(describing: error))")
|
|
324
425
|
}
|
|
325
426
|
}
|
|
326
427
|
|
|
327
|
-
// Combine
|
|
428
|
+
// Combine all publishers
|
|
328
429
|
self.activeCallSubscription = AnyCancellable {
|
|
329
430
|
callPublisher.cancel()
|
|
330
431
|
statePublisher.cancel()
|
|
432
|
+
callEvents.cancel()
|
|
331
433
|
}
|
|
332
434
|
|
|
333
435
|
print("Active call subscription setup completed")
|
|
@@ -421,9 +523,20 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
421
523
|
tokenSubscription = nil
|
|
422
524
|
activeCallSubscription?.cancel()
|
|
423
525
|
activeCallSubscription = nil
|
|
526
|
+
speakerSubscription?.cancel()
|
|
527
|
+
speakerSubscription = nil
|
|
424
528
|
lastVoIPToken = nil
|
|
529
|
+
state = .notInitialized
|
|
425
530
|
|
|
426
531
|
SecureUserRepository.shared.removeCurrentUser()
|
|
532
|
+
self.callViewModel = nil
|
|
533
|
+
self.overlayView = nil
|
|
534
|
+
|
|
535
|
+
// Clean up touch intercept view
|
|
536
|
+
if let touchInterceptView = self.touchInterceptView {
|
|
537
|
+
touchInterceptView.removeFromSuperview()
|
|
538
|
+
self.touchInterceptView = nil
|
|
539
|
+
}
|
|
427
540
|
|
|
428
541
|
// Update the CallOverlayView with nil StreamVideo instance
|
|
429
542
|
Task { @MainActor in
|
|
@@ -457,6 +570,85 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
457
570
|
}
|
|
458
571
|
}
|
|
459
572
|
|
|
573
|
+
@objc func toggleViews(_ call: CAPPluginCall) {
|
|
574
|
+
Task { @MainActor in
|
|
575
|
+
guard let viewModel = self.callViewModel else {
|
|
576
|
+
call.reject("ViewModel is not initialized.")
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let layouts: [ParticipantsLayout] = [.spotlight, .fullScreen, .grid]
|
|
581
|
+
let currentLayout = viewModel.participantsLayout
|
|
582
|
+
|
|
583
|
+
// If currentLayout is not in layouts, default to .grid index
|
|
584
|
+
let currentIndex = layouts.firstIndex(of: currentLayout) ?? layouts.firstIndex(of: .grid) ?? 0
|
|
585
|
+
let nextIndex = (currentIndex + 1) % layouts.count
|
|
586
|
+
let nextLayout = layouts[nextIndex]
|
|
587
|
+
|
|
588
|
+
viewModel.update(participantsLayout: nextLayout)
|
|
589
|
+
|
|
590
|
+
call.resolve([
|
|
591
|
+
"newLayout": "\(nextLayout)"
|
|
592
|
+
])
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
@objc func joinCall(_ call: CAPPluginCall) {
|
|
597
|
+
guard let callId = call.getString("callId") else {
|
|
598
|
+
call.reject("Missing required parameter: callId")
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
guard let callType = call.getString("callType") else {
|
|
603
|
+
call.reject("Missing required parameter: callType")
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Initialize if needed
|
|
608
|
+
if state == .notInitialized {
|
|
609
|
+
initializeStreamVideo()
|
|
610
|
+
if state != .initialized {
|
|
611
|
+
call.reject("Failed to initialize StreamVideo")
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
do {
|
|
617
|
+
try requireInitialized()
|
|
618
|
+
|
|
619
|
+
Task {
|
|
620
|
+
do {
|
|
621
|
+
print("Joining call:")
|
|
622
|
+
print("- Call ID: \(callId)")
|
|
623
|
+
print("- Call Type: \(callType)")
|
|
624
|
+
|
|
625
|
+
// Create the call object
|
|
626
|
+
await self.callViewModel?.joinCall(
|
|
627
|
+
callType: callType,
|
|
628
|
+
callId: callId
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
// Now send the created event with complete member data
|
|
632
|
+
self.updateCallStatusAndNotify(callId: callId, state: "joined")
|
|
633
|
+
|
|
634
|
+
// Update UI on main thread
|
|
635
|
+
await MainActor.run {
|
|
636
|
+
// self.overlayViewModel?.updateCall(streamCall)
|
|
637
|
+
self.overlayView?.isHidden = false
|
|
638
|
+
self.webView?.isOpaque = false
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
call.resolve([
|
|
642
|
+
"success": true
|
|
643
|
+
])
|
|
644
|
+
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
call.reject("StreamVideo not initialized")
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
460
652
|
@objc func call(_ call: CAPPluginCall) {
|
|
461
653
|
guard let members = call.getArray("userIds", String.self) else {
|
|
462
654
|
call.reject("Missing required parameter: userIds (array of user IDs)")
|
|
@@ -497,6 +689,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
497
689
|
print("- Users: \(members)")
|
|
498
690
|
print("- Should Ring: \(shouldRing)")
|
|
499
691
|
print("- Team: \(String(describing: team))")
|
|
692
|
+
// Play outgoing call sound
|
|
693
|
+
self.utils.callSoundsPlayer.playOutgoingCallSound()
|
|
500
694
|
|
|
501
695
|
// Create the call object
|
|
502
696
|
await self.callViewModel?.startCall(
|
|
@@ -625,7 +819,97 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
625
819
|
}
|
|
626
820
|
}
|
|
627
821
|
|
|
822
|
+
private func endCallInternal() {
|
|
823
|
+
// Stop ongoing sound when call ends
|
|
824
|
+
self.utils.callSoundsPlayer.stopOngoingSound()
|
|
825
|
+
|
|
826
|
+
do {
|
|
827
|
+
try requireInitialized()
|
|
828
|
+
|
|
829
|
+
Task {
|
|
830
|
+
// Check both active call and callViewModel's call state to handle outgoing calls
|
|
831
|
+
let activeCall = streamVideo?.state.activeCall
|
|
832
|
+
let viewModelCall = await callViewModel?.call
|
|
833
|
+
|
|
834
|
+
// Helper function to determine if we should end or leave the call
|
|
835
|
+
func shouldEndCall(for streamCall: Call) async throws -> Bool {
|
|
836
|
+
do {
|
|
837
|
+
let callInfo = try await streamCall.get()
|
|
838
|
+
let currentUserId = streamVideo?.user.id
|
|
839
|
+
let createdBy = callInfo.call.createdBy.id
|
|
840
|
+
let isCreator = createdBy == currentUserId
|
|
841
|
+
|
|
842
|
+
// Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
|
|
843
|
+
// let totalParticipants = await streamCall.state.participants.count
|
|
844
|
+
let forceEnd = await (activeCall?.state.custom["type"]?.stringValue == "direct")
|
|
845
|
+
let shouldEnd = isCreator || forceEnd
|
|
846
|
+
|
|
847
|
+
print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), ShouldEnd: \(shouldEnd)")
|
|
848
|
+
|
|
849
|
+
return shouldEnd
|
|
850
|
+
} catch {
|
|
851
|
+
print("Error getting call info for \(streamCall.cId), defaulting to leave: \(error)")
|
|
852
|
+
return false // Fallback to leave if we can't determine
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if let activeCall = activeCall {
|
|
857
|
+
// There's an active call, check if we should end or leave
|
|
858
|
+
do {
|
|
859
|
+
let shouldEnd = try await shouldEndCall(for: activeCall)
|
|
860
|
+
|
|
861
|
+
if shouldEnd {
|
|
862
|
+
print("Ending active call \(activeCall.cId) for all participants")
|
|
863
|
+
try await activeCall.end()
|
|
864
|
+
} else {
|
|
865
|
+
print("Leaving active call \(activeCall.cId)")
|
|
866
|
+
try await activeCall.leave()
|
|
867
|
+
}
|
|
868
|
+
} catch {
|
|
869
|
+
print("Error ending/leaving active call: \(error)")
|
|
870
|
+
try await activeCall.leave() // Fallback to leave
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
await MainActor.run {
|
|
874
|
+
self.overlayView?.isHidden = true
|
|
875
|
+
self.webView?.isOpaque = true
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
} else if let viewModelCall = viewModelCall {
|
|
879
|
+
// There's a call in the viewModel (likely outgoing/ringing), check if we should end or leave
|
|
880
|
+
do {
|
|
881
|
+
let shouldEnd = try await shouldEndCall(for: viewModelCall)
|
|
882
|
+
|
|
883
|
+
if shouldEnd {
|
|
884
|
+
print("Ending viewModel call \(viewModelCall.cId) for all participants")
|
|
885
|
+
try await viewModelCall.end()
|
|
886
|
+
} else {
|
|
887
|
+
print("Leaving viewModel call \(viewModelCall.cId)")
|
|
888
|
+
try await viewModelCall.leave()
|
|
889
|
+
}
|
|
890
|
+
} catch {
|
|
891
|
+
print("Error ending/leaving viewModel call: \(error)")
|
|
892
|
+
try await viewModelCall.leave() // Fallback to leave
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Also hang up to reset the calling state
|
|
896
|
+
await callViewModel?.hangUp()
|
|
897
|
+
|
|
898
|
+
await MainActor.run {
|
|
899
|
+
self.overlayView?.isHidden = true
|
|
900
|
+
self.webView?.isOpaque = true
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} catch {
|
|
905
|
+
log.error("StreamVideo not initialized")
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
628
909
|
@objc func endCall(_ call: CAPPluginCall) {
|
|
910
|
+
// Stop ongoing sound when call ends
|
|
911
|
+
self.utils.callSoundsPlayer.stopOngoingSound()
|
|
912
|
+
|
|
629
913
|
do {
|
|
630
914
|
try requireInitialized()
|
|
631
915
|
|
|
@@ -641,10 +925,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
641
925
|
let currentUserId = streamVideo?.user.id
|
|
642
926
|
let createdBy = callInfo.call.createdBy.id
|
|
643
927
|
let isCreator = createdBy == currentUserId
|
|
928
|
+
let forceEnd = await (activeCall?.state.custom["type"]?.stringValue == "direct")
|
|
644
929
|
|
|
645
930
|
// Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
|
|
646
931
|
let totalParticipants = await streamCall.state.participants.count
|
|
647
|
-
let shouldEnd = isCreator ||
|
|
932
|
+
let shouldEnd = isCreator || forceEnd
|
|
648
933
|
|
|
649
934
|
print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), TotalParticipants: \(totalParticipants), ShouldEnd: \(shouldEnd)")
|
|
650
935
|
|
|
@@ -735,10 +1020,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
735
1020
|
Task {
|
|
736
1021
|
do {
|
|
737
1022
|
if let activeCall = streamVideo?.state.activeCall {
|
|
738
|
-
if enabled {
|
|
739
|
-
try await activeCall.microphone.
|
|
740
|
-
} else {
|
|
741
|
-
try await activeCall.microphone.
|
|
1023
|
+
if enabled && activeCall.microphone.status == .disabled {
|
|
1024
|
+
try await activeCall.microphone.toggle()
|
|
1025
|
+
} else if !enabled && activeCall.microphone.status == .enabled {
|
|
1026
|
+
try await activeCall.microphone.toggle()
|
|
742
1027
|
}
|
|
743
1028
|
call.resolve([
|
|
744
1029
|
"success": true
|
|
@@ -768,10 +1053,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
768
1053
|
Task {
|
|
769
1054
|
do {
|
|
770
1055
|
if let activeCall = streamVideo?.state.activeCall {
|
|
771
|
-
if enabled {
|
|
772
|
-
try await activeCall.camera.
|
|
773
|
-
} else {
|
|
774
|
-
try await activeCall.camera.
|
|
1056
|
+
if enabled && activeCall.camera.status == .disabled {
|
|
1057
|
+
try await activeCall.camera.toggle()
|
|
1058
|
+
} else if !enabled && activeCall.camera.status == .enabled {
|
|
1059
|
+
try await activeCall.camera.toggle()
|
|
775
1060
|
}
|
|
776
1061
|
call.resolve([
|
|
777
1062
|
"success": true
|
|
@@ -896,7 +1181,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
896
1181
|
Task { @MainActor in
|
|
897
1182
|
self.callViewModel = CallViewModel(participantsLayout: .grid)
|
|
898
1183
|
// self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
|
|
899
|
-
|
|
900
1184
|
// Setup subscriptions for new StreamVideo instance
|
|
901
1185
|
self.setupActiveCallSubscription()
|
|
902
1186
|
}
|
|
@@ -905,7 +1189,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
905
1189
|
|
|
906
1190
|
state = .initialized
|
|
907
1191
|
callKitAdapter.streamVideo = self.streamVideo
|
|
908
|
-
callKitAdapter.availabilityPolicy = .
|
|
1192
|
+
callKitAdapter.availabilityPolicy = .regionBased
|
|
909
1193
|
|
|
910
1194
|
setupTokenSubscription()
|
|
911
1195
|
|
|
@@ -1035,6 +1319,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1035
1319
|
webView.isOpaque = false
|
|
1036
1320
|
webView.backgroundColor = .clear
|
|
1037
1321
|
webView.scrollView.backgroundColor = .clear
|
|
1322
|
+
|
|
1323
|
+
// Add touch intercept view when making existing overlay visible
|
|
1324
|
+
addTouchInterceptView(webView: webView, parent: parent, overlayView: existingOverlayView)
|
|
1038
1325
|
return
|
|
1039
1326
|
}
|
|
1040
1327
|
|
|
@@ -1067,8 +1354,38 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1067
1354
|
self.hostingController = overlayView
|
|
1068
1355
|
self.overlayView = overlayView.view
|
|
1069
1356
|
|
|
1070
|
-
// Add touch
|
|
1071
|
-
|
|
1357
|
+
// Add touch intercept view for the new overlay
|
|
1358
|
+
addTouchInterceptView(webView: webView, parent: parent, overlayView: overlayView.view)
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
private func addTouchInterceptView(webView: WKWebView, parent: UIView, overlayView: UIView) {
|
|
1362
|
+
// Create touch intercept view
|
|
1363
|
+
let touchInterceptView = TouchInterceptView(frame: parent.bounds)
|
|
1364
|
+
touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
|
|
1365
|
+
touchInterceptView.backgroundColor = .clear
|
|
1366
|
+
touchInterceptView.isOpaque = false
|
|
1367
|
+
touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
|
|
1368
|
+
|
|
1369
|
+
// Add to parent view
|
|
1370
|
+
parent.addSubview(touchInterceptView)
|
|
1371
|
+
|
|
1372
|
+
// Set up active call check function
|
|
1373
|
+
touchInterceptView.setActiveCallCheck { [weak self] in
|
|
1374
|
+
return self?.streamVideo?.state.activeCall != nil
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Store reference and set call active
|
|
1378
|
+
self.touchInterceptView = touchInterceptView
|
|
1379
|
+
|
|
1380
|
+
// Setup constraints for touchInterceptView to cover the entire parent
|
|
1381
|
+
NSLayoutConstraint.activate([
|
|
1382
|
+
touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
|
|
1383
|
+
touchInterceptView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
|
|
1384
|
+
touchInterceptView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
|
|
1385
|
+
touchInterceptView.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
|
|
1386
|
+
])
|
|
1387
|
+
|
|
1388
|
+
print("Touch intercept view added and activated for call")
|
|
1072
1389
|
}
|
|
1073
1390
|
|
|
1074
1391
|
// MARK: - Dynamic API Key Management
|
|
@@ -1094,6 +1411,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1094
1411
|
}
|
|
1095
1412
|
|
|
1096
1413
|
func ensureViewRemoved() {
|
|
1414
|
+
print("Removing call views and touch intercept")
|
|
1415
|
+
|
|
1416
|
+
// Remove touch intercept view
|
|
1417
|
+
if let touchInterceptView = self.touchInterceptView {
|
|
1418
|
+
print("Removing touch intercept view")
|
|
1419
|
+
touchInterceptView.removeFromSuperview()
|
|
1420
|
+
self.touchInterceptView = nil
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1097
1423
|
// Check if we have an overlay view
|
|
1098
1424
|
if let existingOverlayView = self.overlayView {
|
|
1099
1425
|
print("Hiding call overlay view")
|
|
@@ -1120,93 +1446,52 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1120
1446
|
return
|
|
1121
1447
|
}
|
|
1122
1448
|
|
|
1123
|
-
|
|
1449
|
+
let result: [String: Any] = [
|
|
1124
1450
|
"callId": currentCallId,
|
|
1125
1451
|
"state": currentCallState
|
|
1126
|
-
]
|
|
1127
|
-
}
|
|
1452
|
+
]
|
|
1128
1453
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
call.reject("Missing required parameter: callId")
|
|
1132
|
-
return
|
|
1133
|
-
}
|
|
1454
|
+
call.resolve(result)
|
|
1455
|
+
}
|
|
1134
1456
|
|
|
1457
|
+
func getCallInfo(callId: String, activeCall: Call) async -> [String: Any] {
|
|
1135
1458
|
do {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
guard let activeCall = streamVideo?.state.activeCall, activeCall.cId == callId else {
|
|
1139
|
-
call.reject("Call ID does not match active call")
|
|
1140
|
-
return
|
|
1141
|
-
}
|
|
1459
|
+
let customRaw = await MainActor.run { activeCall.state.custom }
|
|
1142
1460
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
// Extract caller information
|
|
1149
|
-
var caller: [String: Any]?
|
|
1150
|
-
let createdBy = callInfo.call.createdBy
|
|
1151
|
-
var callerData: [String: Any] = [:]
|
|
1152
|
-
callerData["userId"] = createdBy.id
|
|
1153
|
-
callerData["name"] = createdBy.name
|
|
1154
|
-
callerData["imageURL"] = createdBy.image
|
|
1155
|
-
callerData["role"] = createdBy.role
|
|
1156
|
-
caller = callerData
|
|
1157
|
-
|
|
1158
|
-
// Extract members information
|
|
1159
|
-
var membersArray: [[String: Any]] = []
|
|
1160
|
-
let participants = await activeCall.state.participants
|
|
1161
|
-
for participant in participants {
|
|
1162
|
-
var memberData: [String: Any] = [:]
|
|
1163
|
-
memberData["userId"] = participant.userId
|
|
1164
|
-
memberData["name"] = participant.name
|
|
1165
|
-
memberData["imageURL"] = participant.profileImageURL
|
|
1166
|
-
memberData["role"] = participant.roles.first ?? ""
|
|
1167
|
-
membersArray.append(memberData)
|
|
1168
|
-
}
|
|
1169
|
-
let members = membersArray
|
|
1170
|
-
|
|
1171
|
-
// Determine call state based on current calling state
|
|
1172
|
-
let state: String
|
|
1173
|
-
let callingState = await self.callViewModel?.callingState
|
|
1174
|
-
switch callingState {
|
|
1175
|
-
case .idle:
|
|
1176
|
-
state = "idle"
|
|
1177
|
-
case .incoming:
|
|
1178
|
-
state = "ringing"
|
|
1179
|
-
case .outgoing:
|
|
1180
|
-
state = "ringing"
|
|
1181
|
-
case .inCall:
|
|
1182
|
-
state = "joined"
|
|
1183
|
-
case .lobby:
|
|
1184
|
-
state = "lobby"
|
|
1185
|
-
case .joining:
|
|
1186
|
-
state = "joining"
|
|
1187
|
-
case .reconnecting:
|
|
1188
|
-
state = "reconnecting"
|
|
1189
|
-
case .none:
|
|
1190
|
-
state = "unknown"
|
|
1191
|
-
}
|
|
1461
|
+
// Convert RawJSON dictionary to [String: Any] via JSONSerialization
|
|
1462
|
+
let custom: [String: Any]
|
|
1463
|
+
let jsonData = try JSONEncoder().encode(customRaw)
|
|
1464
|
+
custom = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] ?? [:]
|
|
1192
1465
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1466
|
+
let result: [String: Any] = [
|
|
1467
|
+
"callId": callId,
|
|
1468
|
+
"custom": custom
|
|
1469
|
+
]
|
|
1196
1470
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1471
|
+
print("Debug: Resolving getCallInfo with custom =", custom)
|
|
1472
|
+
return result
|
|
1473
|
+
} catch {
|
|
1474
|
+
print("Error resolving getCallInfo:", error)
|
|
1475
|
+
return [
|
|
1476
|
+
"callId": callId,
|
|
1477
|
+
"custom": [:]
|
|
1478
|
+
]
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1200
1481
|
|
|
1201
|
-
|
|
1482
|
+
@objc func enableBluetooth(call: CAPPluginCall) {
|
|
1483
|
+
Task {
|
|
1484
|
+
do {
|
|
1202
1485
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1486
|
+
let policy = DefaultAudioSessionPolicy()
|
|
1487
|
+
try await self.callViewModel?.call?.updateAudioSessionPolicy(policy)
|
|
1488
|
+
call.resolve([
|
|
1489
|
+
"success": true
|
|
1490
|
+
])
|
|
1491
|
+
} catch {
|
|
1492
|
+
print("Failed to update policy: \(error)")
|
|
1493
|
+
call.reject("Unable to set bluetooth policy")
|
|
1207
1494
|
}
|
|
1208
|
-
} catch {
|
|
1209
|
-
call.reject("StreamVideo not initialized")
|
|
1210
1495
|
}
|
|
1211
1496
|
}
|
|
1212
1497
|
|
|
@@ -1350,4 +1635,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1350
1635
|
}
|
|
1351
1636
|
}
|
|
1352
1637
|
|
|
1638
|
+
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
1639
|
+
call.resolve(["version": self.pluginVersion])
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1353
1642
|
}
|