@capgo/capacitor-stream-call 7.1.31 → 7.4.0
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 +82 -10
- 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 +2937 -2464
- package/android/src/main/res/raw/outgoing.mp3 +0 -0
- package/dist/docs.json +185 -1
- package/dist/esm/definitions.d.ts +55 -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 +420 -136
- 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 PLUGIN_VERSION: String = "7.4.0"
|
|
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,6 +64,7 @@ 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
|
|
@@ -188,9 +194,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
188
194
|
DispatchQueue.main.async { [weak self] in
|
|
189
195
|
guard let self = self else { return }
|
|
190
196
|
|
|
191
|
-
// Cancel existing
|
|
197
|
+
// Cancel existing subscriptions if any
|
|
192
198
|
self.activeCallSubscription?.cancel()
|
|
193
199
|
self.activeCallSubscription = nil
|
|
200
|
+
self.speakerSubscription?.cancel()
|
|
201
|
+
self.speakerSubscription = nil
|
|
194
202
|
|
|
195
203
|
// Verify callViewModel exists
|
|
196
204
|
guard let callViewModel = self.callViewModel, let streamVideo = self.streamVideo else {
|
|
@@ -208,6 +216,59 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
208
216
|
// while the subscription is active
|
|
209
217
|
let viewModel = callViewModel
|
|
210
218
|
|
|
219
|
+
let callEvents = streamVideo
|
|
220
|
+
.eventPublisher()
|
|
221
|
+
.sink { [weak self] event in
|
|
222
|
+
guard let self else { return }
|
|
223
|
+
switch event {
|
|
224
|
+
case let .typeCallRejectedEvent(response):
|
|
225
|
+
let data: [String: Any] = [
|
|
226
|
+
"callId": response.callCid,
|
|
227
|
+
"state": "rejected"
|
|
228
|
+
]
|
|
229
|
+
notifyListeners("callEvent", data: data)
|
|
230
|
+
case let .typeCallAcceptedEvent(response):
|
|
231
|
+
if let streamUserId = self.streamVideo?.user.id,
|
|
232
|
+
response.user.id == streamUserId,
|
|
233
|
+
response.callCid != self.currentCallId {
|
|
234
|
+
|
|
235
|
+
self.updateCallStatusAndNotify(callId: response.callCid, state: "joined")
|
|
236
|
+
}
|
|
237
|
+
case let .typeCallEndedEvent(response):
|
|
238
|
+
let data: [String: Any] = [
|
|
239
|
+
"callId": response.callCid,
|
|
240
|
+
"state": "left"
|
|
241
|
+
]
|
|
242
|
+
notifyListeners("callEvent", data: data)
|
|
243
|
+
|
|
244
|
+
case let .typeCallSessionParticipantCountsUpdatedEvent(response):
|
|
245
|
+
let activeCall = streamVideo.state.activeCall
|
|
246
|
+
let callDropped = self.currentCallId == response.callCid && activeCall == nil
|
|
247
|
+
let onlyOneParticipant = activeCall?.cId == response.callCid && activeCall?.state.participantCount == 1
|
|
248
|
+
if onlyOneParticipant || callDropped {
|
|
249
|
+
self.endCallInternal()
|
|
250
|
+
} else {
|
|
251
|
+
print("""
|
|
252
|
+
onlyOneParticipant check:
|
|
253
|
+
- activeCall?.cId: \(String(describing: activeCall?.cId))
|
|
254
|
+
- response.callCid: \(response.callCid)
|
|
255
|
+
- activeCall?.state.participantCount: \(String(describing: activeCall?.state.participantCount))
|
|
256
|
+
- Result (onlyOneParticipant): \(onlyOneParticipant)
|
|
257
|
+
""")
|
|
258
|
+
}
|
|
259
|
+
if let count = activeCall?.state.participantCount {
|
|
260
|
+
let data: [String: Any] = [
|
|
261
|
+
"callId": response.callCid,
|
|
262
|
+
"state": "participant_counts",
|
|
263
|
+
"count": count
|
|
264
|
+
]
|
|
265
|
+
notifyListeners("callEvent", data: data)
|
|
266
|
+
}
|
|
267
|
+
default:
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
211
272
|
// Subscribe to streamVideo.state.$activeCall to handle CallKit integration
|
|
212
273
|
let callPublisher = streamVideo.state.$activeCall
|
|
213
274
|
.receive(on: DispatchQueue.main)
|
|
@@ -220,6 +281,41 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
220
281
|
// Sync callViewModel with activeCall from streamVideo state
|
|
221
282
|
// This ensures CallKit integration works properly
|
|
222
283
|
viewModel.setActiveCall(activeCall)
|
|
284
|
+
viewModel.update(participantsLayout: .grid)
|
|
285
|
+
|
|
286
|
+
// Subscribe to speaker status for this active call
|
|
287
|
+
self.speakerSubscription = activeCall.speaker.$status
|
|
288
|
+
.receive(on: DispatchQueue.main)
|
|
289
|
+
.sink { [weak self] speakerStatus in
|
|
290
|
+
guard let self = self else { return }
|
|
291
|
+
|
|
292
|
+
// Only emit if the current active call matches our current call ID
|
|
293
|
+
guard activeCall.cId == self.currentCallId else {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
print("Speaker status update: \(speakerStatus)")
|
|
298
|
+
|
|
299
|
+
let state: String
|
|
300
|
+
switch speakerStatus {
|
|
301
|
+
case .enabled:
|
|
302
|
+
state = "speaker_enabled"
|
|
303
|
+
case .disabled:
|
|
304
|
+
state = "speaker_disabled"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let data: [String: Any] = [
|
|
308
|
+
"callId": self.currentCallId,
|
|
309
|
+
"state": state
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
self.notifyListeners("callEvent", data: data)
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// Clean up speaker subscription when activeCall becomes nil
|
|
316
|
+
print("Active call became nil, cleaning up speaker subscription")
|
|
317
|
+
self.speakerSubscription?.cancel()
|
|
318
|
+
self.speakerSubscription = nil
|
|
223
319
|
}
|
|
224
320
|
}
|
|
225
321
|
|
|
@@ -238,6 +334,50 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
238
334
|
do {
|
|
239
335
|
try self.requireInitialized()
|
|
240
336
|
print("Call State Update: \(newState)")
|
|
337
|
+
// Check if transitioning from outgoing to idle
|
|
338
|
+
if newState == .idle,
|
|
339
|
+
let previousState = self.previousCallingState,
|
|
340
|
+
case .outgoing = previousState {
|
|
341
|
+
print("Call state changed from outgoing to idle. Ending call with ID: \(self.currentCallId)")
|
|
342
|
+
|
|
343
|
+
// End the call using the stored call ID and type
|
|
344
|
+
if !self.currentCallId.isEmpty {
|
|
345
|
+
Task {
|
|
346
|
+
do {
|
|
347
|
+
// Parse call type and id from currentCallId format "type:id"
|
|
348
|
+
let components = self.currentCallId.components(separatedBy: ":")
|
|
349
|
+
if components.count >= 2 {
|
|
350
|
+
let callType = components[0]
|
|
351
|
+
let callId = components[1]
|
|
352
|
+
|
|
353
|
+
// Try to get the call and end it using the parsed call type
|
|
354
|
+
if let streamVideo = self.streamVideo {
|
|
355
|
+
let call = streamVideo.call(callType: callType, callId: callId)
|
|
356
|
+
try await call.end()
|
|
357
|
+
print("Successfully ended outgoing call: \(callId) with type: \(callType)")
|
|
358
|
+
|
|
359
|
+
let data: [String: Any] = [
|
|
360
|
+
"callId": call.cId,
|
|
361
|
+
"state": "outgoing_call_ended"
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
self.notifyListeners("callEvent", data: data)
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
print("Invalid call ID format: \(self.currentCallId)")
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
print("Error ending outgoing call \(self.currentCallId): \(error)")
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Clean up stored call information
|
|
374
|
+
DispatchQueue.main.async {
|
|
375
|
+
self.currentCallId = ""
|
|
376
|
+
print("Cleaned up call information after ending outgoing call")
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
241
381
|
|
|
242
382
|
if newState == .inCall {
|
|
243
383
|
print("- In call state detected")
|
|
@@ -255,50 +395,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
255
395
|
self.updateCallStatusAndNotify(callId: callId, state: "joined")
|
|
256
396
|
self.hasNotifiedCallJoined = true
|
|
257
397
|
}
|
|
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
398
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
399
|
+
} else if case .incoming(let incomingCall) = newState {
|
|
400
|
+
self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing")
|
|
401
|
+
// }
|
|
302
402
|
} else if newState == .idle {
|
|
303
403
|
print("Call state changed to idle. CurrentCallId: \(self.currentCallId), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
|
|
304
404
|
|
|
@@ -313,21 +413,26 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
313
413
|
// Reset notification flag when call ends
|
|
314
414
|
self.hasNotifiedCallJoined = false
|
|
315
415
|
|
|
316
|
-
// Remove the call overlay view when not in a call
|
|
416
|
+
// Remove the call overlay view and touch intercept view when not in a call
|
|
317
417
|
self.ensureViewRemoved()
|
|
418
|
+
|
|
318
419
|
} else {
|
|
319
420
|
print("Not sending left event - CurrentCallId: \(self.currentCallId), ActiveCall exists: \(self.streamVideo?.state.activeCall != nil)")
|
|
320
421
|
}
|
|
321
422
|
}
|
|
423
|
+
|
|
424
|
+
// Update the previous state for next comparison
|
|
425
|
+
self.previousCallingState = newState
|
|
322
426
|
} catch {
|
|
323
427
|
log.error("Error handling call state update: \(String(describing: error))")
|
|
324
428
|
}
|
|
325
429
|
}
|
|
326
430
|
|
|
327
|
-
// Combine
|
|
431
|
+
// Combine all publishers
|
|
328
432
|
self.activeCallSubscription = AnyCancellable {
|
|
329
433
|
callPublisher.cancel()
|
|
330
434
|
statePublisher.cancel()
|
|
435
|
+
callEvents.cancel()
|
|
331
436
|
}
|
|
332
437
|
|
|
333
438
|
print("Active call subscription setup completed")
|
|
@@ -421,9 +526,20 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
421
526
|
tokenSubscription = nil
|
|
422
527
|
activeCallSubscription?.cancel()
|
|
423
528
|
activeCallSubscription = nil
|
|
529
|
+
speakerSubscription?.cancel()
|
|
530
|
+
speakerSubscription = nil
|
|
424
531
|
lastVoIPToken = nil
|
|
532
|
+
state = .notInitialized
|
|
425
533
|
|
|
426
534
|
SecureUserRepository.shared.removeCurrentUser()
|
|
535
|
+
self.callViewModel = nil
|
|
536
|
+
self.overlayView = nil
|
|
537
|
+
|
|
538
|
+
// Clean up touch intercept view
|
|
539
|
+
if let touchInterceptView = self.touchInterceptView {
|
|
540
|
+
touchInterceptView.removeFromSuperview()
|
|
541
|
+
self.touchInterceptView = nil
|
|
542
|
+
}
|
|
427
543
|
|
|
428
544
|
// Update the CallOverlayView with nil StreamVideo instance
|
|
429
545
|
Task { @MainActor in
|
|
@@ -457,6 +573,85 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
457
573
|
}
|
|
458
574
|
}
|
|
459
575
|
|
|
576
|
+
@objc func toggleViews(_ call: CAPPluginCall) {
|
|
577
|
+
Task { @MainActor in
|
|
578
|
+
guard let viewModel = self.callViewModel else {
|
|
579
|
+
call.reject("ViewModel is not initialized.")
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let layouts: [ParticipantsLayout] = [.spotlight, .fullScreen, .grid]
|
|
584
|
+
let currentLayout = viewModel.participantsLayout
|
|
585
|
+
|
|
586
|
+
// If currentLayout is not in layouts, default to .grid index
|
|
587
|
+
let currentIndex = layouts.firstIndex(of: currentLayout) ?? layouts.firstIndex(of: .grid) ?? 0
|
|
588
|
+
let nextIndex = (currentIndex + 1) % layouts.count
|
|
589
|
+
let nextLayout = layouts[nextIndex]
|
|
590
|
+
|
|
591
|
+
viewModel.update(participantsLayout: nextLayout)
|
|
592
|
+
|
|
593
|
+
call.resolve([
|
|
594
|
+
"newLayout": "\(nextLayout)"
|
|
595
|
+
])
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
@objc func joinCall(_ call: CAPPluginCall) {
|
|
600
|
+
guard let callId = call.getString("callId") else {
|
|
601
|
+
call.reject("Missing required parameter: callId")
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
guard let callType = call.getString("callType") else {
|
|
606
|
+
call.reject("Missing required parameter: callType")
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Initialize if needed
|
|
611
|
+
if state == .notInitialized {
|
|
612
|
+
initializeStreamVideo()
|
|
613
|
+
if state != .initialized {
|
|
614
|
+
call.reject("Failed to initialize StreamVideo")
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
do {
|
|
620
|
+
try requireInitialized()
|
|
621
|
+
|
|
622
|
+
Task {
|
|
623
|
+
do {
|
|
624
|
+
print("Joining call:")
|
|
625
|
+
print("- Call ID: \(callId)")
|
|
626
|
+
print("- Call Type: \(callType)")
|
|
627
|
+
|
|
628
|
+
// Create the call object
|
|
629
|
+
await self.callViewModel?.joinCall(
|
|
630
|
+
callType: callType,
|
|
631
|
+
callId: callId
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
// Now send the created event with complete member data
|
|
635
|
+
self.updateCallStatusAndNotify(callId: callId, state: "joined")
|
|
636
|
+
|
|
637
|
+
// Update UI on main thread
|
|
638
|
+
await MainActor.run {
|
|
639
|
+
// self.overlayViewModel?.updateCall(streamCall)
|
|
640
|
+
self.overlayView?.isHidden = false
|
|
641
|
+
self.webView?.isOpaque = false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
call.resolve([
|
|
645
|
+
"success": true
|
|
646
|
+
])
|
|
647
|
+
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} catch {
|
|
651
|
+
call.reject("StreamVideo not initialized")
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
460
655
|
@objc func call(_ call: CAPPluginCall) {
|
|
461
656
|
guard let members = call.getArray("userIds", String.self) else {
|
|
462
657
|
call.reject("Missing required parameter: userIds (array of user IDs)")
|
|
@@ -625,6 +820,90 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
625
820
|
}
|
|
626
821
|
}
|
|
627
822
|
|
|
823
|
+
private func endCallInternal() {
|
|
824
|
+
do {
|
|
825
|
+
try requireInitialized()
|
|
826
|
+
|
|
827
|
+
Task {
|
|
828
|
+
// Check both active call and callViewModel's call state to handle outgoing calls
|
|
829
|
+
let activeCall = streamVideo?.state.activeCall
|
|
830
|
+
let viewModelCall = await callViewModel?.call
|
|
831
|
+
|
|
832
|
+
// Helper function to determine if we should end or leave the call
|
|
833
|
+
func shouldEndCall(for streamCall: Call) async throws -> Bool {
|
|
834
|
+
do {
|
|
835
|
+
let callInfo = try await streamCall.get()
|
|
836
|
+
let currentUserId = streamVideo?.user.id
|
|
837
|
+
let createdBy = callInfo.call.createdBy.id
|
|
838
|
+
let isCreator = createdBy == currentUserId
|
|
839
|
+
|
|
840
|
+
// Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
|
|
841
|
+
// let totalParticipants = await streamCall.state.participants.count
|
|
842
|
+
let forceEnd = await (activeCall?.state.custom["type"]?.stringValue == "direct")
|
|
843
|
+
let shouldEnd = isCreator || forceEnd
|
|
844
|
+
|
|
845
|
+
print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), ShouldEnd: \(shouldEnd)")
|
|
846
|
+
|
|
847
|
+
return shouldEnd
|
|
848
|
+
} catch {
|
|
849
|
+
print("Error getting call info for \(streamCall.cId), defaulting to leave: \(error)")
|
|
850
|
+
return false // Fallback to leave if we can't determine
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if let activeCall = activeCall {
|
|
855
|
+
// There's an active call, check if we should end or leave
|
|
856
|
+
do {
|
|
857
|
+
let shouldEnd = try await shouldEndCall(for: activeCall)
|
|
858
|
+
|
|
859
|
+
if shouldEnd {
|
|
860
|
+
print("Ending active call \(activeCall.cId) for all participants")
|
|
861
|
+
try await activeCall.end()
|
|
862
|
+
} else {
|
|
863
|
+
print("Leaving active call \(activeCall.cId)")
|
|
864
|
+
try await activeCall.leave()
|
|
865
|
+
}
|
|
866
|
+
} catch {
|
|
867
|
+
print("Error ending/leaving active call: \(error)")
|
|
868
|
+
try await activeCall.leave() // Fallback to leave
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
await MainActor.run {
|
|
872
|
+
self.overlayView?.isHidden = true
|
|
873
|
+
self.webView?.isOpaque = true
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
} else if let viewModelCall = viewModelCall {
|
|
877
|
+
// There's a call in the viewModel (likely outgoing/ringing), check if we should end or leave
|
|
878
|
+
do {
|
|
879
|
+
let shouldEnd = try await shouldEndCall(for: viewModelCall)
|
|
880
|
+
|
|
881
|
+
if shouldEnd {
|
|
882
|
+
print("Ending viewModel call \(viewModelCall.cId) for all participants")
|
|
883
|
+
try await viewModelCall.end()
|
|
884
|
+
} else {
|
|
885
|
+
print("Leaving viewModel call \(viewModelCall.cId)")
|
|
886
|
+
try await viewModelCall.leave()
|
|
887
|
+
}
|
|
888
|
+
} catch {
|
|
889
|
+
print("Error ending/leaving viewModel call: \(error)")
|
|
890
|
+
try await viewModelCall.leave() // Fallback to leave
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Also hang up to reset the calling state
|
|
894
|
+
await callViewModel?.hangUp()
|
|
895
|
+
|
|
896
|
+
await MainActor.run {
|
|
897
|
+
self.overlayView?.isHidden = true
|
|
898
|
+
self.webView?.isOpaque = true
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
log.error("StreamVideo not initialized")
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
628
907
|
@objc func endCall(_ call: CAPPluginCall) {
|
|
629
908
|
do {
|
|
630
909
|
try requireInitialized()
|
|
@@ -641,10 +920,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
641
920
|
let currentUserId = streamVideo?.user.id
|
|
642
921
|
let createdBy = callInfo.call.createdBy.id
|
|
643
922
|
let isCreator = createdBy == currentUserId
|
|
923
|
+
let forceEnd = await (activeCall?.state.custom["type"]?.stringValue == "direct")
|
|
644
924
|
|
|
645
925
|
// Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
|
|
646
926
|
let totalParticipants = await streamCall.state.participants.count
|
|
647
|
-
let shouldEnd = isCreator ||
|
|
927
|
+
let shouldEnd = isCreator || forceEnd
|
|
648
928
|
|
|
649
929
|
print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), TotalParticipants: \(totalParticipants), ShouldEnd: \(shouldEnd)")
|
|
650
930
|
|
|
@@ -735,10 +1015,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
735
1015
|
Task {
|
|
736
1016
|
do {
|
|
737
1017
|
if let activeCall = streamVideo?.state.activeCall {
|
|
738
|
-
if enabled {
|
|
739
|
-
try await activeCall.microphone.
|
|
740
|
-
} else {
|
|
741
|
-
try await activeCall.microphone.
|
|
1018
|
+
if enabled && activeCall.microphone.status == .disabled {
|
|
1019
|
+
try await activeCall.microphone.toggle()
|
|
1020
|
+
} else if !enabled && activeCall.microphone.status == .enabled {
|
|
1021
|
+
try await activeCall.microphone.toggle()
|
|
742
1022
|
}
|
|
743
1023
|
call.resolve([
|
|
744
1024
|
"success": true
|
|
@@ -768,10 +1048,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
768
1048
|
Task {
|
|
769
1049
|
do {
|
|
770
1050
|
if let activeCall = streamVideo?.state.activeCall {
|
|
771
|
-
if enabled {
|
|
772
|
-
try await activeCall.camera.
|
|
773
|
-
} else {
|
|
774
|
-
try await activeCall.camera.
|
|
1051
|
+
if enabled && activeCall.camera.status == .disabled {
|
|
1052
|
+
try await activeCall.camera.toggle()
|
|
1053
|
+
} else if !enabled && activeCall.camera.status == .enabled {
|
|
1054
|
+
try await activeCall.camera.toggle()
|
|
775
1055
|
}
|
|
776
1056
|
call.resolve([
|
|
777
1057
|
"success": true
|
|
@@ -896,7 +1176,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
896
1176
|
Task { @MainActor in
|
|
897
1177
|
self.callViewModel = CallViewModel(participantsLayout: .grid)
|
|
898
1178
|
// self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
|
|
899
|
-
|
|
900
1179
|
// Setup subscriptions for new StreamVideo instance
|
|
901
1180
|
self.setupActiveCallSubscription()
|
|
902
1181
|
}
|
|
@@ -905,7 +1184,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
905
1184
|
|
|
906
1185
|
state = .initialized
|
|
907
1186
|
callKitAdapter.streamVideo = self.streamVideo
|
|
908
|
-
callKitAdapter.availabilityPolicy = .
|
|
1187
|
+
callKitAdapter.availabilityPolicy = .regionBased
|
|
909
1188
|
|
|
910
1189
|
setupTokenSubscription()
|
|
911
1190
|
|
|
@@ -1035,6 +1314,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1035
1314
|
webView.isOpaque = false
|
|
1036
1315
|
webView.backgroundColor = .clear
|
|
1037
1316
|
webView.scrollView.backgroundColor = .clear
|
|
1317
|
+
|
|
1318
|
+
// Add touch intercept view when making existing overlay visible
|
|
1319
|
+
addTouchInterceptView(webView: webView, parent: parent, overlayView: existingOverlayView)
|
|
1038
1320
|
return
|
|
1039
1321
|
}
|
|
1040
1322
|
|
|
@@ -1067,8 +1349,38 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1067
1349
|
self.hostingController = overlayView
|
|
1068
1350
|
self.overlayView = overlayView.view
|
|
1069
1351
|
|
|
1070
|
-
// Add touch
|
|
1071
|
-
|
|
1352
|
+
// Add touch intercept view for the new overlay
|
|
1353
|
+
addTouchInterceptView(webView: webView, parent: parent, overlayView: overlayView.view)
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
private func addTouchInterceptView(webView: WKWebView, parent: UIView, overlayView: UIView) {
|
|
1357
|
+
// Create touch intercept view
|
|
1358
|
+
let touchInterceptView = TouchInterceptView(frame: parent.bounds)
|
|
1359
|
+
touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
|
|
1360
|
+
touchInterceptView.backgroundColor = .clear
|
|
1361
|
+
touchInterceptView.isOpaque = false
|
|
1362
|
+
touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
|
|
1363
|
+
|
|
1364
|
+
// Add to parent view
|
|
1365
|
+
parent.addSubview(touchInterceptView)
|
|
1366
|
+
|
|
1367
|
+
// Set up active call check function
|
|
1368
|
+
touchInterceptView.setActiveCallCheck { [weak self] in
|
|
1369
|
+
return self?.streamVideo?.state.activeCall != nil
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Store reference and set call active
|
|
1373
|
+
self.touchInterceptView = touchInterceptView
|
|
1374
|
+
|
|
1375
|
+
// Setup constraints for touchInterceptView to cover the entire parent
|
|
1376
|
+
NSLayoutConstraint.activate([
|
|
1377
|
+
touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
|
|
1378
|
+
touchInterceptView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
|
|
1379
|
+
touchInterceptView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
|
|
1380
|
+
touchInterceptView.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
|
|
1381
|
+
])
|
|
1382
|
+
|
|
1383
|
+
print("Touch intercept view added and activated for call")
|
|
1072
1384
|
}
|
|
1073
1385
|
|
|
1074
1386
|
// MARK: - Dynamic API Key Management
|
|
@@ -1094,6 +1406,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1094
1406
|
}
|
|
1095
1407
|
|
|
1096
1408
|
func ensureViewRemoved() {
|
|
1409
|
+
print("Removing call views and touch intercept")
|
|
1410
|
+
|
|
1411
|
+
// Remove touch intercept view
|
|
1412
|
+
if let touchInterceptView = self.touchInterceptView {
|
|
1413
|
+
print("Removing touch intercept view")
|
|
1414
|
+
touchInterceptView.removeFromSuperview()
|
|
1415
|
+
self.touchInterceptView = nil
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1097
1418
|
// Check if we have an overlay view
|
|
1098
1419
|
if let existingOverlayView = self.overlayView {
|
|
1099
1420
|
print("Hiding call overlay view")
|
|
@@ -1120,93 +1441,52 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1120
1441
|
return
|
|
1121
1442
|
}
|
|
1122
1443
|
|
|
1123
|
-
|
|
1444
|
+
let result: [String: Any] = [
|
|
1124
1445
|
"callId": currentCallId,
|
|
1125
1446
|
"state": currentCallState
|
|
1126
|
-
]
|
|
1127
|
-
}
|
|
1447
|
+
]
|
|
1128
1448
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
call.reject("Missing required parameter: callId")
|
|
1132
|
-
return
|
|
1133
|
-
}
|
|
1449
|
+
call.resolve(result)
|
|
1450
|
+
}
|
|
1134
1451
|
|
|
1452
|
+
func getCallInfo(callId: String, activeCall: Call) async -> [String: Any] {
|
|
1135
1453
|
do {
|
|
1136
|
-
|
|
1454
|
+
let customRaw = await MainActor.run { activeCall.state.custom }
|
|
1137
1455
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1456
|
+
// Convert RawJSON dictionary to [String: Any] via JSONSerialization
|
|
1457
|
+
let custom: [String: Any]
|
|
1458
|
+
let jsonData = try JSONEncoder().encode(customRaw)
|
|
1459
|
+
custom = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] ?? [:]
|
|
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
|
+
let result: [String: Any] = [
|
|
1462
|
+
"callId": callId,
|
|
1463
|
+
"custom": custom
|
|
1464
|
+
]
|
|
1192
1465
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1466
|
+
print("Debug: Resolving getCallInfo with custom =", custom)
|
|
1467
|
+
return result
|
|
1468
|
+
} catch {
|
|
1469
|
+
print("Error resolving getCallInfo:", error)
|
|
1470
|
+
return [
|
|
1471
|
+
"callId": callId,
|
|
1472
|
+
"custom": [:]
|
|
1473
|
+
]
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1200
1476
|
|
|
1201
|
-
|
|
1477
|
+
@objc func enableBluetooth(call: CAPPluginCall) {
|
|
1478
|
+
Task {
|
|
1479
|
+
do {
|
|
1202
1480
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1481
|
+
let policy = DefaultAudioSessionPolicy()
|
|
1482
|
+
try await self.callViewModel?.call?.updateAudioSessionPolicy(policy)
|
|
1483
|
+
call.resolve([
|
|
1484
|
+
"success": true
|
|
1485
|
+
])
|
|
1486
|
+
} catch {
|
|
1487
|
+
print("Failed to update policy: \(error)")
|
|
1488
|
+
call.reject("Unable to set bluetooth policy")
|
|
1207
1489
|
}
|
|
1208
|
-
} catch {
|
|
1209
|
-
call.reject("StreamVideo not initialized")
|
|
1210
1490
|
}
|
|
1211
1491
|
}
|
|
1212
1492
|
|
|
@@ -1350,4 +1630,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1350
1630
|
}
|
|
1351
1631
|
}
|
|
1352
1632
|
|
|
1633
|
+
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
1634
|
+
call.resolve(["version": self.PLUGIN_VERSION])
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1353
1637
|
}
|