@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.
- package/CHANGELOG.md +3162 -0
- package/android/src/main/AndroidManifest.xml +1 -8
- package/android/src/main/AndroidManifestNew.xml +0 -11
- package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +5 -42
- package/android/src/main/java/com/streamvideo/reactnative/audio/utils/WebRtcAudioUtils.kt +6 -70
- package/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt +4 -6
- package/android/src/main/java/com/streamvideo/reactnative/util/CallAliveServiceChecker.kt +95 -0
- package/dist/commonjs/components/Call/CallContent/CallContent.js +13 -7
- package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
- package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +50 -14
- package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
- package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js +27 -0
- package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js +12 -9
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
- package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js +19 -4
- package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
- package/dist/commonjs/hooks/push/index.js +2 -0
- package/dist/commonjs/hooks/push/index.js.map +1 -1
- package/dist/commonjs/hooks/push/useIosCallkeepWithCallingStateEffect.js +160 -0
- package/dist/commonjs/hooks/push/useIosCallkeepWithCallingStateEffect.js.map +1 -0
- package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js +31 -18
- package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
- package/dist/commonjs/hooks/push/useProcessPushCallEffect.js +67 -0
- package/dist/commonjs/hooks/push/useProcessPushCallEffect.js.map +1 -0
- package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js +97 -64
- package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
- package/dist/commonjs/index.js +0 -1
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/modules/call-manager/CallManager.js +0 -26
- package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
- package/dist/commonjs/providers/StreamCall/index.js +6 -6
- package/dist/commonjs/providers/StreamCall/index.js.map +1 -1
- package/dist/commonjs/utils/StreamVideoRN/index.js +21 -33
- package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
- package/dist/commonjs/utils/hooks/index.js +0 -11
- package/dist/commonjs/utils/hooks/index.js.map +1 -1
- package/dist/commonjs/utils/internal/registerSDKGlobals.js +3 -52
- package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
- package/dist/commonjs/utils/push/android.js +202 -151
- package/dist/commonjs/utils/push/android.js.map +1 -1
- package/dist/commonjs/utils/push/internal/ios.js +34 -17
- package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
- package/dist/commonjs/utils/push/internal/rxSubjects.js +45 -1
- package/dist/commonjs/utils/push/internal/rxSubjects.js.map +1 -1
- package/dist/commonjs/utils/push/internal/utils.js +20 -32
- package/dist/commonjs/utils/push/internal/utils.js.map +1 -1
- package/dist/commonjs/utils/push/ios.js.map +1 -1
- package/dist/commonjs/utils/push/libs/callkeep.js +17 -0
- package/dist/commonjs/utils/push/libs/callkeep.js.map +1 -0
- package/dist/commonjs/utils/push/libs/index.js +19 -8
- package/dist/commonjs/utils/push/libs/index.js.map +1 -1
- package/dist/commonjs/utils/push/libs/notifee/index.js +19 -0
- package/dist/commonjs/utils/push/libs/notifee/index.js.map +1 -1
- package/dist/commonjs/utils/push/libs/voipPushNotification.js +17 -0
- package/dist/commonjs/utils/push/libs/voipPushNotification.js.map +1 -0
- package/dist/commonjs/utils/push/setupIosCallKeepEvents.js +205 -0
- package/dist/commonjs/utils/push/setupIosCallKeepEvents.js.map +1 -0
- package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +6 -7
- package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/commonjs/version.js.map +1 -1
- package/dist/module/components/Call/CallContent/CallContent.js +10 -4
- package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
- package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +52 -16
- package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
- package/dist/module/components/Call/CallContent/RTCViewPipNative.js +27 -0
- package/dist/module/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
- package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
- package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
- package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js +15 -12
- package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
- package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js +20 -5
- package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
- package/dist/module/hooks/push/index.js +2 -0
- package/dist/module/hooks/push/index.js.map +1 -1
- package/dist/module/hooks/push/useIosCallkeepWithCallingStateEffect.js +153 -0
- package/dist/module/hooks/push/useIosCallkeepWithCallingStateEffect.js.map +1 -0
- package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js +31 -18
- package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
- package/dist/module/hooks/push/useProcessPushCallEffect.js +60 -0
- package/dist/module/hooks/push/useProcessPushCallEffect.js.map +1 -0
- package/dist/module/hooks/useAndroidKeepCallAliveEffect.js +99 -66
- package/dist/module/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
- package/dist/module/index.js +0 -1
- package/dist/module/index.js.map +1 -1
- package/dist/module/modules/call-manager/CallManager.js +0 -26
- package/dist/module/modules/call-manager/CallManager.js.map +1 -1
- package/dist/module/providers/StreamCall/index.js +6 -6
- package/dist/module/providers/StreamCall/index.js.map +1 -1
- package/dist/module/utils/StreamVideoRN/index.js +21 -33
- package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
- package/dist/module/utils/hooks/index.js +0 -1
- package/dist/module/utils/hooks/index.js.map +1 -1
- package/dist/module/utils/internal/registerSDKGlobals.js +3 -52
- package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
- package/dist/module/utils/push/android.js +204 -153
- package/dist/module/utils/push/android.js.map +1 -1
- package/dist/module/utils/push/internal/ios.js +34 -17
- package/dist/module/utils/push/internal/ios.js.map +1 -1
- package/dist/module/utils/push/internal/rxSubjects.js +44 -0
- package/dist/module/utils/push/internal/rxSubjects.js.map +1 -1
- package/dist/module/utils/push/internal/utils.js +19 -29
- package/dist/module/utils/push/internal/utils.js.map +1 -1
- package/dist/module/utils/push/ios.js.map +1 -1
- package/dist/module/utils/push/libs/callkeep.js +11 -0
- package/dist/module/utils/push/libs/callkeep.js.map +1 -0
- package/dist/module/utils/push/libs/index.js +2 -1
- package/dist/module/utils/push/libs/index.js.map +1 -1
- package/dist/module/utils/push/libs/notifee/index.js +18 -0
- package/dist/module/utils/push/libs/notifee/index.js.map +1 -1
- package/dist/module/utils/push/libs/voipPushNotification.js +11 -0
- package/dist/module/utils/push/libs/voipPushNotification.js.map +1 -0
- package/dist/module/utils/push/setupIosCallKeepEvents.js +199 -0
- package/dist/module/utils/push/setupIosCallKeepEvents.js.map +1 -0
- package/dist/module/utils/push/setupIosVoipPushEvents.js +6 -7
- package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/module/version.js.map +1 -1
- package/dist/typescript/components/Call/CallContent/CallContent.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts +18 -0
- package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallParticipantsList/CallParticipantsList.d.ts.map +1 -1
- package/dist/typescript/hooks/push/index.d.ts.map +1 -1
- package/dist/typescript/hooks/push/useIosCallkeepWithCallingStateEffect.d.ts +5 -0
- package/dist/typescript/hooks/push/useIosCallkeepWithCallingStateEffect.d.ts.map +1 -0
- package/dist/typescript/hooks/push/useIosVoipPushEventsSetupEffect.d.ts.map +1 -1
- package/dist/typescript/hooks/push/useProcessPushCallEffect.d.ts +8 -0
- package/dist/typescript/hooks/push/useProcessPushCallEffect.d.ts.map +1 -0
- package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts.map +1 -1
- package/dist/typescript/index.d.ts +0 -1
- package/dist/typescript/index.d.ts.map +1 -1
- package/dist/typescript/modules/call-manager/CallManager.d.ts +0 -5
- package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
- package/dist/typescript/utils/StreamVideoRN/index.d.ts +2 -20
- package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
- package/dist/typescript/utils/StreamVideoRN/types.d.ts +29 -54
- package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
- package/dist/typescript/utils/hooks/index.d.ts +0 -1
- package/dist/typescript/utils/hooks/index.d.ts.map +1 -1
- package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
- package/dist/typescript/utils/push/android.d.ts +2 -1
- package/dist/typescript/utils/push/android.d.ts.map +1 -1
- package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
- package/dist/typescript/utils/push/internal/rxSubjects.d.ts +33 -0
- package/dist/typescript/utils/push/internal/rxSubjects.d.ts.map +1 -1
- package/dist/typescript/utils/push/internal/utils.d.ts +1 -8
- package/dist/typescript/utils/push/internal/utils.d.ts.map +1 -1
- package/dist/typescript/utils/push/ios.d.ts +2 -1
- package/dist/typescript/utils/push/ios.d.ts.map +1 -1
- package/dist/typescript/utils/push/libs/callkeep.d.ts +3 -0
- package/dist/typescript/utils/push/libs/callkeep.d.ts.map +1 -0
- package/dist/typescript/utils/push/libs/index.d.ts +2 -1
- package/dist/typescript/utils/push/libs/index.d.ts.map +1 -1
- package/dist/typescript/utils/push/libs/notifee/index.d.ts +1 -0
- package/dist/typescript/utils/push/libs/notifee/index.d.ts.map +1 -1
- package/dist/typescript/utils/push/libs/voipPushNotification.d.ts +3 -0
- package/dist/typescript/utils/push/libs/voipPushNotification.d.ts.map +1 -0
- package/dist/typescript/utils/push/setupIosCallKeepEvents.d.ts +6 -0
- package/dist/typescript/utils/push/setupIosCallKeepEvents.d.ts.map +1 -0
- package/dist/typescript/utils/push/setupIosVoipPushEvents.d.ts.map +1 -1
- package/dist/typescript/version.d.ts +1 -1
- package/dist/typescript/version.d.ts.map +1 -1
- package/expo-config-plugin/dist/withAndroidManifest.js +33 -1
- package/expo-config-plugin/dist/withAndroidPermissions.js +7 -2
- package/expo-config-plugin/dist/withAppDelegate.js +197 -19
- package/expo-config-plugin/dist/withMainActivity.js +1 -1
- package/expo-config-plugin/dist/withiOSInfoPlist.js +3 -2
- package/ios/PictureInPicture/PictureInPictureAvatarView.swift +273 -0
- package/ios/PictureInPicture/PictureInPictureConnectionQualityIndicator.swift +162 -0
- package/ios/PictureInPicture/PictureInPictureContent.swift +173 -0
- package/ios/PictureInPicture/PictureInPictureContentState.swift +123 -0
- package/ios/PictureInPicture/PictureInPictureDelegateProxy.swift +89 -0
- package/ios/PictureInPicture/PictureInPictureEnforcedStopAdapter.swift +166 -0
- package/ios/PictureInPicture/PictureInPictureLogger.swift +16 -0
- package/ios/PictureInPicture/PictureInPictureParticipantOverlayView.swift +217 -0
- package/ios/PictureInPicture/PictureInPictureReconnectionView.swift +193 -0
- package/ios/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +125 -7
- package/ios/PictureInPicture/StreamPictureInPictureController.swift +237 -63
- package/ios/PictureInPicture/StreamPictureInPictureControllerProtocol.swift +30 -0
- package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +384 -12
- package/ios/RTCViewPip.swift +187 -21
- package/ios/RTCViewPipManager.mm +9 -0
- package/ios/RTCViewPipManager.swift +3 -3
- package/ios/StreamInCallManager.m +0 -2
- package/ios/StreamInCallManager.swift +7 -19
- package/ios/StreamVideoReactNative.h +4 -7
- package/ios/StreamVideoReactNative.m +82 -189
- package/package.json +19 -14
- package/src/components/Call/CallContent/CallContent.tsx +16 -8
- package/src/components/Call/CallContent/RTCViewPipIOS.tsx +81 -15
- package/src/components/Call/CallContent/RTCViewPipNative.tsx +36 -0
- package/src/components/Call/CallLayout/CallParticipantsGrid.tsx +28 -14
- package/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +19 -10
- package/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +20 -5
- package/src/hooks/push/index.ts +2 -0
- package/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts +235 -0
- package/src/hooks/push/useIosVoipPushEventsSetupEffect.ts +34 -21
- package/src/hooks/push/useProcessPushCallEffect.ts +108 -0
- package/src/hooks/useAndroidKeepCallAliveEffect.ts +120 -94
- package/src/index.ts +0 -1
- package/src/modules/call-manager/CallManager.ts +0 -36
- package/src/modules/call-manager/native-module.d.ts +0 -7
- package/src/providers/StreamCall/index.tsx +6 -6
- package/src/utils/StreamVideoRN/index.ts +30 -40
- package/src/utils/StreamVideoRN/types.ts +29 -56
- package/src/utils/hooks/index.ts +0 -1
- package/src/utils/internal/registerSDKGlobals.ts +4 -47
- package/src/utils/push/android.ts +309 -227
- package/src/utils/push/internal/ios.ts +44 -28
- package/src/utils/push/internal/rxSubjects.ts +61 -0
- package/src/utils/push/internal/utils.ts +26 -45
- package/src/utils/push/ios.ts +6 -1
- package/src/utils/push/libs/callkeep.ts +16 -0
- package/src/utils/push/libs/index.ts +2 -1
- package/src/utils/push/libs/notifee/index.ts +27 -0
- package/src/utils/push/libs/voipPushNotification.ts +17 -0
- package/src/utils/push/setupIosCallKeepEvents.ts +252 -0
- package/src/utils/push/setupIosVoipPushEvents.ts +7 -11
- package/src/version.ts +1 -1
- package/android/src/main/java/com/streamvideo/reactnative/keepalive/KeepAliveNotification.kt +0 -83
- package/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt +0 -149
- package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +0 -121
- package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +0 -1
- package/dist/commonjs/utils/hooks/useDebouncedValue.js +0 -24
- package/dist/commonjs/utils/hooks/useDebouncedValue.js.map +0 -1
- package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js +0 -58
- package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js.map +0 -1
- package/dist/commonjs/utils/internal/callingx/callingx.js +0 -109
- package/dist/commonjs/utils/internal/callingx/callingx.js.map +0 -1
- package/dist/commonjs/utils/keepCallAliveHeadlessTask.js +0 -48
- package/dist/commonjs/utils/keepCallAliveHeadlessTask.js.map +0 -1
- package/dist/commonjs/utils/push/libs/callingx.js +0 -75
- package/dist/commonjs/utils/push/libs/callingx.js.map +0 -1
- package/dist/commonjs/utils/push/setupCallingExpEvents.js +0 -108
- package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +0 -1
- package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +0 -114
- package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +0 -1
- package/dist/module/utils/hooks/useDebouncedValue.js +0 -19
- package/dist/module/utils/hooks/useDebouncedValue.js.map +0 -1
- package/dist/module/utils/internal/callingx/audioSessionPromise.js +0 -51
- package/dist/module/utils/internal/callingx/audioSessionPromise.js.map +0 -1
- package/dist/module/utils/internal/callingx/callingx.js +0 -100
- package/dist/module/utils/internal/callingx/callingx.js.map +0 -1
- package/dist/module/utils/keepCallAliveHeadlessTask.js +0 -42
- package/dist/module/utils/keepCallAliveHeadlessTask.js.map +0 -1
- package/dist/module/utils/push/libs/callingx.js +0 -67
- package/dist/module/utils/push/libs/callingx.js.map +0 -1
- package/dist/module/utils/push/setupCallingExpEvents.js +0 -102
- package/dist/module/utils/push/setupCallingExpEvents.js.map +0 -1
- package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts +0 -5
- package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +0 -1
- package/dist/typescript/utils/hooks/useDebouncedValue.d.ts +0 -8
- package/dist/typescript/utils/hooks/useDebouncedValue.d.ts.map +0 -1
- package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts +0 -16
- package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts.map +0 -1
- package/dist/typescript/utils/internal/callingx/callingx.d.ts +0 -14
- package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +0 -1
- package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts +0 -10
- package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts.map +0 -1
- package/dist/typescript/utils/push/libs/callingx.d.ts +0 -9
- package/dist/typescript/utils/push/libs/callingx.d.ts.map +0 -1
- package/dist/typescript/utils/push/setupCallingExpEvents.d.ts +0 -8
- package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +0 -1
- package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +0 -147
- package/src/utils/hooks/useDebouncedValue.ts +0 -21
- package/src/utils/internal/callingx/audioSessionPromise.ts +0 -53
- package/src/utils/internal/callingx/callingx.ts +0 -146
- package/src/utils/keepCallAliveHeadlessTask.ts +0 -54
- package/src/utils/push/libs/callingx.ts +0 -90
- 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
|
+
}
|