@capgo/capacitor-stream-call 0.0.83 → 0.0.85

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.
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
13
13
  s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
14
  s.ios.deployment_target = '14.0'
15
15
  s.dependency 'Capacitor'
16
- s.dependency 'StreamVideo', '1.24.0'
17
- s.dependency 'StreamVideoSwiftUI', '1.24.0'
16
+ s.dependency 'StreamVideo', '1.28.0'
17
+ s.dependency 'StreamVideoSwiftUI', '1.28.0'
18
18
  s.swift_version = '5.1'
19
19
  end
package/Package.swift CHANGED
@@ -11,7 +11,7 @@ let package = Package(
11
11
  ],
12
12
  dependencies: [
13
13
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0"),
14
- .package(url: "https://github.com/GetStream/stream-video-swift.git", exact: "1.24.0")
14
+ .package(url: "https://github.com/GetStream/stream-video-swift.git", exact: "1.28.0")
15
15
  ],
16
16
  targets: [
17
17
  .target(
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
  <a href="https://capgo.app/"><img src='https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png' alt='Capgo - Instant updates for capacitor'/></a>
3
3
 
4
4
  <div align="center">
5
- <h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo 🚀</a></h2>
6
- <h2><a href="https://capgo.app/consulting/?ref=plugin"> Fix your annoying bug now, Hire a Capacitor expert 💪</a></h2>
5
+ <h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo</a></h2>
6
+ <h2><a href="https://capgo.app/consulting/?ref=plugin"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
7
7
  </div>
8
8
 
9
9
  A Capacitor plugin that uses the [Stream Video SDK](https://getstream.io/) to enable video calling functionality in your app.
@@ -75,8 +75,8 @@ dependencies {
75
75
  implementation "androidx.compose.material3:material3:1.3.2"
76
76
 
77
77
  // Stream dependencies
78
- implementation("io.getstream:stream-video-android-ui-compose:1.9.1")
79
- implementation("io.getstream:stream-video-android-core:1.9.1")
78
+ implementation("io.getstream:stream-video-android-ui-compose:1.9.2")
79
+ implementation("io.getstream:stream-video-android-core:1.9.2")
80
80
  implementation("io.getstream:stream-android-push:1.3.2")
81
81
  implementation("io.getstream:stream-android-push-firebase:1.3.2")
82
82
 
@@ -110,6 +110,7 @@ class StreamCallPlugin : Plugin() {
110
110
  private var cameraStatusJob: Job? = null
111
111
  private var microphoneStatusJob: Job? = null
112
112
  private var lastEventSent: String? = null
113
+ private var callIsAudioOnly: Boolean = false
113
114
 
114
115
  // Store current call info
115
116
  private var currentCallId: String = ""
@@ -132,6 +133,7 @@ class StreamCallPlugin : Plugin() {
132
133
  private var pendingCallTeam: String? = null
133
134
  private var pendingCustomObject: JSObject? = null
134
135
  private var pendingAcceptCall: Call? = null // Store the actual call object for acceptance
136
+ private var pendingSetCameraCall: PluginCall? = null
135
137
 
136
138
  private enum class State {
137
139
  NOT_INITIALIZED,
@@ -162,7 +164,7 @@ class StreamCallPlugin : Plugin() {
162
164
  Log.d("StreamCallPlugin", "handleOnResume: Permission attempt count: $permissionAttemptCount")
163
165
 
164
166
  // Check if permissions were granted after returning from settings or permission dialog
165
- if (checkPermissions()) {
167
+ if (checkPermissions(this.callIsAudioOnly)) {
166
168
  Log.d("StreamCallPlugin", "handleOnResume: Permissions are now granted")
167
169
  // Handle any pending calls that were waiting for permissions
168
170
  handlePermissionGranted()
@@ -181,11 +183,11 @@ class StreamCallPlugin : Plugin() {
181
183
  if (pendingAcceptCall != null) {
182
184
  Log.d("StreamCallPlugin", "handleOnResume: Have active call waiting for permissions, requesting now")
183
185
  permissionAttemptCount = 0
184
- requestPermissions()
186
+ requestPermissions(this.callIsAudioOnly)
185
187
  } else if (pendingCall != null && pendingCallUserIds != null) {
186
188
  Log.d("StreamCallPlugin", "handleOnResume: Have outgoing call waiting for permissions, requesting now")
187
189
  permissionAttemptCount = 0
188
- requestPermissions()
190
+ requestPermissions(this.callIsAudioOnly)
189
191
  }
190
192
  }
191
193
  } else {
@@ -194,11 +196,20 @@ class StreamCallPlugin : Plugin() {
194
196
  }
195
197
 
196
198
  override fun load() {
199
+ try {
200
+ val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
201
+ if (packageInfo.firstInstallTime == packageInfo.lastUpdateTime) {
202
+ Log.d("StreamCallPlugin", "Fresh install detected, clearing user credentials.")
203
+ SecureUserRepository.getInstance(context).removeCurrentUser()
204
+ }
205
+ } catch (e: Exception) {
206
+ Log.e("StreamCallPlugin", "Error checking for fresh install", e)
207
+ }
197
208
  // general init
198
209
  initializeStreamVideo()
199
210
  setupViews()
200
211
  super.load()
201
- checkPermissions()
212
+ checkPermissions(this.callIsAudioOnly)
202
213
  // Register broadcast receiver for ACCEPT_CALL action with high priority
203
214
  val filter = IntentFilter("io.getstream.video.android.action.ACCEPT_CALL")
204
215
  filter.priority = 999 // Set high priority to ensure it captures the intent
@@ -296,7 +307,9 @@ class StreamCallPlugin : Plugin() {
296
307
  Log.d("StreamCallPlugin", " [$index] ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
297
308
  }
298
309
  kotlinx.coroutines.GlobalScope.launch {
299
- internalAcceptCall(call, requestPermissionsAfter = !checkPermissions())
310
+ val isAudioOnly = getIsAudioOnly(call)
311
+ this@StreamCallPlugin.callIsAudioOnly = isAudioOnly
312
+ internalAcceptCall(call, requestPermissionsAfter = !checkPermissions(isAudioOnly))
300
313
  }
301
314
  bringAppToForeground()
302
315
  } else {
@@ -1016,31 +1029,21 @@ class StreamCallPlugin : Plugin() {
1016
1029
 
1017
1030
  @PluginMethod
1018
1031
  fun acceptCall(call: PluginCall) {
1019
- Log.d("StreamCallPlugin", "acceptCall called")
1020
- try {
1021
- val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
1022
- if (streamVideoCall == null) {
1023
- call.reject("Ringing call is null")
1024
- return
1025
- }
1026
-
1027
- Log.d("StreamCallPlugin", "acceptCall: Accepting call immediately, will handle permissions after")
1028
-
1029
- // Accept call immediately regardless of permissions - time is critical!
1032
+ val ringingCall = streamVideoClient?.state?.ringingCall?.value
1033
+ if (ringingCall != null) {
1030
1034
  kotlinx.coroutines.GlobalScope.launch {
1031
1035
  try {
1032
- internalAcceptCall(streamVideoCall, requestPermissionsAfter = !checkPermissions())
1036
+ val isAudioOnly = getIsAudioOnly(ringingCall)
1037
+ internalAcceptCall(ringingCall, requestPermissionsAfter = !checkPermissions(isAudioOnly))
1033
1038
  call.resolve(JSObject().apply {
1034
1039
  put("success", true)
1035
1040
  })
1036
1041
  } catch (e: Exception) {
1037
- Log.e("StreamCallPlugin", "Error accepting call", e)
1038
1042
  call.reject("Failed to accept call: ${e.message}")
1039
1043
  }
1040
1044
  }
1041
- } catch (t: Throwable) {
1042
- Log.d("StreamCallPlugin", "JS -> acceptCall fail", t)
1043
- call.reject("Cannot acceptCall")
1045
+ } else {
1046
+ call.reject("No ringing call")
1044
1047
  }
1045
1048
  }
1046
1049
 
@@ -1068,6 +1071,9 @@ class StreamCallPlugin : Plugin() {
1068
1071
 
1069
1072
  kotlinx.coroutines.GlobalScope.launch {
1070
1073
  try {
1074
+ val isAudioOnly = getIsAudioOnly(call)
1075
+ this@StreamCallPlugin.callIsAudioOnly = isAudioOnly
1076
+
1071
1077
  Log.d("StreamCallPlugin", "internalAcceptCall: Coroutine started for call ${call.id}")
1072
1078
 
1073
1079
  // Hide incoming call view first
@@ -1097,15 +1103,15 @@ class StreamCallPlugin : Plugin() {
1097
1103
  Log.d("StreamCallPlugin", "internalAcceptCall: WebView background set to transparent for call ${call.id}")
1098
1104
  bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
1099
1105
  Log.d("StreamCallPlugin", "internalAcceptCall: WebView brought to front for call ${call.id}")
1100
-
1106
+
1101
1107
  // Enable camera/microphone based on permissions
1102
- val hasPermissions = checkPermissions()
1108
+ val hasPermissions = checkPermissions(isAudioOnly)
1103
1109
  Log.d("StreamCallPlugin", "internalAcceptCall: Has permissions: $hasPermissions for call ${call.id}")
1104
-
1110
+
1105
1111
  call.microphone.setEnabled(hasPermissions)
1106
- call.camera.setEnabled(hasPermissions)
1112
+ call.camera.setEnabled(hasPermissions && !isAudioOnly)
1107
1113
  Log.d("StreamCallPlugin", "internalAcceptCall: Microphone and camera set to $hasPermissions for call ${call.id}")
1108
-
1114
+
1109
1115
  Log.d("StreamCallPlugin", "internalAcceptCall: Setting CallContent with active call ${call.id}")
1110
1116
  setOverlayContent(call)
1111
1117
  Log.d("StreamCallPlugin", "internalAcceptCall: Content set for overlayView for call ${call.id}")
@@ -1117,7 +1123,7 @@ class StreamCallPlugin : Plugin() {
1117
1123
  parent?.removeView(overlayView)
1118
1124
  parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
1119
1125
  Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView re-added to parent at index 0 for call ${call.id}")
1120
-
1126
+
1121
1127
  // Add a small delay to ensure UI refresh
1122
1128
  mainHandler.postDelayed({
1123
1129
  Log.d("StreamCallPlugin", "internalAcceptCall: Delayed UI check, overlay visible: ${overlayView?.isVisible} for call ${call.id}")
@@ -1148,10 +1154,10 @@ class StreamCallPlugin : Plugin() {
1148
1154
  pendingAcceptCall = call
1149
1155
  Log.d("StreamCallPlugin", "internalAcceptCall: Set pendingAcceptCall to ${call.id}, resetting attempt count")
1150
1156
  permissionAttemptCount = 0
1151
- requestPermissions()
1157
+ requestPermissions(isAudioOnly)
1152
1158
  }
1153
1159
  }
1154
-
1160
+
1155
1161
  } catch (e: Exception) {
1156
1162
  Log.e("StreamCallPlugin", "internalAcceptCall: Error accepting call ${call.id}: ${e.message}", e)
1157
1163
  runOnMainThread {
@@ -1166,10 +1172,17 @@ class StreamCallPlugin : Plugin() {
1166
1172
  }
1167
1173
 
1168
1174
  // Function to check required permissions
1169
- private fun checkPermissions(): Boolean {
1170
- Log.d("StreamCallPlugin", "checkPermissions: Entered")
1175
+ private fun checkPermissions(isAudioOnly: Boolean = false): Boolean {
1176
+ Log.d("StreamCallPlugin", "checkPermissions: Entered, isAudioOnly: $isAudioOnly")
1171
1177
  val audioPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
1172
1178
  Log.d("StreamCallPlugin", "checkPermissions: RECORD_AUDIO permission status: $audioPermission (Granted=${PackageManager.PERMISSION_GRANTED})")
1179
+
1180
+ if (isAudioOnly) {
1181
+ val allGranted = audioPermission == PackageManager.PERMISSION_GRANTED
1182
+ Log.d("StreamCallPlugin", "checkPermissions: Audio only call, all permissions granted: $allGranted")
1183
+ return allGranted
1184
+ }
1185
+
1173
1186
  val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
1174
1187
  Log.d("StreamCallPlugin", "checkPermissions: CAMERA permission status: $cameraPermission (Granted=${PackageManager.PERMISSION_GRANTED})")
1175
1188
  val allGranted = audioPermission == PackageManager.PERMISSION_GRANTED && cameraPermission == PackageManager.PERMISSION_GRANTED
@@ -1222,8 +1235,9 @@ class StreamCallPlugin : Plugin() {
1222
1235
  // Determine what type of pending operation we have
1223
1236
  val hasOutgoingCall = pendingCall != null && pendingCallUserIds != null
1224
1237
  val hasActiveCallNeedingPermissions = pendingAcceptCall != null
1238
+ val hasPendingSetCamera = pendingSetCameraCall != null
1225
1239
 
1226
- Log.d("StreamCallPlugin", "handlePermissionGranted: hasOutgoingCall=$hasOutgoingCall, hasActiveCallNeedingPermissions=$hasActiveCallNeedingPermissions")
1240
+ Log.d("StreamCallPlugin", "handlePermissionGranted: hasOutgoingCall=$hasOutgoingCall, hasActiveCallNeedingPermissions=$hasActiveCallNeedingPermissions, hasPendingSetCamera=$hasPendingSetCamera")
1227
1241
 
1228
1242
  when {
1229
1243
  hasOutgoingCall -> {
@@ -1246,7 +1260,7 @@ class StreamCallPlugin : Plugin() {
1246
1260
  runOnMainThread {
1247
1261
  try {
1248
1262
  callToHandle.microphone.setEnabled(true)
1249
- callToHandle.camera.setEnabled(true)
1263
+ callToHandle.camera.setEnabled(!this.callIsAudioOnly)
1250
1264
  Log.d("StreamCallPlugin", "handlePermissionGranted: Camera and microphone enabled for call ${callToHandle.id}")
1251
1265
 
1252
1266
  // Show success message
@@ -1282,7 +1296,7 @@ class StreamCallPlugin : Plugin() {
1282
1296
  runOnMainThread {
1283
1297
  try {
1284
1298
  callToHandle.microphone.setEnabled(true)
1285
- callToHandle.camera.setEnabled(true)
1299
+ callToHandle.camera.setEnabled(!this.callIsAudioOnly)
1286
1300
  Log.d("StreamCallPlugin", "handlePermissionGranted: Camera and microphone enabled for stored call ${callToHandle.id}")
1287
1301
 
1288
1302
  android.widget.Toast.makeText(
@@ -1298,6 +1312,32 @@ class StreamCallPlugin : Plugin() {
1298
1312
  }
1299
1313
  }
1300
1314
 
1315
+ hasPendingSetCamera -> {
1316
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Handling pending setCameraEnabled call.")
1317
+ val callToHandle = pendingSetCameraCall!!
1318
+ val activeCall = streamVideoClient?.state?.activeCall?.value
1319
+
1320
+ if (activeCall != null) {
1321
+ kotlinx.coroutines.GlobalScope.launch {
1322
+ try {
1323
+ activeCall.camera.setEnabled(true)
1324
+ this@StreamCallPlugin.callIsAudioOnly = false
1325
+ callToHandle.resolve(JSObject().apply {
1326
+ put("success", true)
1327
+ })
1328
+ } catch (e: Exception) {
1329
+ Log.e("StreamCallPlugin", "Error enabling camera after permission grant", e)
1330
+ callToHandle.reject("Failed to enable camera after permission grant: ${e.message}")
1331
+ } finally {
1332
+ clearPendingCall()
1333
+ }
1334
+ }
1335
+ } else {
1336
+ callToHandle.reject("No active call found to enable camera.")
1337
+ clearPendingCall()
1338
+ }
1339
+ }
1340
+
1301
1341
  pendingCall != null -> {
1302
1342
  // We have a pending call but unclear what type - fallback handling
1303
1343
  Log.w("StreamCallPlugin", "handlePermissionGranted: Have pendingCall but unclear operation type")
@@ -1348,7 +1388,7 @@ class StreamCallPlugin : Plugin() {
1348
1388
  } else if (permissionAttemptCount < 2) {
1349
1389
  // Try asking again immediately if this is the first denial
1350
1390
  Log.d("StreamCallPlugin", "handlePermissionDenied: First denial (attempt $permissionAttemptCount), asking again immediately")
1351
- requestPermissions() // This will increment the attempt count
1391
+ requestPermissions(this.callIsAudioOnly) // This will increment the attempt count
1352
1392
  } else {
1353
1393
  // Second denial - show settings dialog (final ask)
1354
1394
  Log.d("StreamCallPlugin", "handlePermissionDenied: Second denial (attempt $permissionAttemptCount), showing settings dialog (final ask)")
@@ -1371,7 +1411,7 @@ class StreamCallPlugin : Plugin() {
1371
1411
  clearPendingCall()
1372
1412
 
1373
1413
  // Execute the call creation logic
1374
- createAndStartCall(call, userIds, callType, shouldRing, team, custom)
1414
+ createAndStartCall(call, userIds, callType, shouldRing, team, custom, this.callIsAudioOnly)
1375
1415
  } else {
1376
1416
  Log.w("StreamCallPlugin", "executePendingCall: Missing pending call data")
1377
1417
  call?.reject("Internal error: missing call parameters")
@@ -1386,14 +1426,14 @@ class StreamCallPlugin : Plugin() {
1386
1426
  pendingCallShouldRing = null
1387
1427
  pendingCallTeam = null
1388
1428
  pendingAcceptCall = null
1389
- pendingCallTeam = null
1429
+ pendingSetCameraCall = null
1390
1430
  permissionAttemptCount = 0 // Reset attempt count when clearing
1391
1431
  }
1392
1432
 
1393
1433
 
1394
1434
 
1395
1435
  @OptIn(DelicateCoroutinesApi::class, InternalStreamVideoApi::class)
1396
- private fun createAndStartCall(call: PluginCall, userIds: List<String>, callType: String, shouldRing: Boolean, team: String?, custom: JSObject?) {
1436
+ private fun createAndStartCall(call: PluginCall, userIds: List<String>, callType: String, shouldRing: Boolean, team: String?, custom: JSObject?, isAudioOnly: Boolean) {
1397
1437
  val selfUserId = streamVideoClient?.userId
1398
1438
  if (selfUserId == null) {
1399
1439
  call.reject("No self-user id found. Are you not logged in?")
@@ -1419,6 +1459,7 @@ class StreamCallPlugin : Plugin() {
1419
1459
  custom = custom?.toMap() ?: emptyMap(),
1420
1460
  ring = shouldRing,
1421
1461
  team = team,
1462
+ video = !isAudioOnly
1422
1463
  )
1423
1464
 
1424
1465
  if (createResult?.isFailure == true) {
@@ -1429,7 +1470,7 @@ class StreamCallPlugin : Plugin() {
1429
1470
  // Show overlay view
1430
1471
  activity?.runOnUiThread {
1431
1472
  streamCall?.microphone?.setEnabled(true)
1432
- streamCall?.camera?.setEnabled(true)
1473
+ streamCall?.camera?.setEnabled(!isAudioOnly)
1433
1474
 
1434
1475
  bridge?.webView?.setBackgroundColor(Color.TRANSPARENT) // Make webview transparent
1435
1476
  bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
@@ -1453,21 +1494,27 @@ class StreamCallPlugin : Plugin() {
1453
1494
  }
1454
1495
 
1455
1496
  // Function to request required permissions
1456
- private fun requestPermissions() {
1497
+ private fun requestPermissions(isAudioOnly: Boolean) {
1457
1498
  permissionAttemptCount++
1458
- Log.d("StreamCallPlugin", "requestPermissions: Attempt #$permissionAttemptCount - Requesting RECORD_AUDIO and CAMERA permissions.")
1499
+ Log.d("StreamCallPlugin", "requestPermissions: Attempt #$permissionAttemptCount - Requesting permissions. isAudioOnly: $isAudioOnly")
1459
1500
 
1460
1501
  // Record timing for instant denial detection
1461
1502
  permissionRequestStartTime = System.currentTimeMillis()
1462
1503
  Log.d("StreamCallPlugin", "requestPermissions: Starting permission request at $permissionRequestStartTime")
1463
1504
 
1505
+ val permissionsToRequest = if (isAudioOnly) {
1506
+ arrayOf(Manifest.permission.RECORD_AUDIO)
1507
+ } else {
1508
+ arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
1509
+ }
1510
+
1464
1511
  ActivityCompat.requestPermissions(
1465
1512
  activity,
1466
- arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA),
1513
+ permissionsToRequest,
1467
1514
  9001 // Use high request code to avoid Capacitor conflicts
1468
1515
  )
1469
1516
 
1470
- Log.d("StreamCallPlugin", "requestPermissions: Permission request initiated with code 9001")
1517
+ Log.d("StreamCallPlugin", "requestPermissions: Permission request initiated with code 9001 for permissions: ${permissionsToRequest.joinToString()}")
1471
1518
  }
1472
1519
 
1473
1520
  private fun showPermissionSettingsDialog() {
@@ -1531,6 +1578,7 @@ class StreamCallPlugin : Plugin() {
1531
1578
 
1532
1579
  val hasOutgoingCall = pendingCall != null && pendingCallUserIds != null
1533
1580
  val hasIncomingCall = pendingCall != null && pendingAcceptCall != null
1581
+ val hasPendingSetCamera = pendingSetCameraCall != null
1534
1582
  val activeCall = streamVideoClient?.state?.activeCall?.value
1535
1583
 
1536
1584
  when {
@@ -1541,6 +1589,12 @@ class StreamCallPlugin : Plugin() {
1541
1589
  clearPendingCall()
1542
1590
  }
1543
1591
 
1592
+ hasPendingSetCamera -> {
1593
+ Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Rejecting pending setCameraEnabled call")
1594
+ pendingSetCameraCall?.reject("Camera permission is required to enable the camera.")
1595
+ clearPendingCall()
1596
+ }
1597
+
1544
1598
  hasIncomingCall && activeCall != null && activeCall.id == pendingAcceptCall?.id -> {
1545
1599
  // Incoming call that's already active - DON'T end the call, just keep it without camera/mic
1546
1600
  Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Incoming call already active, keeping call without camera/mic")
@@ -1729,27 +1783,52 @@ class StreamCallPlugin : Plugin() {
1729
1783
  return
1730
1784
  }
1731
1785
 
1732
- try {
1733
- val activeCall = streamVideoClient?.state?.activeCall
1734
- if (activeCall == null) {
1735
- call.reject("No active call")
1736
- return
1786
+ val activeCall = streamVideoClient?.state?.activeCall?.value
1787
+ if (activeCall == null) {
1788
+ call.reject("No active call")
1789
+ return
1790
+ }
1791
+
1792
+ if (!enabled) {
1793
+ // Just disable, no permission needed
1794
+ kotlinx.coroutines.GlobalScope.launch {
1795
+ try {
1796
+ activeCall.camera.setEnabled(false)
1797
+ call.resolve(JSObject().apply {
1798
+ put("success", true)
1799
+ })
1800
+ } catch (e: Exception) {
1801
+ Log.e("StreamCallPlugin", "Error disabling camera: ${e.message}")
1802
+ call.reject("Failed to disable camera: ${e.message}")
1803
+ }
1737
1804
  }
1805
+ return
1806
+ }
1738
1807
 
1808
+ // From here, enabled is true. We need to check for permission.
1809
+ val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
1810
+ if (cameraPermission == PackageManager.PERMISSION_GRANTED) {
1811
+ // Permission is already granted
1739
1812
  kotlinx.coroutines.GlobalScope.launch {
1740
1813
  try {
1741
- activeCall.value?.camera?.setEnabled(enabled)
1814
+ activeCall.camera.setEnabled(true)
1815
+ // When we enable camera, the call is no longer audio-only
1816
+ this@StreamCallPlugin.callIsAudioOnly = false
1742
1817
  call.resolve(JSObject().apply {
1743
1818
  put("success", true)
1744
1819
  })
1745
1820
  } catch (e: Exception) {
1746
- Log.e("StreamCallPlugin", "Error setting camera: ${e.message}")
1747
- call.reject("Failed to set camera: ${e.message}")
1821
+ Log.e("StreamCallPlugin", "Error enabling camera: ${e.message}")
1822
+ call.reject("Failed to enable camera: ${e.message}")
1748
1823
  }
1749
1824
  }
1750
- } catch (e: Exception) {
1751
- Log.e("StreamCallPlugin", "Error setting camera: ${e.message}")
1752
- call.reject("StreamVideo not initialized")
1825
+ } else {
1826
+ // Permission is not granted, request it.
1827
+ Log.d("StreamCallPlugin", "Camera permission not granted. Requesting permission.")
1828
+ this.pendingSetCameraCall = call
1829
+ // we are enabling camera, so it's not an audio only call
1830
+ requestPermissions(false)
1831
+ // The call will be resolved/rejected in the permission result handlers
1753
1832
  }
1754
1833
  }
1755
1834
 
@@ -1768,7 +1847,7 @@ class StreamCallPlugin : Plugin() {
1768
1847
 
1769
1848
  // Use call.state.totalParticipants to get participant count (as per StreamVideo Android SDK docs)
1770
1849
  val totalParticipants = call.state.totalParticipants.value
1771
- val shouldEndCall = isCreator || totalParticipants <= 1
1850
+ val shouldEndCall = isCreator || totalParticipants <= 2
1772
1851
 
1773
1852
  Log.d("StreamCallPlugin", "Call $callId - Creator: $createdBy, CurrentUser: $currentUserId, IsCreator: $isCreator, TotalParticipants: $totalParticipants, ShouldEnd: $shouldEndCall")
1774
1853
 
@@ -1808,7 +1887,6 @@ class StreamCallPlugin : Plugin() {
1808
1887
  val keyguardManager = ctx.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
1809
1888
  if (keyguardManager.isKeyguardLocked) {
1810
1889
  // we allow kill exclusively here
1811
- // the idea is that:
1812
1890
  // the 'empty' instance of this plugin class gets created in application
1813
1891
  // then, it handles a notification and setts the context (this.savedContext)
1814
1892
  // if the context is new
@@ -1958,6 +2036,9 @@ class StreamCallPlugin : Plugin() {
1958
2036
  val shouldRing = call.getBoolean("ring") ?: true
1959
2037
  val callId = java.util.UUID.randomUUID().toString()
1960
2038
  val team = call.getString("team")
2039
+ val isAudioOnly = custom?.getBoolean("audio_only") ?: false
2040
+ this.callIsAudioOnly = isAudioOnly
2041
+
1961
2042
 
1962
2043
  Log.d("StreamCallPlugin", "Creating call:")
1963
2044
  Log.d("StreamCallPlugin", "- Call ID: $callId")
@@ -1969,7 +2050,7 @@ class StreamCallPlugin : Plugin() {
1969
2050
  }
1970
2051
 
1971
2052
  // Check permissions before creating the call
1972
- if (!checkPermissions()) {
2053
+ if (!checkPermissions(isAudioOnly)) {
1973
2054
  Log.d("StreamCallPlugin", "Permissions not granted, storing call parameters and requesting permissions")
1974
2055
  // Store call parameters for later execution
1975
2056
  pendingCall = call
@@ -1982,12 +2063,12 @@ class StreamCallPlugin : Plugin() {
1982
2063
  }
1983
2064
  // Reset attempt count for new permission flow
1984
2065
  permissionAttemptCount = 0
1985
- requestPermissions()
2066
+ requestPermissions(isAudioOnly)
1986
2067
  return // Don't reject immediately, wait for permission result
1987
2068
  }
1988
2069
 
1989
2070
  // Execute call creation immediately if permissions are granted
1990
- createAndStartCall(call, userIds, callType, shouldRing, team, custom)
2071
+ createAndStartCall(call, userIds, callType, shouldRing, team, custom, isAudioOnly)
1991
2072
  } catch (e: Exception) {
1992
2073
  call.reject("Failed to make call: ${e.message}")
1993
2074
  }
@@ -2341,13 +2422,18 @@ class StreamCallPlugin : Plugin() {
2341
2422
  fun joinCall(call: PluginCall) {
2342
2423
  val fragment = callFragment
2343
2424
  if (fragment != null && fragment.getCall() != null) {
2344
- if (!checkPermissions()) {
2345
- requestPermissions()
2346
- call.reject("Permissions required for call. Please grant them.")
2347
- return
2348
- }
2425
+ val activeCall = fragment.getCall()!!
2426
+ // I need to get custom data here, which is async.
2427
+ // The method is not suspend.
2428
+ // Let's launch a coroutine
2349
2429
  CoroutineScope(Dispatchers.Main).launch {
2350
- fragment.getCall()?.join()
2430
+ val isAudioOnly = getIsAudioOnly(activeCall)
2431
+ if (!checkPermissions(isAudioOnly)) {
2432
+ requestPermissions(isAudioOnly)
2433
+ call.reject("Permissions required for call. Please grant them.")
2434
+ return@launch
2435
+ }
2436
+ activeCall.join()
2351
2437
  call.resolve()
2352
2438
  }
2353
2439
  } else {
@@ -2386,7 +2472,9 @@ class StreamCallPlugin : Plugin() {
2386
2472
  if (call != null) {
2387
2473
  Log.d("StreamCallPlugin", "BroadcastReceiver: Accepting call with cid: $cid")
2388
2474
  kotlinx.coroutines.GlobalScope.launch {
2389
- internalAcceptCall(call, requestPermissionsAfter = !checkPermissions())
2475
+ val isAudioOnly = getIsAudioOnly(call)
2476
+ this@StreamCallPlugin.callIsAudioOnly = isAudioOnly
2477
+ internalAcceptCall(call, requestPermissionsAfter = !checkPermissions(isAudioOnly))
2390
2478
  }
2391
2479
  bringAppToForeground()
2392
2480
  } else {
@@ -2428,4 +2516,14 @@ class StreamCallPlugin : Plugin() {
2428
2516
  private const val API_KEY_PREFS_NAME = "stream_video_api_key_prefs"
2429
2517
  private const val DYNAMIC_API_KEY_PREF = "dynamic_api_key"
2430
2518
  }
2519
+
2520
+ private suspend fun getIsAudioOnly(call: Call): Boolean {
2521
+ val callInfoResult = call.get()
2522
+ return if (callInfoResult.isSuccess) {
2523
+ val audioOnlyValue = callInfoResult.getOrNull()?.call?.custom?.get("audio_only")
2524
+ audioOnlyValue?.toString() == "true"
2525
+ } else {
2526
+ false
2527
+ }
2528
+ }
2431
2529
  }
@@ -869,7 +869,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
869
869
  DispatchQueue.main.async {
870
870
  Task { @MainActor in
871
871
  self.callViewModel = CallViewModel(participantsLayout: .grid)
872
- self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
872
+ // self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
873
873
 
874
874
  // Setup subscriptions for new StreamVideo instance
875
875
  self.setupActiveCallSubscription()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-stream-call",
3
- "version": "0.0.83",
3
+ "version": "0.0.85",
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",