@capgo/capacitor-stream-call 0.0.43 → 0.0.51
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 +110 -106
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +184 -33
- package/dist/docs.json +124 -0
- package/dist/esm/definitions.d.ts +34 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -0
- package/dist/esm/web.js +87 -4
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +87 -4
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +87 -4
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +268 -24
- package/package.json +1 -1
|
@@ -26,7 +26,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
26
26
|
CAPPluginMethod(name: "isCameraEnabled", returnType: CAPPluginReturnPromise),
|
|
27
27
|
CAPPluginMethod(name: "getCallStatus", returnType: CAPPluginReturnPromise),
|
|
28
28
|
CAPPluginMethod(name: "setSpeaker", returnType: CAPPluginReturnPromise),
|
|
29
|
-
CAPPluginMethod(name: "switchCamera", returnType: CAPPluginReturnPromise)
|
|
29
|
+
CAPPluginMethod(name: "switchCamera", returnType: CAPPluginReturnPromise),
|
|
30
|
+
CAPPluginMethod(name: "getCallInfo", returnType: CAPPluginReturnPromise)
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
private enum State {
|
|
@@ -60,7 +61,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
60
61
|
private var callViewModel: CallViewModel?
|
|
61
62
|
|
|
62
63
|
// Helper method to update call status and notify listeners
|
|
63
|
-
private func updateCallStatusAndNotify(callId: String, state: String, userId: String? = nil, reason: String? = nil) {
|
|
64
|
+
private func updateCallStatusAndNotify(callId: String, state: String, userId: String? = nil, reason: String? = nil, caller: [String: Any]? = nil, members: [[String: Any]]? = nil) {
|
|
64
65
|
// Update stored call info
|
|
65
66
|
currentCallId = callId
|
|
66
67
|
currentCallState = state
|
|
@@ -78,6 +79,14 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
78
79
|
if let reason = reason {
|
|
79
80
|
data["reason"] = reason
|
|
80
81
|
}
|
|
82
|
+
|
|
83
|
+
if let caller = caller {
|
|
84
|
+
data["caller"] = caller
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if let members = members {
|
|
88
|
+
data["members"] = members
|
|
89
|
+
}
|
|
81
90
|
|
|
82
91
|
// Notify listeners
|
|
83
92
|
notifyListeners("callEvent", data: data)
|
|
@@ -231,20 +240,69 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
231
240
|
self.hasNotifiedCallJoined = true
|
|
232
241
|
}
|
|
233
242
|
} else if case .incoming(let incomingCall) = newState {
|
|
234
|
-
|
|
235
|
-
|
|
243
|
+
// Extract caller information
|
|
244
|
+
Task {
|
|
245
|
+
var caller: [String: Any]? = nil
|
|
246
|
+
var members: [[String: Any]]? = nil
|
|
247
|
+
|
|
248
|
+
do {
|
|
249
|
+
// Get the call from StreamVideo to access detailed information
|
|
250
|
+
if let streamVideo = self.streamVideo {
|
|
251
|
+
let call = streamVideo.call(callType: incomingCall.type, callId: incomingCall.id)
|
|
252
|
+
let callInfo = try await call.get()
|
|
253
|
+
|
|
254
|
+
// Extract caller information
|
|
255
|
+
let createdBy = callInfo.call.createdBy
|
|
256
|
+
var callerData: [String: Any] = [:]
|
|
257
|
+
callerData["userId"] = createdBy.id
|
|
258
|
+
callerData["name"] = createdBy.name
|
|
259
|
+
callerData["imageURL"] = createdBy.image
|
|
260
|
+
callerData["role"] = createdBy.role
|
|
261
|
+
caller = callerData
|
|
262
|
+
|
|
263
|
+
// Extract members information from current participants if available
|
|
264
|
+
var membersArray: [[String: Any]] = []
|
|
265
|
+
if let activeCall = streamVideo.state.activeCall {
|
|
266
|
+
let participants = activeCall.state.participants
|
|
267
|
+
for participant in participants {
|
|
268
|
+
var memberData: [String: Any] = [:]
|
|
269
|
+
memberData["userId"] = participant.userId
|
|
270
|
+
memberData["name"] = participant.name
|
|
271
|
+
memberData["imageURL"] = participant.profileImageURL?.absoluteString ?? ""
|
|
272
|
+
memberData["role"] = participant.roles.first ?? ""
|
|
273
|
+
membersArray.append(memberData)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
members = membersArray.isEmpty ? nil : membersArray
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
print("Failed to get call info for caller details: \(error)")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Notify with caller information
|
|
283
|
+
self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing", caller: caller, members: members)
|
|
284
|
+
}
|
|
285
|
+
} else if newState == .idle {
|
|
236
286
|
// Get the call ID that was active before the state changed
|
|
237
287
|
let endingCallId = viewModel.call?.cId
|
|
238
|
-
print("Call
|
|
239
|
-
|
|
240
|
-
// Notify that call has ended - use the properly tracked call ID
|
|
241
|
-
self.updateCallStatusAndNotify(callId: endingCallId ?? "", state: "left")
|
|
288
|
+
print("Call state changed to idle. EndingCallId: \(String(describing: endingCallId)), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
|
|
242
289
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
290
|
+
// Only notify about call ending if we have a valid call ID and there's truly no active call
|
|
291
|
+
// This prevents false "left" events during normal state transitions
|
|
292
|
+
if let callId = endingCallId, !callId.isEmpty, self.streamVideo?.state.activeCall == nil {
|
|
293
|
+
print("Call actually ending: \(callId)")
|
|
294
|
+
|
|
295
|
+
// Notify that call has ended - use the properly tracked call ID
|
|
296
|
+
self.updateCallStatusAndNotify(callId: callId, state: "left")
|
|
297
|
+
|
|
298
|
+
// Reset notification flag when call ends
|
|
299
|
+
self.hasNotifiedCallJoined = false
|
|
300
|
+
|
|
301
|
+
// Remove the call overlay view when not in a call
|
|
302
|
+
self.ensureViewRemoved()
|
|
303
|
+
} else {
|
|
304
|
+
print("Not sending left event - CallId: \(String(describing: endingCallId)), ActiveCall exists: \(self.streamVideo?.state.activeCall != nil)")
|
|
305
|
+
}
|
|
248
306
|
}
|
|
249
307
|
} catch {
|
|
250
308
|
log.error("Error handling call state update: \(String(describing: error))")
|
|
@@ -409,9 +467,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
409
467
|
print("- Should Ring: \(shouldRing)")
|
|
410
468
|
print("- Team: \(team)")
|
|
411
469
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
// Update the CallOverlayView with the active call
|
|
415
470
|
// Create the call object
|
|
416
471
|
await self.callViewModel?.startCall(
|
|
417
472
|
callType: callType,
|
|
@@ -420,6 +475,46 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
420
475
|
ring: shouldRing
|
|
421
476
|
)
|
|
422
477
|
|
|
478
|
+
// Wait for call state to be populated by WebSocket events
|
|
479
|
+
let callStream = streamVideo!.call(callType: callType, callId: callId)
|
|
480
|
+
|
|
481
|
+
// Wait until we have member data - with timeout to prevent infinite loop
|
|
482
|
+
var allMembers: [[String: Any]] = []
|
|
483
|
+
var attempts = 0
|
|
484
|
+
let maxAttempts = 50 // 5 seconds max
|
|
485
|
+
|
|
486
|
+
while allMembers.isEmpty && attempts < maxAttempts {
|
|
487
|
+
let membersList = await callStream.state.members
|
|
488
|
+
if !membersList.isEmpty {
|
|
489
|
+
allMembers = membersList.map { member in
|
|
490
|
+
[
|
|
491
|
+
"userId": member.user.id,
|
|
492
|
+
"name": member.user.name,
|
|
493
|
+
"imageURL": member.user.imageURL?.absoluteString ?? "",
|
|
494
|
+
"role": member.user.role
|
|
495
|
+
]
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
attempts += 1
|
|
499
|
+
// Wait a bit and try again
|
|
500
|
+
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// If we still don't have members after timeout, use basic data
|
|
505
|
+
if allMembers.isEmpty {
|
|
506
|
+
allMembers = members.map { userId in
|
|
507
|
+
[
|
|
508
|
+
"userId": userId,
|
|
509
|
+
"name": "",
|
|
510
|
+
"imageURL": "",
|
|
511
|
+
"role": ""
|
|
512
|
+
]
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Now send the created event with complete member data
|
|
517
|
+
self.updateCallStatusAndNotify(callId: callId, state: "created", members: allMembers)
|
|
423
518
|
|
|
424
519
|
// Update UI on main thread
|
|
425
520
|
await MainActor.run {
|
|
@@ -430,7 +525,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
430
525
|
|
|
431
526
|
call.resolve([
|
|
432
527
|
"success": true
|
|
433
|
-
])
|
|
528
|
+
])
|
|
434
529
|
|
|
435
530
|
} catch {
|
|
436
531
|
log.error("Error making call: \(String(describing: error))")
|
|
@@ -447,16 +542,81 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
447
542
|
try requireInitialized()
|
|
448
543
|
|
|
449
544
|
Task {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
545
|
+
// Check both active call and callViewModel's call state to handle outgoing calls
|
|
546
|
+
let activeCall = streamVideo?.state.activeCall
|
|
547
|
+
let viewModelCall = await callViewModel?.call
|
|
548
|
+
|
|
549
|
+
// Helper function to determine if we should end or leave the call
|
|
550
|
+
func shouldEndCall(for streamCall: Call) async throws -> Bool {
|
|
551
|
+
do {
|
|
552
|
+
let callInfo = try await streamCall.get()
|
|
553
|
+
let currentUserId = streamVideo?.user.id
|
|
554
|
+
let createdBy = callInfo.call.createdBy.id
|
|
555
|
+
let isCreator = createdBy == currentUserId
|
|
556
|
+
|
|
557
|
+
// Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
|
|
558
|
+
let totalParticipants = await streamCall.state.participants.count
|
|
559
|
+
let shouldEnd = isCreator || totalParticipants <= 2
|
|
560
|
+
|
|
561
|
+
print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), TotalParticipants: \(totalParticipants), ShouldEnd: \(shouldEnd)")
|
|
562
|
+
|
|
563
|
+
return shouldEnd
|
|
564
|
+
} catch {
|
|
565
|
+
print("Error getting call info for \(streamCall.cId), defaulting to leave: \(error)")
|
|
566
|
+
return false // Fallback to leave if we can't determine
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if let activeCall = activeCall {
|
|
571
|
+
// There's an active call, check if we should end or leave
|
|
572
|
+
do {
|
|
573
|
+
let shouldEnd = try await shouldEndCall(for: activeCall)
|
|
574
|
+
|
|
575
|
+
if shouldEnd {
|
|
576
|
+
print("Ending active call \(activeCall.cId) for all participants")
|
|
577
|
+
try await activeCall.end()
|
|
578
|
+
} else {
|
|
579
|
+
print("Leaving active call \(activeCall.cId)")
|
|
580
|
+
try await activeCall.leave()
|
|
581
|
+
}
|
|
582
|
+
} catch {
|
|
583
|
+
print("Error ending/leaving active call: \(error)")
|
|
584
|
+
try await activeCall.leave() // Fallback to leave
|
|
585
|
+
}
|
|
586
|
+
|
|
454
587
|
await MainActor.run {
|
|
455
|
-
// self.overlayViewModel?.updateCall(nil)
|
|
456
588
|
self.overlayView?.isHidden = true
|
|
457
589
|
self.webView?.isOpaque = true
|
|
458
590
|
}
|
|
459
|
-
|
|
591
|
+
|
|
592
|
+
call.resolve([
|
|
593
|
+
"success": true
|
|
594
|
+
])
|
|
595
|
+
} else if let viewModelCall = viewModelCall {
|
|
596
|
+
// There's a call in the viewModel (likely outgoing/ringing), check if we should end or leave
|
|
597
|
+
do {
|
|
598
|
+
let shouldEnd = try await shouldEndCall(for: viewModelCall)
|
|
599
|
+
|
|
600
|
+
if shouldEnd {
|
|
601
|
+
print("Ending viewModel call \(viewModelCall.cId) for all participants")
|
|
602
|
+
try await viewModelCall.end()
|
|
603
|
+
} else {
|
|
604
|
+
print("Leaving viewModel call \(viewModelCall.cId)")
|
|
605
|
+
try await viewModelCall.leave()
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
print("Error ending/leaving viewModel call: \(error)")
|
|
609
|
+
try await viewModelCall.leave() // Fallback to leave
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Also hang up to reset the calling state
|
|
613
|
+
await callViewModel?.hangUp()
|
|
614
|
+
|
|
615
|
+
await MainActor.run {
|
|
616
|
+
self.overlayView?.isHidden = true
|
|
617
|
+
self.webView?.isOpaque = true
|
|
618
|
+
}
|
|
619
|
+
|
|
460
620
|
call.resolve([
|
|
461
621
|
"success": true
|
|
462
622
|
])
|
|
@@ -783,7 +943,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
783
943
|
}
|
|
784
944
|
|
|
785
945
|
@objc func getCallStatus(_ call: CAPPluginCall) {
|
|
786
|
-
//
|
|
946
|
+
// If not in a call, reject
|
|
787
947
|
if currentCallId.isEmpty || currentCallState == "left" {
|
|
788
948
|
call.reject("Not in a call")
|
|
789
949
|
return
|
|
@@ -794,6 +954,90 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
794
954
|
"state": currentCallState
|
|
795
955
|
])
|
|
796
956
|
}
|
|
957
|
+
|
|
958
|
+
@objc func getCallInfo(_ call: CAPPluginCall) {
|
|
959
|
+
guard let callId = call.getString("callId") else {
|
|
960
|
+
call.reject("Missing required parameter: callId")
|
|
961
|
+
return
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
do {
|
|
965
|
+
try requireInitialized()
|
|
966
|
+
|
|
967
|
+
guard let activeCall = streamVideo?.state.activeCall, activeCall.cId == callId else {
|
|
968
|
+
call.reject("Call ID does not match active call")
|
|
969
|
+
return
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
Task {
|
|
973
|
+
do {
|
|
974
|
+
// Get detailed call information
|
|
975
|
+
let callInfo = try await activeCall.get()
|
|
976
|
+
|
|
977
|
+
// Extract caller information
|
|
978
|
+
var caller: [String: Any]? = nil
|
|
979
|
+
let createdBy = callInfo.call.createdBy
|
|
980
|
+
var callerData: [String: Any] = [:]
|
|
981
|
+
callerData["userId"] = createdBy.id
|
|
982
|
+
callerData["name"] = createdBy.name
|
|
983
|
+
callerData["imageURL"] = createdBy.image
|
|
984
|
+
callerData["role"] = createdBy.role
|
|
985
|
+
caller = callerData
|
|
986
|
+
|
|
987
|
+
// Extract members information
|
|
988
|
+
var membersArray: [[String: Any]] = []
|
|
989
|
+
let participants = await activeCall.state.participants
|
|
990
|
+
for participant in participants {
|
|
991
|
+
var memberData: [String: Any] = [:]
|
|
992
|
+
memberData["userId"] = participant.userId
|
|
993
|
+
memberData["name"] = participant.name
|
|
994
|
+
memberData["imageURL"] = participant.profileImageURL
|
|
995
|
+
memberData["role"] = participant.roles.first ?? ""
|
|
996
|
+
membersArray.append(memberData)
|
|
997
|
+
}
|
|
998
|
+
let members = membersArray
|
|
999
|
+
|
|
1000
|
+
// Determine call state based on current calling state
|
|
1001
|
+
let state: String
|
|
1002
|
+
let callingState = await self.callViewModel?.callingState
|
|
1003
|
+
switch callingState {
|
|
1004
|
+
case .idle:
|
|
1005
|
+
state = "idle"
|
|
1006
|
+
case .incoming:
|
|
1007
|
+
state = "ringing"
|
|
1008
|
+
case .outgoing:
|
|
1009
|
+
state = "ringing"
|
|
1010
|
+
case .inCall:
|
|
1011
|
+
state = "joined"
|
|
1012
|
+
case .lobby:
|
|
1013
|
+
state = "lobby"
|
|
1014
|
+
case .joining:
|
|
1015
|
+
state = "joining"
|
|
1016
|
+
case .reconnecting:
|
|
1017
|
+
state = "reconnecting"
|
|
1018
|
+
case .none:
|
|
1019
|
+
state = "unknown"
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
var result: [String: Any] = [:]
|
|
1023
|
+
result["callId"] = callId
|
|
1024
|
+
result["state"] = state
|
|
1025
|
+
|
|
1026
|
+
if let caller = caller {
|
|
1027
|
+
result["caller"] = caller
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
result["members"] = members
|
|
1031
|
+
|
|
1032
|
+
call.resolve(result)
|
|
1033
|
+
} catch {
|
|
1034
|
+
call.reject("Failed to get call info: \(error.localizedDescription)")
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
} catch {
|
|
1038
|
+
call.reject("StreamVideo not initialized")
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
797
1041
|
|
|
798
1042
|
@objc func setSpeaker(_ call: CAPPluginCall) {
|
|
799
1043
|
guard let name = call.getString("name") else {
|
package/package.json
CHANGED