@capgo/capacitor-stream-call 0.0.3 → 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/README.md +1 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +1 -1
- package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +7 -2
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +2 -2
- package/dist/docs.json +7 -0
- package/dist/esm/definitions.d.ts +2 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +10 -0
- package/dist/esm/web.js +243 -8
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +243 -8
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +243 -8
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +256 -24
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +3 -3
- package/package.json +2 -2
|
@@ -48,10 +48,16 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
48
48
|
|
|
49
49
|
private var streamVideo: StreamVideo?
|
|
50
50
|
|
|
51
|
+
// Track the current active call ID
|
|
52
|
+
private var currentActiveCallId: String?
|
|
53
|
+
|
|
51
54
|
@Injected(\.callKitAdapter) var callKitAdapter
|
|
52
55
|
@Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
|
|
53
56
|
private var webviewDelegate: WebviewNavigationDelegate?
|
|
54
57
|
|
|
58
|
+
// Add class property to store call states
|
|
59
|
+
private var callStates: [String: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)] = [:]
|
|
60
|
+
|
|
55
61
|
override public func load() {
|
|
56
62
|
// Read API key from Info.plist
|
|
57
63
|
if let apiKey = Bundle.main.object(forInfoDictionaryKey: "CAPACITOR_STREAM_VIDEO_APIKEY") as? String {
|
|
@@ -158,55 +164,139 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
158
164
|
|
|
159
165
|
private func setupActiveCallSubscription() {
|
|
160
166
|
if let streamVideo = streamVideo {
|
|
161
|
-
// Track participants responses
|
|
162
|
-
var participantResponses: [String: String] = [:]
|
|
163
|
-
|
|
164
167
|
Task {
|
|
165
168
|
for await event in streamVideo.subscribe() {
|
|
166
|
-
print("Event", event)
|
|
169
|
+
// print("Event", event)
|
|
167
170
|
if let ringingEvent = event.rawValue as? CallRingEvent {
|
|
168
171
|
notifyListeners("callEvent", data: [
|
|
169
172
|
"callId": ringingEvent.callCid,
|
|
170
173
|
"state": "ringing"
|
|
171
174
|
])
|
|
172
|
-
|
|
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
|
+
}
|
|
173
201
|
}
|
|
174
202
|
|
|
175
203
|
if let rejectedEvent = event.rawValue as? CallRejectedEvent {
|
|
176
204
|
let userId = rejectedEvent.user.id
|
|
177
|
-
|
|
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)")
|
|
178
217
|
notifyListeners("callEvent", data: [
|
|
179
|
-
"callId":
|
|
218
|
+
"callId": callCid,
|
|
180
219
|
"state": "rejected",
|
|
181
220
|
"userId": userId
|
|
182
221
|
])
|
|
183
222
|
|
|
184
|
-
await checkAllParticipantsResponded(
|
|
185
|
-
|
|
223
|
+
await checkAllParticipantsResponded(callCid: callCid)
|
|
224
|
+
continue
|
|
186
225
|
}
|
|
187
226
|
|
|
188
227
|
if let missedEvent = event.rawValue as? CallMissedEvent {
|
|
189
228
|
let userId = missedEvent.user.id
|
|
190
|
-
|
|
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)")
|
|
191
241
|
notifyListeners("callEvent", data: [
|
|
192
|
-
"callId":
|
|
242
|
+
"callId": callCid,
|
|
193
243
|
"state": "missed",
|
|
194
244
|
"userId": userId
|
|
195
245
|
])
|
|
196
246
|
|
|
197
|
-
await checkAllParticipantsResponded(
|
|
198
|
-
|
|
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
|
+
}
|
|
199
273
|
}
|
|
200
274
|
|
|
201
275
|
if let acceptedEvent = event.rawValue as? CallAcceptedEvent {
|
|
202
276
|
let userId = acceptedEvent.user.id
|
|
203
|
-
|
|
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)")
|
|
204
294
|
notifyListeners("callEvent", data: [
|
|
205
|
-
"callId":
|
|
295
|
+
"callId": callCid,
|
|
206
296
|
"state": "accepted",
|
|
207
297
|
"userId": userId
|
|
208
298
|
])
|
|
209
|
-
|
|
299
|
+
continue
|
|
210
300
|
}
|
|
211
301
|
|
|
212
302
|
notifyListeners("callEvent", data: [
|
|
@@ -221,17 +311,23 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
221
311
|
// Create new subscription
|
|
222
312
|
activeCallSubscription = streamVideo?.state.$activeCall.sink { [weak self] newState in
|
|
223
313
|
guard let self = self else { return }
|
|
314
|
+
|
|
224
315
|
Task { @MainActor in
|
|
225
316
|
do {
|
|
226
317
|
try self.requireInitialized()
|
|
227
318
|
print("Call State Update:")
|
|
228
319
|
print("- Call is nil: \(newState == nil)")
|
|
320
|
+
|
|
229
321
|
if let state = newState?.state {
|
|
230
322
|
print("- state: \(state)")
|
|
231
323
|
print("- Session ID: \(state.sessionId)")
|
|
232
324
|
print("- All participants: \(String(describing: state.participants))")
|
|
233
325
|
print("- Remote participants: \(String(describing: state.remoteParticipants))")
|
|
234
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
|
+
|
|
235
331
|
// Update overlay and make visible when there's an active call
|
|
236
332
|
self.overlayViewModel?.updateCall(newState)
|
|
237
333
|
self.overlayView?.isHidden = false
|
|
@@ -243,16 +339,34 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
243
339
|
"state": "joined"
|
|
244
340
|
])
|
|
245
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
|
+
|
|
246
346
|
// If newState is nil, hide overlay and clear call
|
|
247
347
|
self.overlayViewModel?.updateCall(nil)
|
|
248
348
|
self.overlayView?.isHidden = true
|
|
249
349
|
self.webView?.isOpaque = true
|
|
250
350
|
|
|
251
|
-
// Notify that call has ended
|
|
351
|
+
// Notify that call has ended - use the properly tracked call ID
|
|
252
352
|
self.notifyListeners("callEvent", data: [
|
|
253
|
-
"callId":
|
|
353
|
+
"callId": endingCallId ?? "",
|
|
254
354
|
"state": "left"
|
|
255
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
|
|
256
370
|
}
|
|
257
371
|
} catch {
|
|
258
372
|
log.error("Error handling call state update: \(String(describing: error))")
|
|
@@ -261,16 +375,134 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
261
375
|
}
|
|
262
376
|
}
|
|
263
377
|
|
|
264
|
-
private func
|
|
265
|
-
let
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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" }
|
|
269
489
|
|
|
270
490
|
if allResponded && allRejectedOrMissed {
|
|
271
491
|
print("All participants have rejected or missed the call")
|
|
272
|
-
|
|
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
|
|
273
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
|
+
|
|
274
506
|
self.overlayViewModel?.updateCall(nil)
|
|
275
507
|
self.overlayView?.isHidden = true
|
|
276
508
|
self.webView?.isOpaque = true
|
|
@@ -132,8 +132,8 @@ class TouchInterceptView: UIView {
|
|
|
132
132
|
// Convert point to global coordinates for labeled frame checking
|
|
133
133
|
let globalPoint = convert(point, to: nil)
|
|
134
134
|
|
|
135
|
-
print("TouchInterceptView - Hit test at global point: \(globalPoint)")
|
|
136
|
-
print("Current labeled frames: \(labeledFrames.map { "\($0.label): \($0.frame)" }.joined(separator: ", "))")
|
|
135
|
+
//print("TouchInterceptView - Hit test at global point: \(globalPoint)")
|
|
136
|
+
//print("Current labeled frames: \(labeledFrames.map { "\($0.label): \($0.frame)" }.joined(separator: ", "))")
|
|
137
137
|
|
|
138
138
|
// Convert point for both views
|
|
139
139
|
let webViewPoint = convert(point, to: webView)
|
|
@@ -142,7 +142,7 @@ class TouchInterceptView: UIView {
|
|
|
142
142
|
// First check if the point is inside any labeled frame
|
|
143
143
|
for labeledFrame in labeledFrames {
|
|
144
144
|
if labeledFrame.frame.contains(globalPoint) {
|
|
145
|
-
print("Hit labeled frame: \(labeledFrame.label)")
|
|
145
|
+
//print("Hit labeled frame: \(labeledFrame.label)")
|
|
146
146
|
// If it's in a labeled frame, let the overlay handle it
|
|
147
147
|
if overlayView.point(inside: overlayPoint, with: event),
|
|
148
148
|
let overlayHitView = overlayView.hitTest(overlayPoint, with: event) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-stream-call",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Uses the https://getstream.io/ SDK to implement calling in Capacitor",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"ios",
|
|
40
40
|
"android",
|
|
41
41
|
"web",
|
|
42
|
-
"capacitor-plugin"
|
|
42
|
+
"capacitor-plugin"
|
|
43
43
|
],
|
|
44
44
|
"scripts": {
|
|
45
45
|
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|