Skip to content

Commit 2fe1f9f

Browse files
authored
fix: add useIsInPiPMode for ios (#1947)
### 💡 Overview useIsInPiPMode only worked for Android. But with this change also works for iOS. Can be used for analytics in iOS. ### 📝 Implementation notes Added an `onPiPStateChange` closure to `StreamPictureInPictureController.swift`, which is called when PiP starts or stops, enabling notification of PiP state changes from native code
1 parent fb1f6fc commit 2fe1f9f

File tree

9 files changed

+63
-22
lines changed

9 files changed

+63
-22
lines changed

packages/react-native-sdk/ios/PictureInPicture/StreamPictureInPictureController.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ import Foundation
3232
}
3333
}
3434

35+
/// A closure called when the picture-in-picture state changes.
36+
public var onPiPStateChange: ((Bool) -> Void)?
37+
3538
/// A boolean value indicating whether the picture-in-picture session should start automatically when the app enters background.
3639
public var canStartPictureInPictureAutomaticallyFromInline: Bool
3740

@@ -102,6 +105,7 @@ import Foundation
102105
public func pictureInPictureControllerDidStartPictureInPicture(
103106
_ pictureInPictureController: AVPictureInPictureController
104107
) {
108+
onPiPStateChange?(true)
105109
}
106110

107111
public func pictureInPictureController(
@@ -119,6 +123,7 @@ import Foundation
119123
public func pictureInPictureControllerDidStopPictureInPicture(
120124
_ pictureInPictureController: AVPictureInPictureController
121125
) {
126+
onPiPStateChange?(false)
122127
}
123128

124129
// MARK: - Private helpers

packages/react-native-sdk/ios/RTCViewPip.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class RTCViewPip: UIView {
1414
private var pictureInPictureController = StreamPictureInPictureController()
1515
private var webRtcModule: WebRTCModule?
1616

17+
@objc var onPiPChange: RCTBubblingEventBlock?
18+
1719
private func setupNotificationObserver() {
1820
NotificationCenter.default.addObserver(
1921
self,
@@ -88,6 +90,10 @@ class RTCViewPip: UIView {
8890
setupNotificationObserver()
8991
DispatchQueue.main.async {
9092
self.pictureInPictureController?.sourceView = self
93+
// Set up PiP state change callback
94+
self.pictureInPictureController?.onPiPStateChange = { [weak self] isActive in
95+
self?.sendPiPChangeEvent(isActive: isActive)
96+
}
9197
if let reactTag = self.reactTag, let bridge = self.webRtcModule?.bridge {
9298
if let manager = bridge.module(for: RTCViewPipManager.self) as? RTCViewPipManager,
9399
let size = manager.getCachedSize(for: reactTag) {
@@ -98,4 +104,13 @@ class RTCViewPip: UIView {
98104
}
99105
}
100106
}
107+
108+
private func sendPiPChangeEvent(isActive: Bool) {
109+
guard let onPiPChange = onPiPChange else {
110+
return
111+
}
112+
113+
NSLog("PiP - Sending PiP state change event: \(isActive)")
114+
onPiPChange(["active": isActive])
115+
}
101116
}

packages/react-native-sdk/ios/RTCViewPipManager.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
@interface RCT_EXTERN_MODULE(RTCViewPipManager, RCTViewManager)
1212

1313
RCT_EXPORT_VIEW_PROPERTY(streamURL, NSString)
14+
RCT_EXPORT_VIEW_PROPERTY(onPiPChange, RCTBubblingEventBlock)
1415
RCT_EXTERN_METHOD(onCallClosed:(nonnull NSNumber*) reactTag)
1516
RCT_EXTERN_METHOD(setPreferredContentSize:(nonnull NSNumber *)reactTag width:(CGFloat)w height:(CGFloat)h);
1617

packages/react-native-sdk/src/components/Call/CallContent/RTCViewPipIOS.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ import {
1818
import { useDebouncedValue } from '../../../utils/hooks';
1919
import { shouldDisableIOSLocalVideoOnBackgroundRef } from '../../../utils/internal/shouldDisableIOSLocalVideoOnBackground';
2020
import { useTrackDimensions } from '../../../hooks/useTrackDimensions';
21+
import { isInPiPMode$ } from '../../../utils/internal/rxSubjects';
2122

2223
type Props = {
2324
includeLocalParticipantVideo?: boolean;
25+
/**
26+
* Callback that is called when the PiP mode state changes.
27+
* @param active - true when PiP started, false when PiP stopped
28+
*/
29+
onPiPChange?: (active: boolean) => void;
2430
};
2531

2632
export const RTCViewPipIOS = React.memo((props: Props) => {
27-
const { includeLocalParticipantVideo } = props;
33+
const { includeLocalParticipantVideo, onPiPChange } = props;
2834
const call = useCall();
2935
const { useParticipants } = useCallStateHooks();
3036
const _allParticipants = useParticipants({
@@ -112,9 +118,18 @@ export const RTCViewPipIOS = React.memo((props: Props) => {
112118
return videoStreamToRender?.toURL();
113119
}, [videoStreamToRender]);
114120

121+
const handlePiPChange = (event: { nativeEvent: { active: boolean } }) => {
122+
isInPiPMode$.next(event.nativeEvent.active);
123+
onPiPChange?.(event.nativeEvent.active);
124+
};
125+
115126
return (
116127
<>
117-
<RTCViewPipNative streamURL={streamURL} ref={nativeRef} />
128+
<RTCViewPipNative
129+
streamURL={streamURL}
130+
ref={nativeRef}
131+
onPiPChange={handlePiPChange}
132+
/>
118133
{participantInSpotlight && (
119134
<DimensionsUpdatedRenderless
120135
participant={participantInSpotlight}

packages/react-native-sdk/src/components/Call/CallContent/RTCViewPipNative.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import {
1010

1111
const COMPONENT_NAME = 'RTCViewPip';
1212

13+
export type PiPChangeEvent = {
14+
active: boolean;
15+
};
16+
1317
type RTCViewPipNativeProps = {
1418
streamURL?: string;
19+
onPiPChange?: (event: { nativeEvent: PiPChangeEvent }) => void;
1520
};
1621

1722
const NativeComponent: HostComponent<RTCViewPipNativeProps> =
@@ -48,6 +53,7 @@ export const RTCViewPipNative = React.memo(
4853
React.Ref<any>,
4954
{
5055
streamURL?: string;
56+
onPiPChange?: (event: { nativeEvent: PiPChangeEvent }) => void;
5157
}
5258
>((props, ref) => {
5359
if (Platform.OS !== 'ios') return null;
@@ -58,6 +64,8 @@ export const RTCViewPipNative = React.memo(
5864
pointerEvents={'none'}
5965
// eslint-disable-next-line react/prop-types
6066
streamURL={props.streamURL}
67+
// eslint-disable-next-line react/prop-types
68+
onPiPChange={props.onPiPChange}
6169
// @ts-expect-error - types issue
6270
ref={ref}
6371
/>

packages/react-native-sdk/src/hooks/useIsInPiPMode.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { useEffect, useState } from 'react';
22
import { RxUtils } from '@stream-io/video-client';
3-
import { isInPiPModeAndroid$ } from '../utils/internal/rxSubjects';
3+
import { isInPiPMode$ } from '../utils/internal/rxSubjects';
44

55
export function useIsInPiPMode() {
66
const [value, setValue] = useState<boolean>(() => {
7-
return RxUtils.getCurrentValue(isInPiPModeAndroid$);
7+
return RxUtils.getCurrentValue(isInPiPMode$);
88
});
99

1010
useEffect(() => {
11-
const subscription = isInPiPModeAndroid$.subscribe({
11+
const subscription = isInPiPMode$.subscribe({
1212
next: setValue,
1313
error: (err) => {
14-
console.log('An error occurred while reading isInPiPModeAndroid$', err);
14+
console.log('An error occurred while reading isInPiPMode$', err);
1515
setValue(false);
1616
},
1717
});

packages/react-native-sdk/src/providers/StreamCall/AppStateListener.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import {
77
Platform,
88
} from 'react-native';
99
import { shouldDisableIOSLocalVideoOnBackgroundRef } from '../../utils/internal/shouldDisableIOSLocalVideoOnBackground';
10-
import {
11-
disablePiPMode$,
12-
isInPiPModeAndroid$,
13-
} from '../../utils/internal/rxSubjects';
10+
import { disablePiPMode$, isInPiPMode$ } from '../../utils/internal/rxSubjects';
1411
import { getLogger, RxUtils } from '@stream-io/video-client';
1512

1613
const PIP_CHANGE_EVENT = 'StreamVideoReactNative_PIP_CHANGE_EVENT';
@@ -35,12 +32,12 @@ export const AppStateListener = () => {
3532
const logger = getLogger(['AppStateListener']);
3633
const initialPipMode =
3734
!disablePiP && AppState.currentState === 'background';
38-
isInPiPModeAndroid$.next(initialPipMode);
35+
isInPiPMode$.next(initialPipMode);
3936
logger('debug', 'Initial PiP mode on mount set to ', initialPipMode);
4037

4138
NativeModules?.StreamVideoReactNative?.isInPiPMode().then(
4239
(isInPiP: boolean | null | undefined) => {
43-
isInPiPModeAndroid$.next(!!isInPiP);
40+
isInPiPMode$.next(!!isInPiP);
4441
logger(
4542
'debug',
4643
'Initial PiP mode on mount (after asking native module) set to ',
@@ -56,7 +53,7 @@ export const AppStateListener = () => {
5653
const subscriptionPiPChange = eventEmitter.addListener(
5754
PIP_CHANGE_EVENT,
5855
(isInPiPMode: boolean) => {
59-
isInPiPModeAndroid$.next(isInPiPMode);
56+
isInPiPMode$.next(isInPiPMode);
6057
},
6158
);
6259

@@ -108,11 +105,11 @@ export const AppStateListener = () => {
108105
if (isAndroid8OrAbove) {
109106
// set with an assumption that its enabled so that UI disabling happens faster
110107
const disablePiP = RxUtils.getCurrentValue(disablePiPMode$);
111-
isInPiPModeAndroid$.next(!disablePiP);
108+
isInPiPMode$.next(!disablePiP);
112109
// if PiP was not enabled anyway, then in the next code we ll set it to false and UI wont be shown anyway
113110
NativeModules?.StreamVideoReactNative?.isInPiPMode().then(
114111
(isInPiP: boolean | null | undefined) => {
115-
isInPiPModeAndroid$.next(!!isInPiP);
112+
isInPiPMode$.next(!!isInPiP);
116113
if (!isInPiP) {
117114
if (AppState.currentState === 'active') {
118115
// this is to handle the case that the app became active as soon as it went to background
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BehaviorSubject } from 'rxjs';
22

3-
export const isInPiPModeAndroid$ = new BehaviorSubject<boolean>(false);
3+
export const isInPiPMode$ = new BehaviorSubject<boolean>(false);
44

55
export const disablePiPMode$ = new BehaviorSubject<boolean>(false);

sample-apps/react-native/dogfood/ios/Podfile.lock

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2240,7 +2240,7 @@ PODS:
22402240
- ReactCommon/turbomodule/bridging
22412241
- ReactCommon/turbomodule/core
22422242
- Yoga
2243-
- stream-io-noise-cancellation-react-native (0.2.4):
2243+
- stream-io-noise-cancellation-react-native (0.3.0):
22442244
- DoubleConversion
22452245
- glog
22462246
- hermes-engine
@@ -2266,7 +2266,7 @@ PODS:
22662266
- stream-react-native-webrtc
22672267
- StreamVideoNoiseCancellation
22682268
- Yoga
2269-
- stream-io-video-filters-react-native (0.6.3):
2269+
- stream-io-video-filters-react-native (0.7.0):
22702270
- DoubleConversion
22712271
- glog
22722272
- hermes-engine
@@ -2294,7 +2294,7 @@ PODS:
22942294
- stream-react-native-webrtc (125.4.3):
22952295
- React-Core
22962296
- StreamWebRTC (~> 125.6422.070)
2297-
- stream-video-react-native (1.20.16):
2297+
- stream-video-react-native (1.21.2):
22982298
- DoubleConversion
22992299
- glog
23002300
- hermes-engine
@@ -2720,10 +2720,10 @@ SPEC CHECKSUMS:
27202720
RNVoipPushNotification: 4998fe6724d421da616dca765da7dc421ff54c4e
27212721
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
27222722
stream-chat-react-native: 1803cedc0bf16361b6762e8345f9256e26c60e6f
2723-
stream-io-noise-cancellation-react-native: a159fcc95df6a8f981641cf906ffe19a52854daa
2724-
stream-io-video-filters-react-native: 4a42aa1c2fa9dc921016b5f047c9c42db166c97d
2723+
stream-io-noise-cancellation-react-native: 39911e925efffe7cee4462d6bbc4b1d33de5beae
2724+
stream-io-video-filters-react-native: 6894b6ac20d55f26858a6729d60adf6e73bd2398
27252725
stream-react-native-webrtc: b7076764940085a0450a6551f452e7f5a713f42f
2726-
stream-video-react-native: 67ddbfe623e2f1833b5d059d2a55167d011d239d
2726+
stream-video-react-native: 04b9aa84f92e5f0e5d54529d056a4b40a3c8d902
27272727
StreamVideoNoiseCancellation: 41f5a712aba288f9636b64b17ebfbdff52c61490
27282728
StreamWebRTC: a50ebd8beba4def8f4e378b4895824c3520f9889
27292729
VisionCamera: d19797da4d373ada2c167a6e357e520cc1d9dc56

0 commit comments

Comments
 (0)