@stream-io/video-react-native-sdk 1.29.4-beta.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CHANGELOG.md +3162 -0
  2. package/android/src/main/AndroidManifest.xml +1 -8
  3. package/android/src/main/AndroidManifestNew.xml +0 -11
  4. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +5 -42
  5. package/android/src/main/java/com/streamvideo/reactnative/audio/utils/WebRtcAudioUtils.kt +6 -70
  6. package/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt +4 -6
  7. package/android/src/main/java/com/streamvideo/reactnative/util/CallAliveServiceChecker.kt +95 -0
  8. package/dist/commonjs/components/Call/CallContent/CallContent.js +13 -7
  9. package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
  10. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +50 -14
  11. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
  12. package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js +27 -0
  13. package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
  14. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
  15. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  16. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js +12 -9
  17. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  18. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js +19 -4
  19. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  20. package/dist/commonjs/hooks/push/index.js +2 -0
  21. package/dist/commonjs/hooks/push/index.js.map +1 -1
  22. package/dist/commonjs/hooks/push/useIosCallkeepWithCallingStateEffect.js +160 -0
  23. package/dist/commonjs/hooks/push/useIosCallkeepWithCallingStateEffect.js.map +1 -0
  24. package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js +31 -18
  25. package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
  26. package/dist/commonjs/hooks/push/useProcessPushCallEffect.js +67 -0
  27. package/dist/commonjs/hooks/push/useProcessPushCallEffect.js.map +1 -0
  28. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js +97 -64
  29. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  30. package/dist/commonjs/index.js +0 -1
  31. package/dist/commonjs/index.js.map +1 -1
  32. package/dist/commonjs/modules/call-manager/CallManager.js +0 -26
  33. package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
  34. package/dist/commonjs/providers/StreamCall/index.js +6 -6
  35. package/dist/commonjs/providers/StreamCall/index.js.map +1 -1
  36. package/dist/commonjs/utils/StreamVideoRN/index.js +21 -33
  37. package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
  38. package/dist/commonjs/utils/hooks/index.js +0 -11
  39. package/dist/commonjs/utils/hooks/index.js.map +1 -1
  40. package/dist/commonjs/utils/internal/registerSDKGlobals.js +3 -52
  41. package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
  42. package/dist/commonjs/utils/push/android.js +202 -151
  43. package/dist/commonjs/utils/push/android.js.map +1 -1
  44. package/dist/commonjs/utils/push/internal/ios.js +34 -17
  45. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  46. package/dist/commonjs/utils/push/internal/rxSubjects.js +45 -1
  47. package/dist/commonjs/utils/push/internal/rxSubjects.js.map +1 -1
  48. package/dist/commonjs/utils/push/internal/utils.js +20 -32
  49. package/dist/commonjs/utils/push/internal/utils.js.map +1 -1
  50. package/dist/commonjs/utils/push/ios.js.map +1 -1
  51. package/dist/commonjs/utils/push/libs/callkeep.js +17 -0
  52. package/dist/commonjs/utils/push/libs/callkeep.js.map +1 -0
  53. package/dist/commonjs/utils/push/libs/index.js +19 -8
  54. package/dist/commonjs/utils/push/libs/index.js.map +1 -1
  55. package/dist/commonjs/utils/push/libs/notifee/index.js +19 -0
  56. package/dist/commonjs/utils/push/libs/notifee/index.js.map +1 -1
  57. package/dist/commonjs/utils/push/libs/voipPushNotification.js +17 -0
  58. package/dist/commonjs/utils/push/libs/voipPushNotification.js.map +1 -0
  59. package/dist/commonjs/utils/push/setupIosCallKeepEvents.js +205 -0
  60. package/dist/commonjs/utils/push/setupIosCallKeepEvents.js.map +1 -0
  61. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +6 -7
  62. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
  63. package/dist/commonjs/version.js +1 -1
  64. package/dist/commonjs/version.js.map +1 -1
  65. package/dist/module/components/Call/CallContent/CallContent.js +10 -4
  66. package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
  67. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +52 -16
  68. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
  69. package/dist/module/components/Call/CallContent/RTCViewPipNative.js +27 -0
  70. package/dist/module/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
  71. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
  72. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  73. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js +15 -12
  74. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  75. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js +20 -5
  76. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  77. package/dist/module/hooks/push/index.js +2 -0
  78. package/dist/module/hooks/push/index.js.map +1 -1
  79. package/dist/module/hooks/push/useIosCallkeepWithCallingStateEffect.js +153 -0
  80. package/dist/module/hooks/push/useIosCallkeepWithCallingStateEffect.js.map +1 -0
  81. package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js +31 -18
  82. package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
  83. package/dist/module/hooks/push/useProcessPushCallEffect.js +60 -0
  84. package/dist/module/hooks/push/useProcessPushCallEffect.js.map +1 -0
  85. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js +99 -66
  86. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  87. package/dist/module/index.js +0 -1
  88. package/dist/module/index.js.map +1 -1
  89. package/dist/module/modules/call-manager/CallManager.js +0 -26
  90. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  91. package/dist/module/providers/StreamCall/index.js +6 -6
  92. package/dist/module/providers/StreamCall/index.js.map +1 -1
  93. package/dist/module/utils/StreamVideoRN/index.js +21 -33
  94. package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
  95. package/dist/module/utils/hooks/index.js +0 -1
  96. package/dist/module/utils/hooks/index.js.map +1 -1
  97. package/dist/module/utils/internal/registerSDKGlobals.js +3 -52
  98. package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
  99. package/dist/module/utils/push/android.js +204 -153
  100. package/dist/module/utils/push/android.js.map +1 -1
  101. package/dist/module/utils/push/internal/ios.js +34 -17
  102. package/dist/module/utils/push/internal/ios.js.map +1 -1
  103. package/dist/module/utils/push/internal/rxSubjects.js +44 -0
  104. package/dist/module/utils/push/internal/rxSubjects.js.map +1 -1
  105. package/dist/module/utils/push/internal/utils.js +19 -29
  106. package/dist/module/utils/push/internal/utils.js.map +1 -1
  107. package/dist/module/utils/push/ios.js.map +1 -1
  108. package/dist/module/utils/push/libs/callkeep.js +11 -0
  109. package/dist/module/utils/push/libs/callkeep.js.map +1 -0
  110. package/dist/module/utils/push/libs/index.js +2 -1
  111. package/dist/module/utils/push/libs/index.js.map +1 -1
  112. package/dist/module/utils/push/libs/notifee/index.js +18 -0
  113. package/dist/module/utils/push/libs/notifee/index.js.map +1 -1
  114. package/dist/module/utils/push/libs/voipPushNotification.js +11 -0
  115. package/dist/module/utils/push/libs/voipPushNotification.js.map +1 -0
  116. package/dist/module/utils/push/setupIosCallKeepEvents.js +199 -0
  117. package/dist/module/utils/push/setupIosCallKeepEvents.js.map +1 -0
  118. package/dist/module/utils/push/setupIosVoipPushEvents.js +6 -7
  119. package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
  120. package/dist/module/version.js +1 -1
  121. package/dist/module/version.js.map +1 -1
  122. package/dist/typescript/components/Call/CallContent/CallContent.d.ts.map +1 -1
  123. package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -1
  124. package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts +18 -0
  125. package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts.map +1 -1
  126. package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts.map +1 -1
  127. package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts.map +1 -1
  128. package/dist/typescript/components/Call/CallParticipantsList/CallParticipantsList.d.ts.map +1 -1
  129. package/dist/typescript/hooks/push/index.d.ts.map +1 -1
  130. package/dist/typescript/hooks/push/useIosCallkeepWithCallingStateEffect.d.ts +5 -0
  131. package/dist/typescript/hooks/push/useIosCallkeepWithCallingStateEffect.d.ts.map +1 -0
  132. package/dist/typescript/hooks/push/useIosVoipPushEventsSetupEffect.d.ts.map +1 -1
  133. package/dist/typescript/hooks/push/useProcessPushCallEffect.d.ts +8 -0
  134. package/dist/typescript/hooks/push/useProcessPushCallEffect.d.ts.map +1 -0
  135. package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts.map +1 -1
  136. package/dist/typescript/index.d.ts +0 -1
  137. package/dist/typescript/index.d.ts.map +1 -1
  138. package/dist/typescript/modules/call-manager/CallManager.d.ts +0 -5
  139. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  140. package/dist/typescript/utils/StreamVideoRN/index.d.ts +2 -20
  141. package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
  142. package/dist/typescript/utils/StreamVideoRN/types.d.ts +29 -54
  143. package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
  144. package/dist/typescript/utils/hooks/index.d.ts +0 -1
  145. package/dist/typescript/utils/hooks/index.d.ts.map +1 -1
  146. package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
  147. package/dist/typescript/utils/push/android.d.ts +2 -1
  148. package/dist/typescript/utils/push/android.d.ts.map +1 -1
  149. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  150. package/dist/typescript/utils/push/internal/rxSubjects.d.ts +33 -0
  151. package/dist/typescript/utils/push/internal/rxSubjects.d.ts.map +1 -1
  152. package/dist/typescript/utils/push/internal/utils.d.ts +1 -8
  153. package/dist/typescript/utils/push/internal/utils.d.ts.map +1 -1
  154. package/dist/typescript/utils/push/ios.d.ts +2 -1
  155. package/dist/typescript/utils/push/ios.d.ts.map +1 -1
  156. package/dist/typescript/utils/push/libs/callkeep.d.ts +3 -0
  157. package/dist/typescript/utils/push/libs/callkeep.d.ts.map +1 -0
  158. package/dist/typescript/utils/push/libs/index.d.ts +2 -1
  159. package/dist/typescript/utils/push/libs/index.d.ts.map +1 -1
  160. package/dist/typescript/utils/push/libs/notifee/index.d.ts +1 -0
  161. package/dist/typescript/utils/push/libs/notifee/index.d.ts.map +1 -1
  162. package/dist/typescript/utils/push/libs/voipPushNotification.d.ts +3 -0
  163. package/dist/typescript/utils/push/libs/voipPushNotification.d.ts.map +1 -0
  164. package/dist/typescript/utils/push/setupIosCallKeepEvents.d.ts +6 -0
  165. package/dist/typescript/utils/push/setupIosCallKeepEvents.d.ts.map +1 -0
  166. package/dist/typescript/utils/push/setupIosVoipPushEvents.d.ts.map +1 -1
  167. package/dist/typescript/version.d.ts +1 -1
  168. package/dist/typescript/version.d.ts.map +1 -1
  169. package/expo-config-plugin/dist/withAndroidManifest.js +33 -1
  170. package/expo-config-plugin/dist/withAndroidPermissions.js +7 -2
  171. package/expo-config-plugin/dist/withAppDelegate.js +197 -19
  172. package/expo-config-plugin/dist/withMainActivity.js +1 -1
  173. package/expo-config-plugin/dist/withiOSInfoPlist.js +3 -2
  174. package/ios/PictureInPicture/PictureInPictureAvatarView.swift +273 -0
  175. package/ios/PictureInPicture/PictureInPictureConnectionQualityIndicator.swift +162 -0
  176. package/ios/PictureInPicture/PictureInPictureContent.swift +173 -0
  177. package/ios/PictureInPicture/PictureInPictureContentState.swift +123 -0
  178. package/ios/PictureInPicture/PictureInPictureDelegateProxy.swift +89 -0
  179. package/ios/PictureInPicture/PictureInPictureEnforcedStopAdapter.swift +166 -0
  180. package/ios/PictureInPicture/PictureInPictureLogger.swift +16 -0
  181. package/ios/PictureInPicture/PictureInPictureParticipantOverlayView.swift +217 -0
  182. package/ios/PictureInPicture/PictureInPictureReconnectionView.swift +193 -0
  183. package/ios/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +125 -7
  184. package/ios/PictureInPicture/StreamPictureInPictureController.swift +237 -63
  185. package/ios/PictureInPicture/StreamPictureInPictureControllerProtocol.swift +30 -0
  186. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +384 -12
  187. package/ios/RTCViewPip.swift +187 -21
  188. package/ios/RTCViewPipManager.mm +9 -0
  189. package/ios/RTCViewPipManager.swift +3 -3
  190. package/ios/StreamInCallManager.m +0 -2
  191. package/ios/StreamInCallManager.swift +7 -19
  192. package/ios/StreamVideoReactNative.h +4 -7
  193. package/ios/StreamVideoReactNative.m +82 -189
  194. package/package.json +19 -14
  195. package/src/components/Call/CallContent/CallContent.tsx +16 -8
  196. package/src/components/Call/CallContent/RTCViewPipIOS.tsx +81 -15
  197. package/src/components/Call/CallContent/RTCViewPipNative.tsx +36 -0
  198. package/src/components/Call/CallLayout/CallParticipantsGrid.tsx +28 -14
  199. package/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +19 -10
  200. package/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +20 -5
  201. package/src/hooks/push/index.ts +2 -0
  202. package/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts +235 -0
  203. package/src/hooks/push/useIosVoipPushEventsSetupEffect.ts +34 -21
  204. package/src/hooks/push/useProcessPushCallEffect.ts +108 -0
  205. package/src/hooks/useAndroidKeepCallAliveEffect.ts +120 -94
  206. package/src/index.ts +0 -1
  207. package/src/modules/call-manager/CallManager.ts +0 -36
  208. package/src/modules/call-manager/native-module.d.ts +0 -7
  209. package/src/providers/StreamCall/index.tsx +6 -6
  210. package/src/utils/StreamVideoRN/index.ts +30 -40
  211. package/src/utils/StreamVideoRN/types.ts +29 -56
  212. package/src/utils/hooks/index.ts +0 -1
  213. package/src/utils/internal/registerSDKGlobals.ts +4 -47
  214. package/src/utils/push/android.ts +309 -227
  215. package/src/utils/push/internal/ios.ts +44 -28
  216. package/src/utils/push/internal/rxSubjects.ts +61 -0
  217. package/src/utils/push/internal/utils.ts +26 -45
  218. package/src/utils/push/ios.ts +6 -1
  219. package/src/utils/push/libs/callkeep.ts +16 -0
  220. package/src/utils/push/libs/index.ts +2 -1
  221. package/src/utils/push/libs/notifee/index.ts +27 -0
  222. package/src/utils/push/libs/voipPushNotification.ts +17 -0
  223. package/src/utils/push/setupIosCallKeepEvents.ts +252 -0
  224. package/src/utils/push/setupIosVoipPushEvents.ts +7 -11
  225. package/src/version.ts +1 -1
  226. package/android/src/main/java/com/streamvideo/reactnative/keepalive/KeepAliveNotification.kt +0 -83
  227. package/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt +0 -149
  228. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +0 -121
  229. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +0 -1
  230. package/dist/commonjs/utils/hooks/useDebouncedValue.js +0 -24
  231. package/dist/commonjs/utils/hooks/useDebouncedValue.js.map +0 -1
  232. package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js +0 -58
  233. package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js.map +0 -1
  234. package/dist/commonjs/utils/internal/callingx/callingx.js +0 -109
  235. package/dist/commonjs/utils/internal/callingx/callingx.js.map +0 -1
  236. package/dist/commonjs/utils/keepCallAliveHeadlessTask.js +0 -48
  237. package/dist/commonjs/utils/keepCallAliveHeadlessTask.js.map +0 -1
  238. package/dist/commonjs/utils/push/libs/callingx.js +0 -75
  239. package/dist/commonjs/utils/push/libs/callingx.js.map +0 -1
  240. package/dist/commonjs/utils/push/setupCallingExpEvents.js +0 -108
  241. package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +0 -1
  242. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +0 -114
  243. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +0 -1
  244. package/dist/module/utils/hooks/useDebouncedValue.js +0 -19
  245. package/dist/module/utils/hooks/useDebouncedValue.js.map +0 -1
  246. package/dist/module/utils/internal/callingx/audioSessionPromise.js +0 -51
  247. package/dist/module/utils/internal/callingx/audioSessionPromise.js.map +0 -1
  248. package/dist/module/utils/internal/callingx/callingx.js +0 -100
  249. package/dist/module/utils/internal/callingx/callingx.js.map +0 -1
  250. package/dist/module/utils/keepCallAliveHeadlessTask.js +0 -42
  251. package/dist/module/utils/keepCallAliveHeadlessTask.js.map +0 -1
  252. package/dist/module/utils/push/libs/callingx.js +0 -67
  253. package/dist/module/utils/push/libs/callingx.js.map +0 -1
  254. package/dist/module/utils/push/setupCallingExpEvents.js +0 -102
  255. package/dist/module/utils/push/setupCallingExpEvents.js.map +0 -1
  256. package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts +0 -5
  257. package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +0 -1
  258. package/dist/typescript/utils/hooks/useDebouncedValue.d.ts +0 -8
  259. package/dist/typescript/utils/hooks/useDebouncedValue.d.ts.map +0 -1
  260. package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts +0 -16
  261. package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts.map +0 -1
  262. package/dist/typescript/utils/internal/callingx/callingx.d.ts +0 -14
  263. package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +0 -1
  264. package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts +0 -10
  265. package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts.map +0 -1
  266. package/dist/typescript/utils/push/libs/callingx.d.ts +0 -9
  267. package/dist/typescript/utils/push/libs/callingx.d.ts.map +0 -1
  268. package/dist/typescript/utils/push/setupCallingExpEvents.d.ts +0 -8
  269. package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +0 -1
  270. package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +0 -147
  271. package/src/utils/hooks/useDebouncedValue.ts +0 -21
  272. package/src/utils/internal/callingx/audioSessionPromise.ts +0 -53
  273. package/src/utils/internal/callingx/callingx.ts +0 -146
  274. package/src/utils/keepCallAliveHeadlessTask.ts +0 -54
  275. package/src/utils/push/libs/callingx.ts +0 -90
  276. package/src/utils/push/setupCallingExpEvents.ts +0 -130
@@ -0,0 +1,166 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import AVKit
6
+ import Combine
7
+ import UIKit
8
+
9
+ /// An adapter responsible for enforcing the stop of Picture-in-Picture
10
+ /// playback when the application returns to the foreground.
11
+ ///
12
+ /// This adapter listens to application state changes and PiP activity to ensure
13
+ /// PiP is stopped when the app becomes active (foreground). This behavior matches
14
+ /// iOS user expectations where PiP should dismiss when returning to the app.
15
+ final class PictureInPictureEnforcedStopAdapter {
16
+
17
+ private enum DisposableKey: String {
18
+ case stopEnforceOperation
19
+ }
20
+
21
+ private enum ApplicationState: Equatable {
22
+ case foreground
23
+ case background
24
+ case unknown
25
+ }
26
+
27
+ /// Refresh-rate-based timer interval used for enforcement attempts.
28
+ private let refreshRate: TimeInterval
29
+
30
+ /// Lifecycle subscriptions.
31
+ private var cancellables: Set<AnyCancellable> = []
32
+
33
+ /// Keyed operations that can be replaced/cancelled independently.
34
+ private var operationCancellables: [String: AnyCancellable] = [:]
35
+
36
+ /// Initializes the adapter with a Picture-in-Picture controller and
37
+ /// starts observing application state and PiP activity to enforce stop.
38
+ ///
39
+ /// - Parameter pictureInPictureController: The PiP controller to manage.
40
+ init(_ pictureInPictureController: StreamPictureInPictureControllerProtocol) {
41
+ refreshRate = Self.makeRefreshRate()
42
+
43
+ // Keep enforcement strictly state-driven: we only run the stop loop
44
+ // while the app is foregrounded *and* PiP is still active.
45
+ Publishers
46
+ .CombineLatest(
47
+ Self.makeApplicationStatePublisher(),
48
+ pictureInPictureController
49
+ .isPictureInPictureActivePublisher
50
+ .removeDuplicates()
51
+ )
52
+ .receive(on: DispatchQueue.main)
53
+ .sink { [weak self, weak pictureInPictureController] applicationState, isActive in
54
+ self?.didUpdate(
55
+ applicationState: applicationState,
56
+ isPictureInPictureActive: isActive,
57
+ pictureInPictureController: pictureInPictureController
58
+ )
59
+ }
60
+ .store(in: &cancellables)
61
+ }
62
+
63
+ deinit {
64
+ cancellables.removeAll()
65
+ removeAllOperations()
66
+ }
67
+
68
+ // MARK: - Private helpers
69
+
70
+ private func didUpdate(
71
+ applicationState: ApplicationState,
72
+ isPictureInPictureActive: Bool,
73
+ pictureInPictureController: StreamPictureInPictureControllerProtocol?
74
+ ) {
75
+ switch (applicationState, isPictureInPictureActive) {
76
+ case (.foreground, true):
77
+ // Foreground + active PiP is the only state where we enforce stop.
78
+ startStopEnforcement(for: pictureInPictureController)
79
+ default:
80
+ // Any other state (background/inactive PiP) should tear down the loop.
81
+ removeOperation(for: DisposableKey.stopEnforceOperation.rawValue)
82
+ }
83
+ }
84
+
85
+ private func startStopEnforcement(
86
+ for pictureInPictureController: StreamPictureInPictureControllerProtocol?
87
+ ) {
88
+ guard let pictureInPictureController else {
89
+ removeOperation(for: DisposableKey.stopEnforceOperation.rawValue)
90
+ return
91
+ }
92
+
93
+ let operation = Timer
94
+ .publish(every: refreshRate, on: .main, in: .common)
95
+ .autoconnect()
96
+ .filter { _ in
97
+ UIApplication.shared.applicationState == .active
98
+ }
99
+ .sink { [weak pictureInPictureController] _ in
100
+ // Calling stop repeatedly at display cadence covers cases where
101
+ // AVKit does not settle PiP shutdown on the first attempt.
102
+ pictureInPictureController?.stopPictureInPicture()
103
+ }
104
+
105
+ store(operation, key: DisposableKey.stopEnforceOperation.rawValue)
106
+ }
107
+
108
+ private func store(_ operation: AnyCancellable, key: String) {
109
+ // Keyed replacement ensures exactly one enforcement loop is active.
110
+ removeOperation(for: key)
111
+ operationCancellables[key] = operation
112
+ }
113
+
114
+ private func removeOperation(for key: String) {
115
+ operationCancellables[key]?.cancel()
116
+ operationCancellables[key] = nil
117
+ }
118
+
119
+ private func removeAllOperations() {
120
+ operationCancellables.values.forEach { $0.cancel() }
121
+ operationCancellables.removeAll()
122
+ }
123
+
124
+ private static func makeApplicationStatePublisher(
125
+ notificationCenter: NotificationCenter = .default
126
+ ) -> AnyPublisher<ApplicationState, Never> {
127
+ let foreground = Publishers.Merge(
128
+ notificationCenter
129
+ .publisher(for: UIApplication.willEnterForegroundNotification)
130
+ .map { _ in ApplicationState.foreground },
131
+ notificationCenter
132
+ .publisher(for: UIApplication.didBecomeActiveNotification)
133
+ .map { _ in ApplicationState.foreground }
134
+ )
135
+ let background = notificationCenter
136
+ .publisher(for: UIApplication.didEnterBackgroundNotification)
137
+ .map { _ in ApplicationState.background }
138
+
139
+ return Publishers.Merge(foreground, background)
140
+ // Emit the current app state immediately so newly created adapters
141
+ // do not wait for the next lifecycle notification.
142
+ .prepend(currentApplicationState())
143
+ .removeDuplicates()
144
+ .eraseToAnyPublisher()
145
+ }
146
+
147
+ private static func currentApplicationState() -> ApplicationState {
148
+ switch UIApplication.shared.applicationState {
149
+ case .active:
150
+ return .foreground
151
+ case .background:
152
+ return .background
153
+ case .inactive:
154
+ return .unknown
155
+ @unknown default:
156
+ return .unknown
157
+ }
158
+ }
159
+
160
+ private static func makeRefreshRate() -> TimeInterval {
161
+ // Keep cadence aligned to the device's display refresh rate while
162
+ // enforcing a practical minimum (30fps) for older/limited devices.
163
+ let maximumFramesPerSecond = max(30, UIScreen.main.maximumFramesPerSecond)
164
+ return 1.0 / Double(maximumFramesPerSecond)
165
+ }
166
+ }
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import Foundation
6
+
7
+ /// Centralized logger for Picture in Picture diagnostics.
8
+ ///
9
+ /// Logging is debug-only to avoid production noise and overhead.
10
+ enum PictureInPictureLogger {
11
+ static func log(_ message: @autoclosure () -> String) {
12
+ #if DEBUG
13
+ NSLog("PiP - %@", message())
14
+ #endif
15
+ }
16
+ }
@@ -0,0 +1,217 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import UIKit
6
+
7
+ /// A view that displays participant information overlay in Picture-in-Picture mode.
8
+ /// Shows participant name, pin indicator, sound indicator, and video paused indicator
9
+ /// at the bottom-left of the PiP window.
10
+ /// This aligns with upstream stream-video-swift ParticipantInfoView.
11
+ final class PictureInPictureParticipantOverlayView: UIView {
12
+
13
+ // MARK: - Properties
14
+
15
+ /// The participant's name to display
16
+ var participantName: String? {
17
+ didSet {
18
+ nameLabel.text = participantName
19
+ updateVisibility()
20
+ }
21
+ }
22
+
23
+ /// Whether the participant is pinned
24
+ var isPinned: Bool = false {
25
+ didSet {
26
+ pinIconView.isHidden = !isPinned
27
+ }
28
+ }
29
+
30
+ /// Whether the participant has audio enabled (not muted)
31
+ var hasAudio: Bool = true {
32
+ didSet {
33
+ updateSoundIndicator()
34
+ }
35
+ }
36
+
37
+ /// Whether the video track is paused/disabled
38
+ var isTrackPaused: Bool = false {
39
+ didSet {
40
+ updateVideoPausedIndicator()
41
+ }
42
+ }
43
+
44
+ /// Controls whether the overlay is shown
45
+ var isOverlayEnabled: Bool = true {
46
+ didSet {
47
+ updateVisibility()
48
+ }
49
+ }
50
+
51
+ // MARK: - UI Components
52
+
53
+ /// Container for the bottom info bar with gradient background
54
+ private lazy var containerView: UIView = {
55
+ let view = UIView()
56
+ view.translatesAutoresizingMaskIntoConstraints = false
57
+ return view
58
+ }()
59
+
60
+ /// Gradient layer for the bottom fade effect
61
+ private lazy var gradientLayer: CAGradientLayer = {
62
+ let layer = CAGradientLayer()
63
+ layer.colors = [
64
+ UIColor.clear.cgColor,
65
+ UIColor.black.withAlphaComponent(0.6).cgColor
66
+ ]
67
+ layer.locations = [0.0, 1.0]
68
+ return layer
69
+ }()
70
+
71
+ /// Container for the content (name + indicators)
72
+ private lazy var contentStackView: UIStackView = {
73
+ let stack = UIStackView()
74
+ stack.translatesAutoresizingMaskIntoConstraints = false
75
+ stack.axis = .horizontal
76
+ stack.spacing = 4
77
+ stack.alignment = .center
78
+ return stack
79
+ }()
80
+
81
+ /// Pin indicator icon (shown when participant is pinned)
82
+ private lazy var pinIconView: UIImageView = {
83
+ let imageView = UIImageView()
84
+ imageView.translatesAutoresizingMaskIntoConstraints = false
85
+ imageView.contentMode = .scaleAspectFit
86
+ imageView.tintColor = .white
87
+
88
+ // Use SF Symbol for pin
89
+ let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .medium)
90
+ imageView.image = UIImage(systemName: "pin.fill", withConfiguration: config)
91
+ imageView.isHidden = true // Hidden by default
92
+ imageView.setContentHuggingPriority(.required, for: .horizontal)
93
+ imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
94
+ return imageView
95
+ }()
96
+
97
+ /// Label showing participant name
98
+ private lazy var nameLabel: UILabel = {
99
+ let label = UILabel()
100
+ label.translatesAutoresizingMaskIntoConstraints = false
101
+ label.font = .systemFont(ofSize: 11, weight: .medium)
102
+ label.textColor = .white
103
+ label.lineBreakMode = .byTruncatingTail
104
+ label.setContentHuggingPriority(.defaultLow, for: .horizontal)
105
+ label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
106
+ return label
107
+ }()
108
+
109
+ /// Video paused indicator icon (wifi.slash when track is paused)
110
+ private lazy var videoPausedIconView: UIImageView = {
111
+ let imageView = UIImageView()
112
+ imageView.translatesAutoresizingMaskIntoConstraints = false
113
+ imageView.contentMode = .scaleAspectFit
114
+ imageView.tintColor = .white
115
+
116
+ // Use SF Symbol for video paused (wifi.slash as in upstream)
117
+ let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .medium)
118
+ imageView.image = UIImage(systemName: "wifi.slash", withConfiguration: config)
119
+ imageView.isHidden = true // Hidden by default
120
+ imageView.setContentHuggingPriority(.required, for: .horizontal)
121
+ imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
122
+ return imageView
123
+ }()
124
+
125
+ /// Sound indicator icon (microphone on/off)
126
+ private lazy var soundIndicatorView: UIImageView = {
127
+ let imageView = UIImageView()
128
+ imageView.translatesAutoresizingMaskIntoConstraints = false
129
+ imageView.contentMode = .scaleAspectFit
130
+ imageView.tintColor = .white
131
+ imageView.setContentHuggingPriority(.required, for: .horizontal)
132
+ imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
133
+ return imageView
134
+ }()
135
+
136
+ // MARK: - Initialization
137
+
138
+ override init(frame: CGRect) {
139
+ super.init(frame: frame)
140
+ setUp()
141
+ }
142
+
143
+ required init?(coder: NSCoder) {
144
+ fatalError("init(coder:) has not been implemented")
145
+ }
146
+
147
+ override func layoutSubviews() {
148
+ super.layoutSubviews()
149
+ // Update gradient frame when view bounds change
150
+ CATransaction.begin()
151
+ CATransaction.setDisableActions(true)
152
+ gradientLayer.frame = containerView.bounds
153
+ CATransaction.commit()
154
+ }
155
+
156
+ // MARK: - Private Methods
157
+
158
+ private func setUp() {
159
+ isUserInteractionEnabled = false
160
+ isHidden = true // Hidden by default until participant info is set
161
+
162
+ addSubview(containerView)
163
+ containerView.layer.insertSublayer(gradientLayer, at: 0)
164
+ containerView.addSubview(contentStackView)
165
+
166
+ contentStackView.addArrangedSubview(pinIconView)
167
+ contentStackView.addArrangedSubview(nameLabel)
168
+ contentStackView.addArrangedSubview(videoPausedIconView)
169
+ contentStackView.addArrangedSubview(soundIndicatorView)
170
+
171
+ NSLayoutConstraint.activate([
172
+ // Container positioned at the bottom
173
+ containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
174
+ containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
175
+ containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
176
+ containerView.heightAnchor.constraint(equalToConstant: 28),
177
+
178
+ // Content stack with padding
179
+ contentStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
180
+ contentStackView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor, constant: -8),
181
+ contentStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -6),
182
+
183
+ // Icon sizes
184
+ pinIconView.widthAnchor.constraint(equalToConstant: 12),
185
+ pinIconView.heightAnchor.constraint(equalToConstant: 12),
186
+ videoPausedIconView.widthAnchor.constraint(equalToConstant: 12),
187
+ videoPausedIconView.heightAnchor.constraint(equalToConstant: 12),
188
+ soundIndicatorView.widthAnchor.constraint(equalToConstant: 12),
189
+ soundIndicatorView.heightAnchor.constraint(equalToConstant: 12)
190
+ ])
191
+
192
+ // Initialize indicators
193
+ updateSoundIndicator()
194
+ updateVideoPausedIndicator()
195
+ }
196
+
197
+ private func updateVisibility() {
198
+ // Show overlay only if enabled and we have a participant name
199
+ let hasName = participantName != nil && !(participantName?.isEmpty ?? true)
200
+ isHidden = !isOverlayEnabled || !hasName
201
+ }
202
+
203
+ private func updateSoundIndicator() {
204
+ let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .medium)
205
+ if hasAudio {
206
+ soundIndicatorView.image = UIImage(systemName: "mic.fill", withConfiguration: config)
207
+ soundIndicatorView.tintColor = .white
208
+ } else {
209
+ soundIndicatorView.image = UIImage(systemName: "mic.slash.fill", withConfiguration: config)
210
+ soundIndicatorView.tintColor = UIColor(white: 0.7, alpha: 1.0) // Slightly dimmed when muted
211
+ }
212
+ }
213
+
214
+ private func updateVideoPausedIndicator() {
215
+ videoPausedIconView.isHidden = !isTrackPaused
216
+ }
217
+ }
@@ -0,0 +1,193 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import UIKit
6
+
7
+ /// A view that displays a reconnection indicator when the call connection is being recovered.
8
+ /// Shows three pulsing dots with a "Reconnecting" message, matching upstream CallingIndicator style.
9
+ final class PictureInPictureReconnectionView: UIView {
10
+
11
+ // MARK: - Properties
12
+
13
+ /// Whether the view should be visible (when reconnecting)
14
+ var isReconnecting: Bool = false {
15
+ didSet {
16
+ updateVisibility()
17
+ }
18
+ }
19
+
20
+ // MARK: - Private Properties
21
+
22
+ private let containerView: UIView = {
23
+ let view = UIView()
24
+ view.translatesAutoresizingMaskIntoConstraints = false
25
+ view.backgroundColor = UIColor(red: 0.12, green: 0.13, blue: 0.15, alpha: 0.85)
26
+ return view
27
+ }()
28
+
29
+ private let contentStackView: UIStackView = {
30
+ let stack = UIStackView()
31
+ stack.translatesAutoresizingMaskIntoConstraints = false
32
+ stack.axis = .vertical
33
+ stack.alignment = .center
34
+ stack.spacing = 8
35
+ return stack
36
+ }()
37
+
38
+ private let messageLabel: UILabel = {
39
+ let label = UILabel()
40
+ label.translatesAutoresizingMaskIntoConstraints = false
41
+ label.text = "Reconnecting"
42
+ label.textColor = .white
43
+ label.textAlignment = .center
44
+ label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
45
+ label.accessibilityIdentifier = "reconnectingMessage"
46
+ return label
47
+ }()
48
+
49
+ /// Three dots indicator matching upstream CallingIndicator style
50
+ private let dotsStackView: UIStackView = {
51
+ let stack = UIStackView()
52
+ stack.translatesAutoresizingMaskIntoConstraints = false
53
+ stack.axis = .horizontal
54
+ stack.alignment = .center
55
+ stack.spacing = 2 // Matches upstream
56
+ stack.accessibilityIdentifier = "callingIndicator"
57
+ return stack
58
+ }()
59
+
60
+ private let dotSize: CGFloat = 4 // Matches upstream
61
+ private var dots: [UIView] = []
62
+
63
+ // MARK: - Lifecycle
64
+
65
+ override init(frame: CGRect) {
66
+ super.init(frame: frame)
67
+ setUp()
68
+ }
69
+
70
+ required init?(coder: NSCoder) {
71
+ fatalError("init(coder:) has not been implemented")
72
+ }
73
+
74
+ deinit {
75
+ stopAnimation()
76
+ }
77
+
78
+ // MARK: - Private Helpers
79
+
80
+ private func createDot() -> UIView {
81
+ let dot = UIView()
82
+ dot.translatesAutoresizingMaskIntoConstraints = false
83
+ dot.backgroundColor = .white
84
+ dot.layer.cornerRadius = dotSize / 2
85
+ dot.alpha = 0 // Start invisible (matches upstream)
86
+ NSLayoutConstraint.activate([
87
+ dot.widthAnchor.constraint(equalToConstant: dotSize),
88
+ dot.heightAnchor.constraint(equalToConstant: dotSize)
89
+ ])
90
+ return dot
91
+ }
92
+
93
+ private func setUp() {
94
+ addSubview(containerView)
95
+ containerView.addSubview(contentStackView)
96
+
97
+ // Order matches upstream: text first, then dots indicator
98
+ contentStackView.addArrangedSubview(messageLabel)
99
+ contentStackView.addArrangedSubview(dotsStackView)
100
+
101
+ // Add three dots (matches upstream)
102
+ for _ in 0..<3 {
103
+ let dot = createDot()
104
+ dots.append(dot)
105
+ dotsStackView.addArrangedSubview(dot)
106
+ }
107
+
108
+ NSLayoutConstraint.activate([
109
+ containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
110
+ containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
111
+ containerView.topAnchor.constraint(equalTo: topAnchor),
112
+ containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
113
+
114
+ contentStackView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
115
+ contentStackView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
116
+ contentStackView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor, constant: 16),
117
+ contentStackView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor, constant: -16)
118
+ ])
119
+
120
+ // Initially hidden
121
+ updateVisibility()
122
+ }
123
+
124
+ override func didMoveToWindow() {
125
+ super.didMoveToWindow()
126
+ // Restart animation when view is added to window (animations are removed when view leaves window)
127
+ if window != nil && isReconnecting && !isHidden {
128
+ startAnimation()
129
+ }
130
+ }
131
+
132
+ private func updateVisibility() {
133
+ isHidden = !isReconnecting
134
+
135
+ if isReconnecting {
136
+ startAnimation()
137
+ } else {
138
+ stopAnimation()
139
+ }
140
+ }
141
+
142
+ // MARK: - Animation (matches upstream CallingIndicator)
143
+
144
+ /// Starts the pulsing animation matching upstream exactly:
145
+ /// - All dots animate from alpha 0 → 1
146
+ /// - Same 0.2s delay for all dots
147
+ /// - 1 second duration
148
+ /// - Different easing: easeOut, easeInOut, easeIn
149
+ /// - Repeat forever with autoreverse
150
+ private func startAnimation() {
151
+ // Only animate if we're in a window
152
+ guard window != nil else {
153
+ PictureInPictureLogger.log("ReconnectionView: startAnimation called but not in window yet")
154
+ return
155
+ }
156
+
157
+ PictureInPictureLogger.log("ReconnectionView: starting dot animation with CABasicAnimation")
158
+
159
+ // Stop any existing animations first
160
+ stopAnimation()
161
+
162
+ // Use CABasicAnimation for better compatibility with PiP
163
+ // Matches upstream: easeOut, easeInOut, easeIn timing functions
164
+ let timingFunctions: [CAMediaTimingFunction] = [
165
+ CAMediaTimingFunction(name: .easeOut),
166
+ CAMediaTimingFunction(name: .easeInEaseOut),
167
+ CAMediaTimingFunction(name: .easeIn)
168
+ ]
169
+
170
+ for (index, dot) in dots.enumerated() {
171
+ let animation = CABasicAnimation(keyPath: "opacity")
172
+ animation.fromValue = 0.0
173
+ animation.toValue = 1.0
174
+ animation.duration = 1.0
175
+ animation.beginTime = CACurrentMediaTime() + 0.2 // 0.2s delay
176
+ animation.timingFunction = timingFunctions[index]
177
+ animation.autoreverses = true
178
+ animation.repeatCount = .infinity
179
+ animation.fillMode = .forwards
180
+ animation.isRemovedOnCompletion = false
181
+
182
+ dot.layer.add(animation, forKey: "pulseAnimation")
183
+ dot.alpha = 0 // Set initial state
184
+ }
185
+ }
186
+
187
+ private func stopAnimation() {
188
+ dots.forEach { dot in
189
+ dot.layer.removeAllAnimations()
190
+ dot.alpha = 0
191
+ }
192
+ }
193
+ }