@capgo/capacitor-stream-call 0.0.2 → 0.0.4
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/{StreamCall.podspec → CapgoCapacitorStreamCall.podspec} +1 -1
- package/README.md +37 -28
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CallOverlayView.kt +0 -53
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +10 -19
- package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +25 -9
- package/android/src/main/java/ee/forgr/capacitor/streamcall/RingtonePlayer.kt +0 -2
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +173 -40
- package/dist/docs.json +43 -37
- package/dist/esm/definitions.d.ts +9 -11
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +12 -1
- package/dist/esm/web.js +252 -9
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +252 -9
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +252 -9
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/StreamCallPlugin/CustomCallView.swift +0 -1
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +373 -192
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +3 -3
- package/package.json +3 -3
|
@@ -22,9 +22,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
22
22
|
CAPPluginMethod(name: "endCall", returnType: CAPPluginReturnPromise),
|
|
23
23
|
CAPPluginMethod(name: "setMicrophoneEnabled", returnType: CAPPluginReturnPromise),
|
|
24
24
|
CAPPluginMethod(name: "setCameraEnabled", returnType: CAPPluginReturnPromise),
|
|
25
|
-
CAPPluginMethod(name: "acceptCall", returnType: CAPPluginReturnPromise)
|
|
25
|
+
CAPPluginMethod(name: "acceptCall", returnType: CAPPluginReturnPromise),
|
|
26
|
+
CAPPluginMethod(name: "isCameraEnabled", returnType: CAPPluginReturnPromise)
|
|
26
27
|
]
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
private enum State {
|
|
29
30
|
case notInitialized
|
|
30
31
|
case initializing
|
|
@@ -36,7 +37,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
36
37
|
private static let tokenRefreshSemaphore = DispatchSemaphore(value: 1)
|
|
37
38
|
private var currentToken: String?
|
|
38
39
|
private var tokenWaitSemaphore: DispatchSemaphore?
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
private var overlayView: UIView?
|
|
41
42
|
private var hostingController: UIHostingController<CallOverlayView>?
|
|
42
43
|
private var overlayViewModel: CallOverlayViewModel?
|
|
@@ -44,17 +45,19 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
44
45
|
private var activeCallSubscription: AnyCancellable?
|
|
45
46
|
private var lastVoIPToken: String?
|
|
46
47
|
private var touchInterceptView: TouchInterceptView?
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
private var streamVideo: StreamVideo?
|
|
49
|
-
|
|
50
|
+
|
|
51
|
+
// Track the current active call ID
|
|
52
|
+
private var currentActiveCallId: String?
|
|
53
|
+
|
|
50
54
|
@Injected(\.callKitAdapter) var callKitAdapter
|
|
51
55
|
@Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
|
|
52
|
-
|
|
53
|
-
private var refreshTokenURL: String?
|
|
54
|
-
private var refreshTokenHeaders: [String: String]?
|
|
55
|
-
|
|
56
56
|
private var webviewDelegate: WebviewNavigationDelegate?
|
|
57
|
-
|
|
57
|
+
|
|
58
|
+
// Add class property to store call states
|
|
59
|
+
private var callStates: [String: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)] = [:]
|
|
60
|
+
|
|
58
61
|
override public func load() {
|
|
59
62
|
// Read API key from Info.plist
|
|
60
63
|
if let apiKey = Bundle.main.object(forInfoDictionaryKey: "CAPACITOR_STREAM_VIDEO_APIKEY") as? String {
|
|
@@ -63,7 +66,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
63
66
|
if self.apiKey == nil {
|
|
64
67
|
fatalError("Cannot get apikey")
|
|
65
68
|
}
|
|
66
|
-
|
|
69
|
+
|
|
67
70
|
// Check if we have a logged in user for handling incoming calls
|
|
68
71
|
if let credentials = SecureUserRepository.shared.loadCurrentUser() {
|
|
69
72
|
print("Loading user for StreamCallPlugin: \(credentials.user.name)")
|
|
@@ -71,21 +74,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
71
74
|
self.initializeStreamVideo()
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
|
-
|
|
77
|
+
|
|
75
78
|
// Create and set the navigation delegate
|
|
76
79
|
self.webviewDelegate = WebviewNavigationDelegate(
|
|
77
80
|
wrappedDelegate: self.webView?.navigationDelegate,
|
|
78
81
|
onSetupOverlay: { [weak self] in
|
|
79
82
|
guard let self = self else { return }
|
|
80
83
|
print("Attempting to setup call view")
|
|
81
|
-
|
|
84
|
+
|
|
82
85
|
self.setupViews()
|
|
83
86
|
}
|
|
84
87
|
)
|
|
85
|
-
|
|
88
|
+
|
|
86
89
|
self.webView?.navigationDelegate = self.webviewDelegate
|
|
87
90
|
}
|
|
88
|
-
|
|
91
|
+
|
|
89
92
|
// private func cleanupStreamVideo() {
|
|
90
93
|
// // Cancel subscriptions
|
|
91
94
|
// tokenSubscription?.cancel()
|
|
@@ -111,125 +114,28 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
111
114
|
//
|
|
112
115
|
// state = .notInitialized
|
|
113
116
|
// }
|
|
114
|
-
|
|
117
|
+
|
|
115
118
|
private func requireInitialized() throws {
|
|
116
119
|
guard state == .initialized else {
|
|
117
120
|
throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "StreamVideo not initialized"])
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
|
-
|
|
121
|
-
private func refreshToken() throws -> UserToken {
|
|
122
|
-
// Acquire the semaphore in a thread-safe way
|
|
123
|
-
StreamCallPlugin.tokenRefreshQueue.sync {
|
|
124
|
-
StreamCallPlugin.tokenRefreshSemaphore.wait()
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
defer {
|
|
128
|
-
// Always release the semaphore when we're done, even if we throw an error
|
|
129
|
-
StreamCallPlugin.tokenRefreshSemaphore.signal()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Clear current token
|
|
133
|
-
currentToken = nil
|
|
134
|
-
|
|
135
|
-
// Create a local semaphore for waiting on the token
|
|
136
|
-
let localSemaphore = DispatchSemaphore(value: 0)
|
|
137
|
-
tokenWaitSemaphore = localSemaphore
|
|
138
|
-
|
|
139
|
-
// Capture webView before async context
|
|
140
|
-
guard let webView = self.webView else {
|
|
141
|
-
print("WebView not available")
|
|
142
|
-
tokenWaitSemaphore = nil
|
|
143
|
-
throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "WebView not available"])
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
guard let refreshURL = self.refreshTokenURL else {
|
|
147
|
-
print("Refresh URL not configured")
|
|
148
|
-
tokenWaitSemaphore = nil
|
|
149
|
-
throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "Refresh URL not configured"])
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let headersJSON = (self.refreshTokenHeaders ?? [:]).reduce(into: "") { result, pair in
|
|
153
|
-
result += "\n'\(pair.key)': '\(pair.value)',"
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let script = """
|
|
157
|
-
(function() {
|
|
158
|
-
console.log('Starting token refresh...');
|
|
159
|
-
fetch('\(refreshURL)', {
|
|
160
|
-
headers: {
|
|
161
|
-
\(headersJSON)
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
.then(response => {
|
|
165
|
-
console.log('Got response:', response.status);
|
|
166
|
-
return response.json();
|
|
167
|
-
})
|
|
168
|
-
.then(data => {
|
|
169
|
-
console.log('Got data, token length:', data.token?.length);
|
|
170
|
-
const tokenA = data.token;
|
|
171
|
-
window.Capacitor.Plugins.StreamCall.loginMagicToken({
|
|
172
|
-
token: tokenA
|
|
173
|
-
});
|
|
174
|
-
})
|
|
175
|
-
.catch(error => {
|
|
176
|
-
console.error('Token refresh error:', error);
|
|
177
|
-
});
|
|
178
|
-
})();
|
|
179
|
-
"""
|
|
180
|
-
|
|
181
|
-
if Thread.isMainThread {
|
|
182
|
-
print("Executing script on main thread")
|
|
183
|
-
webView.evaluateJavaScript(script, completionHandler: nil)
|
|
184
|
-
} else {
|
|
185
|
-
print("Executing script from background thread")
|
|
186
|
-
DispatchQueue.main.sync {
|
|
187
|
-
webView.evaluateJavaScript(script, completionHandler: nil)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Set up a timeout
|
|
192
|
-
let timeoutQueue = DispatchQueue.global()
|
|
193
|
-
let timeoutWork = DispatchWorkItem {
|
|
194
|
-
print("Token refresh timed out")
|
|
195
|
-
self.tokenWaitSemaphore?.signal()
|
|
196
|
-
self.tokenWaitSemaphore = nil
|
|
197
|
-
}
|
|
198
|
-
timeoutQueue.asyncAfter(deadline: .now() + 10.0, execute: timeoutWork) // 10 second timeout
|
|
199
|
-
|
|
200
|
-
// Wait for token to be set via loginMagicToken or timeout
|
|
201
|
-
localSemaphore.wait()
|
|
202
|
-
timeoutWork.cancel()
|
|
203
|
-
tokenWaitSemaphore = nil
|
|
204
|
-
|
|
205
|
-
guard let token = currentToken else {
|
|
206
|
-
print("Failed to get token")
|
|
207
|
-
throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get token or timeout occurred"])
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Save the token
|
|
211
|
-
SecureUserRepository.shared.save(token: token)
|
|
212
|
-
|
|
213
|
-
print("Got the token!!!")
|
|
214
|
-
return UserToken(stringLiteral: token)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
123
|
@objc func loginMagicToken(_ call: CAPPluginCall) {
|
|
218
124
|
guard let token = call.getString("token") else {
|
|
219
125
|
call.reject("Missing token parameter")
|
|
220
126
|
return
|
|
221
127
|
}
|
|
222
|
-
|
|
128
|
+
|
|
223
129
|
print("loginMagicToken received token")
|
|
224
130
|
currentToken = token
|
|
225
131
|
tokenWaitSemaphore?.signal()
|
|
226
132
|
call.resolve()
|
|
227
133
|
}
|
|
228
|
-
|
|
134
|
+
|
|
229
135
|
private func setupTokenSubscription() {
|
|
230
136
|
// Cancel existing subscription if any
|
|
231
137
|
tokenSubscription?.cancel()
|
|
232
|
-
|
|
138
|
+
|
|
233
139
|
// Create new subscription
|
|
234
140
|
tokenSubscription = callKitPushNotificationAdapter.$deviceToken.sink { [weak self] (updatedDeviceToken: String) in
|
|
235
141
|
guard let self = self else { return }
|
|
@@ -255,19 +161,144 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
255
161
|
}
|
|
256
162
|
}
|
|
257
163
|
}
|
|
258
|
-
|
|
164
|
+
|
|
259
165
|
private func setupActiveCallSubscription() {
|
|
260
166
|
if let streamVideo = streamVideo {
|
|
261
167
|
Task {
|
|
262
168
|
for await event in streamVideo.subscribe() {
|
|
263
|
-
print("Event", event)
|
|
169
|
+
// print("Event", event)
|
|
264
170
|
if let ringingEvent = event.rawValue as? CallRingEvent {
|
|
265
171
|
notifyListeners("callEvent", data: [
|
|
266
172
|
"callId": ringingEvent.callCid,
|
|
267
173
|
"state": "ringing"
|
|
268
174
|
])
|
|
269
|
-
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if let callCreatedEvent = event.rawValue as? CallCreatedEvent {
|
|
179
|
+
print("CallCreatedEvent \(String(describing: userId))")
|
|
180
|
+
|
|
181
|
+
let callCid = callCreatedEvent.callCid
|
|
182
|
+
let members = callCreatedEvent.members
|
|
183
|
+
|
|
184
|
+
// Create timer on main thread
|
|
185
|
+
await MainActor.run {
|
|
186
|
+
// Store in the combined callStates map
|
|
187
|
+
self.callStates[callCid] = (
|
|
188
|
+
members: members,
|
|
189
|
+
participantResponses: [:],
|
|
190
|
+
createdAt: Date(),
|
|
191
|
+
timer: nil
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Start timer to check for timeout every second
|
|
195
|
+
// Use @objc method as timer target to avoid sendable closure issues
|
|
196
|
+
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.checkCallTimeoutTimer(_:)), userInfo: callCid, repeats: true)
|
|
197
|
+
|
|
198
|
+
// Update timer in callStates
|
|
199
|
+
self.callStates[callCid]?.timer = timer
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if let rejectedEvent = event.rawValue as? CallRejectedEvent {
|
|
204
|
+
let userId = rejectedEvent.user.id
|
|
205
|
+
let callCid = rejectedEvent.callCid
|
|
206
|
+
|
|
207
|
+
// Operate on callStates on the main thread
|
|
208
|
+
await MainActor.run {
|
|
209
|
+
// Update the combined callStates map
|
|
210
|
+
if var callState = self.callStates[callCid] {
|
|
211
|
+
callState.participantResponses[userId] = "rejected"
|
|
212
|
+
self.callStates[callCid] = callState
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
print("CallRejectedEvent \(userId)")
|
|
217
|
+
notifyListeners("callEvent", data: [
|
|
218
|
+
"callId": callCid,
|
|
219
|
+
"state": "rejected",
|
|
220
|
+
"userId": userId
|
|
221
|
+
])
|
|
222
|
+
|
|
223
|
+
await checkAllParticipantsResponded(callCid: callCid)
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if let missedEvent = event.rawValue as? CallMissedEvent {
|
|
228
|
+
let userId = missedEvent.user.id
|
|
229
|
+
let callCid = missedEvent.callCid
|
|
230
|
+
|
|
231
|
+
// Operate on callStates on the main thread
|
|
232
|
+
await MainActor.run {
|
|
233
|
+
// Update the combined callStates map
|
|
234
|
+
if var callState = self.callStates[callCid] {
|
|
235
|
+
callState.participantResponses[userId] = "missed"
|
|
236
|
+
self.callStates[callCid] = callState
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
print("CallMissedEvent \(userId)")
|
|
241
|
+
notifyListeners("callEvent", data: [
|
|
242
|
+
"callId": callCid,
|
|
243
|
+
"state": "missed",
|
|
244
|
+
"userId": userId
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
await checkAllParticipantsResponded(callCid: callCid)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if let participantLeftEvent = event.rawValue as? CallSessionParticipantLeftEvent {
|
|
252
|
+
let callIdSplit = participantLeftEvent.callCid.split(separator: ":")
|
|
253
|
+
if (callIdSplit.count != 2) {
|
|
254
|
+
print("CallSessionParticipantLeftEvent invalid cID \(participantLeftEvent.callCid)")
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let callType = callIdSplit[0]
|
|
259
|
+
let callId = callIdSplit[1]
|
|
260
|
+
|
|
261
|
+
let call = streamVideo.call(callType: String(callType), callId: String(callId))
|
|
262
|
+
if await MainActor.run(body: { (call.state.session?.participants.count ?? 1) - 1 <= 1 }) {
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
print("We are left solo in a call. Ending. cID: \(participantLeftEvent.callCid)")
|
|
266
|
+
|
|
267
|
+
Task {
|
|
268
|
+
if let activeCall = streamVideo.state.activeCall {
|
|
269
|
+
activeCall.leave()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if let acceptedEvent = event.rawValue as? CallAcceptedEvent {
|
|
276
|
+
let userId = acceptedEvent.user.id
|
|
277
|
+
let callCid = acceptedEvent.callCid
|
|
278
|
+
|
|
279
|
+
// Operate on callStates on the main thread
|
|
280
|
+
await MainActor.run {
|
|
281
|
+
// Update the combined callStates map
|
|
282
|
+
if var callState = self.callStates[callCid] {
|
|
283
|
+
callState.participantResponses[userId] = "accepted"
|
|
284
|
+
|
|
285
|
+
// If someone accepted, invalidate the timer as we don't need to check anymore
|
|
286
|
+
callState.timer?.invalidate()
|
|
287
|
+
callState.timer = nil
|
|
288
|
+
|
|
289
|
+
self.callStates[callCid] = callState
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
print("CallAcceptedEvent \(userId)")
|
|
294
|
+
notifyListeners("callEvent", data: [
|
|
295
|
+
"callId": callCid,
|
|
296
|
+
"state": "accepted",
|
|
297
|
+
"userId": userId
|
|
298
|
+
])
|
|
299
|
+
continue
|
|
270
300
|
}
|
|
301
|
+
|
|
271
302
|
notifyListeners("callEvent", data: [
|
|
272
303
|
"callId": streamVideo.state.activeCall?.callId ?? "",
|
|
273
304
|
"state": event.type
|
|
@@ -280,38 +311,62 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
280
311
|
// Create new subscription
|
|
281
312
|
activeCallSubscription = streamVideo?.state.$activeCall.sink { [weak self] newState in
|
|
282
313
|
guard let self = self else { return }
|
|
314
|
+
|
|
283
315
|
Task { @MainActor in
|
|
284
316
|
do {
|
|
285
317
|
try self.requireInitialized()
|
|
286
318
|
print("Call State Update:")
|
|
287
319
|
print("- Call is nil: \(newState == nil)")
|
|
320
|
+
|
|
288
321
|
if let state = newState?.state {
|
|
289
322
|
print("- state: \(state)")
|
|
290
323
|
print("- Session ID: \(state.sessionId)")
|
|
291
324
|
print("- All participants: \(String(describing: state.participants))")
|
|
292
325
|
print("- Remote participants: \(String(describing: state.remoteParticipants))")
|
|
293
|
-
|
|
326
|
+
|
|
327
|
+
// Store the active call ID when a call becomes active
|
|
328
|
+
self.currentActiveCallId = newState?.cId
|
|
329
|
+
print("Updated current active call ID: \(String(describing: self.currentActiveCallId))")
|
|
330
|
+
|
|
294
331
|
// Update overlay and make visible when there's an active call
|
|
295
332
|
self.overlayViewModel?.updateCall(newState)
|
|
296
333
|
self.overlayView?.isHidden = false
|
|
297
334
|
self.webView?.isOpaque = false
|
|
298
|
-
|
|
335
|
+
|
|
299
336
|
// Notify that a call has started
|
|
300
337
|
self.notifyListeners("callEvent", data: [
|
|
301
338
|
"callId": newState?.cId ?? "",
|
|
302
339
|
"state": "joined"
|
|
303
340
|
])
|
|
304
341
|
} else {
|
|
342
|
+
// Get the call ID that was active before the state changed to nil
|
|
343
|
+
let endingCallId = self.currentActiveCallId
|
|
344
|
+
print("Call ending: \(String(describing: endingCallId))")
|
|
345
|
+
|
|
305
346
|
// If newState is nil, hide overlay and clear call
|
|
306
347
|
self.overlayViewModel?.updateCall(nil)
|
|
307
348
|
self.overlayView?.isHidden = true
|
|
308
349
|
self.webView?.isOpaque = true
|
|
309
|
-
|
|
310
|
-
// Notify that call has ended
|
|
350
|
+
|
|
351
|
+
// Notify that call has ended - use the properly tracked call ID
|
|
311
352
|
self.notifyListeners("callEvent", data: [
|
|
312
|
-
"callId":
|
|
353
|
+
"callId": endingCallId ?? "",
|
|
313
354
|
"state": "left"
|
|
314
355
|
])
|
|
356
|
+
|
|
357
|
+
// Clean up any resources for this call
|
|
358
|
+
if let callCid = endingCallId {
|
|
359
|
+
// Invalidate and remove the timer
|
|
360
|
+
self.callStates[callCid]?.timer?.invalidate()
|
|
361
|
+
|
|
362
|
+
// Remove call from callStates
|
|
363
|
+
self.callStates.removeValue(forKey: callCid)
|
|
364
|
+
|
|
365
|
+
print("Cleaned up resources for ended call: \(callCid)")
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Clear the active call ID
|
|
369
|
+
self.currentActiveCallId = nil
|
|
315
370
|
}
|
|
316
371
|
} catch {
|
|
317
372
|
log.error("Error handling call state update: \(String(describing: error))")
|
|
@@ -319,57 +374,187 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
319
374
|
}
|
|
320
375
|
}
|
|
321
376
|
}
|
|
322
|
-
|
|
377
|
+
|
|
378
|
+
@objc private func checkCallTimeoutTimer(_ timer: Timer) {
|
|
379
|
+
guard let callCid = timer.userInfo as? String else { return }
|
|
380
|
+
|
|
381
|
+
Task { [weak self] in
|
|
382
|
+
guard let self = self else { return }
|
|
383
|
+
await self.checkCallTimeout(callCid: callCid)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private func checkCallTimeout(callCid: String) async {
|
|
388
|
+
// Get a local copy of the call state from the main thread
|
|
389
|
+
let callState: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)? = await MainActor.run {
|
|
390
|
+
return self.callStates[callCid]
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
guard let callState = callState else { return }
|
|
394
|
+
|
|
395
|
+
// Calculate time elapsed since call creation
|
|
396
|
+
let now = Date()
|
|
397
|
+
let elapsedSeconds = now.timeIntervalSince(callState.createdAt)
|
|
398
|
+
|
|
399
|
+
// Check if 30 seconds have passed
|
|
400
|
+
if elapsedSeconds >= 30.0 {
|
|
401
|
+
|
|
402
|
+
// Check if anyone has accepted
|
|
403
|
+
let hasAccepted = callState.participantResponses.values.contains { $0 == "accepted" }
|
|
404
|
+
|
|
405
|
+
if !hasAccepted {
|
|
406
|
+
print("Call \(callCid) has timed out after \(elapsedSeconds) seconds")
|
|
407
|
+
print("No one accepted call \(callCid), marking all non-responders as missed")
|
|
408
|
+
|
|
409
|
+
// Mark all members who haven't responded as "missed"
|
|
410
|
+
for member in callState.members {
|
|
411
|
+
let memberId = member.userId
|
|
412
|
+
let needsToBeMarkedAsMissed = await MainActor.run {
|
|
413
|
+
return self.callStates[callCid]?.participantResponses[memberId] == nil
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if needsToBeMarkedAsMissed {
|
|
417
|
+
// Update callStates map on main thread
|
|
418
|
+
await MainActor.run {
|
|
419
|
+
var updatedCallState = self.callStates[callCid]
|
|
420
|
+
updatedCallState?.participantResponses[memberId] = "missed"
|
|
421
|
+
if let updatedCallState = updatedCallState {
|
|
422
|
+
self.callStates[callCid] = updatedCallState
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Notify listeners
|
|
427
|
+
await MainActor.run {
|
|
428
|
+
self.notifyListeners("callEvent", data: [
|
|
429
|
+
"callId": callCid,
|
|
430
|
+
"state": "missed",
|
|
431
|
+
"userId": memberId
|
|
432
|
+
])
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// End the call
|
|
438
|
+
if let call = streamVideo?.state.activeCall, call.cId == callCid {
|
|
439
|
+
call.leave()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Clean up timer on main thread
|
|
443
|
+
await MainActor.run {
|
|
444
|
+
self.callStates[callCid]?.timer?.invalidate()
|
|
445
|
+
var updatedCallState = self.callStates[callCid]
|
|
446
|
+
updatedCallState?.timer = nil
|
|
447
|
+
if let updatedCallState = updatedCallState {
|
|
448
|
+
self.callStates[callCid] = updatedCallState
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Remove from callStates
|
|
452
|
+
self.callStates.removeValue(forKey: callCid)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Update UI
|
|
456
|
+
await MainActor.run {
|
|
457
|
+
self.overlayViewModel?.updateCall(nil)
|
|
458
|
+
self.overlayView?.isHidden = true
|
|
459
|
+
self.webView?.isOpaque = true
|
|
460
|
+
self.notifyListeners("callEvent", data: [
|
|
461
|
+
"callId": callCid,
|
|
462
|
+
"state": "ended",
|
|
463
|
+
"reason": "timeout"
|
|
464
|
+
])
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private func checkAllParticipantsResponded(callCid: String) async {
|
|
471
|
+
// Get a local copy of the call state from the main thread
|
|
472
|
+
let callState: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)? = await MainActor.run {
|
|
473
|
+
return self.callStates[callCid]
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
guard let callState = callState else {
|
|
477
|
+
print("Call state not found for cId: \(callCid)")
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let totalParticipants = callState.members.count
|
|
482
|
+
let responseCount = callState.participantResponses.count
|
|
483
|
+
|
|
484
|
+
print("Total participants: \(totalParticipants), Responses: \(responseCount)")
|
|
485
|
+
|
|
486
|
+
let allResponded = responseCount >= totalParticipants
|
|
487
|
+
let allRejectedOrMissed = allResponded &&
|
|
488
|
+
callState.participantResponses.values.allSatisfy { $0 == "rejected" || $0 == "missed" }
|
|
489
|
+
|
|
490
|
+
if allResponded && allRejectedOrMissed {
|
|
491
|
+
print("All participants have rejected or missed the call")
|
|
492
|
+
|
|
493
|
+
// End the call
|
|
494
|
+
if let call = streamVideo?.state.activeCall, call.cId == callCid {
|
|
495
|
+
call.leave()
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Clean up timer and remove from callStates on main thread
|
|
499
|
+
await MainActor.run {
|
|
500
|
+
// Clean up timer
|
|
501
|
+
self.callStates[callCid]?.timer?.invalidate()
|
|
502
|
+
|
|
503
|
+
// Remove from callStates
|
|
504
|
+
self.callStates.removeValue(forKey: callCid)
|
|
505
|
+
|
|
506
|
+
self.overlayViewModel?.updateCall(nil)
|
|
507
|
+
self.overlayView?.isHidden = true
|
|
508
|
+
self.webView?.isOpaque = true
|
|
509
|
+
self.notifyListeners("callEvent", data: [
|
|
510
|
+
"callId": callCid,
|
|
511
|
+
"state": "ended",
|
|
512
|
+
"reason": "all_rejected_or_missed"
|
|
513
|
+
])
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
323
518
|
@objc func login(_ call: CAPPluginCall) {
|
|
324
519
|
guard let token = call.getString("token"),
|
|
325
520
|
let userId = call.getString("userId"),
|
|
326
|
-
let name = call.getString("name")
|
|
327
|
-
let apiKey = call.getString("apiKey") else {
|
|
521
|
+
let name = call.getString("name") else {
|
|
328
522
|
call.reject("Missing required parameters")
|
|
329
523
|
return
|
|
330
524
|
}
|
|
331
|
-
|
|
525
|
+
|
|
332
526
|
let imageURL = call.getString("imageURL")
|
|
333
|
-
let refreshTokenConfig = call.getObject("refreshToken")
|
|
334
|
-
let refreshTokenURL = refreshTokenConfig?["url"] as? String
|
|
335
|
-
let refreshTokenHeaders = refreshTokenConfig?["headers"] as? [String: String]
|
|
336
|
-
|
|
337
527
|
let user = User(
|
|
338
528
|
id: userId,
|
|
339
529
|
name: name,
|
|
340
530
|
imageURL: imageURL.flatMap { URL(string: $0) },
|
|
341
531
|
customData: [:]
|
|
342
532
|
)
|
|
343
|
-
|
|
533
|
+
|
|
344
534
|
let credentials = UserCredentials(user: user, tokenValue: token)
|
|
345
535
|
SecureUserRepository.shared.save(user: credentials)
|
|
346
|
-
|
|
347
|
-
// Store API key and refresh config for later use
|
|
348
|
-
self.refreshTokenURL = refreshTokenURL
|
|
349
|
-
self.refreshTokenHeaders = refreshTokenHeaders
|
|
350
|
-
|
|
351
536
|
// Initialize Stream Video with new credentials
|
|
352
537
|
initializeStreamVideo()
|
|
353
|
-
|
|
538
|
+
|
|
354
539
|
if state != .initialized {
|
|
355
540
|
call.reject("Failed to initialize StreamVideo")
|
|
356
541
|
return
|
|
357
542
|
}
|
|
358
|
-
|
|
543
|
+
|
|
359
544
|
// Update the CallOverlayView with new StreamVideo instance
|
|
360
545
|
Task { @MainActor in
|
|
361
546
|
self.overlayViewModel?.updateStreamVideo(self.streamVideo)
|
|
362
547
|
}
|
|
363
|
-
|
|
548
|
+
|
|
364
549
|
call.resolve([
|
|
365
550
|
"success": true
|
|
366
551
|
])
|
|
367
552
|
}
|
|
368
|
-
|
|
553
|
+
|
|
369
554
|
@objc func logout(_ call: CAPPluginCall) {
|
|
370
555
|
// Remove VOIP token from repository
|
|
371
556
|
SecureUserRepository.shared.save(voipPushToken: nil)
|
|
372
|
-
|
|
557
|
+
|
|
373
558
|
// Try to delete the device from Stream if we have the last token
|
|
374
559
|
if let lastVoIPToken = lastVoIPToken {
|
|
375
560
|
Task {
|
|
@@ -380,16 +565,16 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
380
565
|
}
|
|
381
566
|
}
|
|
382
567
|
}
|
|
383
|
-
|
|
568
|
+
|
|
384
569
|
// Cancel subscriptions
|
|
385
570
|
tokenSubscription?.cancel()
|
|
386
571
|
tokenSubscription = nil
|
|
387
572
|
activeCallSubscription?.cancel()
|
|
388
573
|
activeCallSubscription = nil
|
|
389
574
|
lastVoIPToken = nil
|
|
390
|
-
|
|
575
|
+
|
|
391
576
|
SecureUserRepository.shared.removeCurrentUser()
|
|
392
|
-
|
|
577
|
+
|
|
393
578
|
// Update the CallOverlayView with nil StreamVideo instance
|
|
394
579
|
Task { @MainActor in
|
|
395
580
|
self.overlayViewModel?.updateCall(nil)
|
|
@@ -397,24 +582,41 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
397
582
|
self.overlayView?.isHidden = true
|
|
398
583
|
self.webView?.isOpaque = true
|
|
399
584
|
}
|
|
400
|
-
|
|
585
|
+
|
|
401
586
|
call.resolve([
|
|
402
587
|
"success": true
|
|
403
588
|
])
|
|
404
589
|
}
|
|
590
|
+
|
|
591
|
+
@objc func isCameraEnabled(_ call: CAPPluginCall) {
|
|
592
|
+
do {
|
|
593
|
+
try requireInitialized()
|
|
594
|
+
} catch {
|
|
595
|
+
call.reject("SDK not initialized")
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if let activeCall = streamVideo?.state.activeCall {
|
|
599
|
+
call.resolve([
|
|
600
|
+
"enabled": activeCall.camera.status == .enabled
|
|
601
|
+
])
|
|
602
|
+
} else {
|
|
603
|
+
call.reject("No active call")
|
|
604
|
+
}
|
|
605
|
+
}
|
|
405
606
|
|
|
406
607
|
@objc func call(_ call: CAPPluginCall) {
|
|
407
|
-
guard let
|
|
408
|
-
call.reject("Missing required parameter:
|
|
608
|
+
guard let members = call.getArray("userIds", String.self) else {
|
|
609
|
+
call.reject("Missing required parameter: userIds (array of user IDs)")
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if members.isEmpty {
|
|
614
|
+
call.reject("userIds array cannot be empty")
|
|
409
615
|
return
|
|
410
616
|
}
|
|
411
617
|
|
|
412
618
|
// Initialize if needed
|
|
413
619
|
if state == .notInitialized {
|
|
414
|
-
guard let credentials = SecureUserRepository.shared.loadCurrentUser() else {
|
|
415
|
-
call.reject("No user credentials found")
|
|
416
|
-
return
|
|
417
|
-
}
|
|
418
620
|
initializeStreamVideo()
|
|
419
621
|
if state != .initialized {
|
|
420
622
|
call.reject("Failed to initialize StreamVideo")
|
|
@@ -436,16 +638,16 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
436
638
|
print("Creating call:")
|
|
437
639
|
print("- Call ID: \(callId)")
|
|
438
640
|
print("- Call Type: \(callType)")
|
|
439
|
-
print("-
|
|
641
|
+
print("- Users: \(members)")
|
|
440
642
|
print("- Should Ring: \(shouldRing)")
|
|
441
643
|
|
|
442
644
|
// Create the call object
|
|
443
645
|
let streamCall = streamVideo?.call(callType: callType, callId: callId)
|
|
444
646
|
|
|
445
|
-
// Start the call with the
|
|
446
|
-
print("Creating call with
|
|
647
|
+
// Start the call with the members
|
|
648
|
+
print("Creating call with members...")
|
|
447
649
|
try await streamCall?.create(
|
|
448
|
-
memberIds:
|
|
650
|
+
memberIds: members,
|
|
449
651
|
custom: [:],
|
|
450
652
|
ring: shouldRing
|
|
451
653
|
)
|
|
@@ -480,26 +682,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
480
682
|
try requireInitialized()
|
|
481
683
|
|
|
482
684
|
Task {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
try await activeCall.leave()
|
|
486
|
-
|
|
487
|
-
// Update view state instead of cleaning up
|
|
488
|
-
await MainActor.run {
|
|
489
|
-
self.overlayViewModel?.updateCall(nil)
|
|
490
|
-
self.overlayView?.isHidden = true
|
|
491
|
-
self.webView?.isOpaque = true
|
|
492
|
-
}
|
|
685
|
+
if let activeCall = streamVideo?.state.activeCall {
|
|
686
|
+
activeCall.leave()
|
|
493
687
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
688
|
+
// Update view state instead of cleaning up
|
|
689
|
+
await MainActor.run {
|
|
690
|
+
self.overlayViewModel?.updateCall(nil)
|
|
691
|
+
self.overlayView?.isHidden = true
|
|
692
|
+
self.webView?.isOpaque = true
|
|
499
693
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
694
|
+
|
|
695
|
+
call.resolve([
|
|
696
|
+
"success": true
|
|
697
|
+
])
|
|
698
|
+
} else {
|
|
699
|
+
call.reject("No active call to end")
|
|
503
700
|
}
|
|
504
701
|
}
|
|
505
702
|
} catch {
|
|
@@ -625,26 +822,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
625
822
|
}
|
|
626
823
|
print("Initializing with saved credentials for user: \(savedCredentials.user.name)")
|
|
627
824
|
|
|
628
|
-
// Create a local reference to refreshToken to avoid capturing self
|
|
629
|
-
let refreshTokenFn = self.refreshToken
|
|
630
|
-
|
|
631
825
|
self.streamVideo = StreamVideo(
|
|
632
826
|
apiKey: apiKey,
|
|
633
827
|
user: savedCredentials.user,
|
|
634
|
-
token: UserToken(stringLiteral: savedCredentials.tokenValue)
|
|
635
|
-
tokenProvider: { result in
|
|
636
|
-
print("attempt to refresh")
|
|
637
|
-
DispatchQueue.global().async {
|
|
638
|
-
do {
|
|
639
|
-
let newToken = try refreshTokenFn()
|
|
640
|
-
print("Refresh successful")
|
|
641
|
-
result(.success(newToken))
|
|
642
|
-
} catch {
|
|
643
|
-
print("Refresh fail")
|
|
644
|
-
result(.failure(error))
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
828
|
+
token: UserToken(stringLiteral: savedCredentials.tokenValue)
|
|
648
829
|
)
|
|
649
830
|
|
|
650
831
|
state = .initialized
|