diff --git a/packages/react-native-sdk/ios/PictureInPicture/StreamPictureInPictureController.swift b/packages/react-native-sdk/ios/PictureInPicture/StreamPictureInPictureController.swift
index 3082b0e627..2bd24f2c52 100644
--- a/packages/react-native-sdk/ios/PictureInPicture/StreamPictureInPictureController.swift
+++ b/packages/react-native-sdk/ios/PictureInPicture/StreamPictureInPictureController.swift
@@ -32,6 +32,9 @@ import Foundation
}
}
+ /// A closure called when the picture-in-picture state changes.
+ public var onPiPStateChange: ((Bool) -> Void)?
+
/// A boolean value indicating whether the picture-in-picture session should start automatically when the app enters background.
public var canStartPictureInPictureAutomaticallyFromInline: Bool
@@ -102,6 +105,7 @@ import Foundation
public func pictureInPictureControllerDidStartPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
+ onPiPStateChange?(true)
}
public func pictureInPictureController(
@@ -119,6 +123,7 @@ import Foundation
public func pictureInPictureControllerDidStopPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
+ onPiPStateChange?(false)
}
// MARK: - Private helpers
diff --git a/packages/react-native-sdk/ios/RTCViewPip.swift b/packages/react-native-sdk/ios/RTCViewPip.swift
index f14bb15e8c..d820a64827 100644
--- a/packages/react-native-sdk/ios/RTCViewPip.swift
+++ b/packages/react-native-sdk/ios/RTCViewPip.swift
@@ -14,6 +14,8 @@ class RTCViewPip: UIView {
private var pictureInPictureController = StreamPictureInPictureController()
private var webRtcModule: WebRTCModule?
+ @objc var onPiPChange: RCTBubblingEventBlock?
+
private func setupNotificationObserver() {
NotificationCenter.default.addObserver(
self,
@@ -88,6 +90,10 @@ class RTCViewPip: UIView {
setupNotificationObserver()
DispatchQueue.main.async {
self.pictureInPictureController?.sourceView = self
+ // Set up PiP state change callback
+ self.pictureInPictureController?.onPiPStateChange = { [weak self] isActive in
+ self?.sendPiPChangeEvent(isActive: isActive)
+ }
if let reactTag = self.reactTag, let bridge = self.webRtcModule?.bridge {
if let manager = bridge.module(for: RTCViewPipManager.self) as? RTCViewPipManager,
let size = manager.getCachedSize(for: reactTag) {
@@ -98,4 +104,13 @@ class RTCViewPip: UIView {
}
}
}
+
+ private func sendPiPChangeEvent(isActive: Bool) {
+ guard let onPiPChange = onPiPChange else {
+ return
+ }
+
+ NSLog("PiP - Sending PiP state change event: \(isActive)")
+ onPiPChange(["active": isActive])
+ }
}
diff --git a/packages/react-native-sdk/ios/RTCViewPipManager.mm b/packages/react-native-sdk/ios/RTCViewPipManager.mm
index 6beabc9c74..c80c3bdd47 100644
--- a/packages/react-native-sdk/ios/RTCViewPipManager.mm
+++ b/packages/react-native-sdk/ios/RTCViewPipManager.mm
@@ -11,6 +11,7 @@
@interface RCT_EXTERN_MODULE(RTCViewPipManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(streamURL, NSString)
+RCT_EXPORT_VIEW_PROPERTY(onPiPChange, RCTBubblingEventBlock)
RCT_EXTERN_METHOD(onCallClosed:(nonnull NSNumber*) reactTag)
RCT_EXTERN_METHOD(setPreferredContentSize:(nonnull NSNumber *)reactTag width:(CGFloat)w height:(CGFloat)h);
diff --git a/packages/react-native-sdk/src/components/Call/CallContent/RTCViewPipIOS.tsx b/packages/react-native-sdk/src/components/Call/CallContent/RTCViewPipIOS.tsx
index f564e80b43..5854df2bba 100644
--- a/packages/react-native-sdk/src/components/Call/CallContent/RTCViewPipIOS.tsx
+++ b/packages/react-native-sdk/src/components/Call/CallContent/RTCViewPipIOS.tsx
@@ -18,13 +18,19 @@ import {
import { useDebouncedValue } from '../../../utils/hooks';
import { shouldDisableIOSLocalVideoOnBackgroundRef } from '../../../utils/internal/shouldDisableIOSLocalVideoOnBackground';
import { useTrackDimensions } from '../../../hooks/useTrackDimensions';
+import { isInPiPMode$ } from '../../../utils/internal/rxSubjects';
type Props = {
includeLocalParticipantVideo?: boolean;
+ /**
+ * Callback that is called when the PiP mode state changes.
+ * @param active - true when PiP started, false when PiP stopped
+ */
+ onPiPChange?: (active: boolean) => void;
};
export const RTCViewPipIOS = React.memo((props: Props) => {
- const { includeLocalParticipantVideo } = props;
+ const { includeLocalParticipantVideo, onPiPChange } = props;
const call = useCall();
const { useParticipants } = useCallStateHooks();
const _allParticipants = useParticipants({
@@ -112,9 +118,18 @@ export const RTCViewPipIOS = React.memo((props: Props) => {
return videoStreamToRender?.toURL();
}, [videoStreamToRender]);
+ const handlePiPChange = (event: { nativeEvent: { active: boolean } }) => {
+ isInPiPMode$.next(event.nativeEvent.active);
+ onPiPChange?.(event.nativeEvent.active);
+ };
+
return (
<>
-
+
{participantInSpotlight && (
void;
};
const NativeComponent: HostComponent =
@@ -48,6 +53,7 @@ export const RTCViewPipNative = React.memo(
React.Ref,
{
streamURL?: string;
+ onPiPChange?: (event: { nativeEvent: PiPChangeEvent }) => void;
}
>((props, ref) => {
if (Platform.OS !== 'ios') return null;
@@ -58,6 +64,8 @@ export const RTCViewPipNative = React.memo(
pointerEvents={'none'}
// eslint-disable-next-line react/prop-types
streamURL={props.streamURL}
+ // eslint-disable-next-line react/prop-types
+ onPiPChange={props.onPiPChange}
// @ts-expect-error - types issue
ref={ref}
/>
diff --git a/packages/react-native-sdk/src/hooks/useIsInPiPMode.tsx b/packages/react-native-sdk/src/hooks/useIsInPiPMode.tsx
index 3dc3c7b06c..e0774eae9b 100644
--- a/packages/react-native-sdk/src/hooks/useIsInPiPMode.tsx
+++ b/packages/react-native-sdk/src/hooks/useIsInPiPMode.tsx
@@ -1,17 +1,17 @@
import { useEffect, useState } from 'react';
import { RxUtils } from '@stream-io/video-client';
-import { isInPiPModeAndroid$ } from '../utils/internal/rxSubjects';
+import { isInPiPMode$ } from '../utils/internal/rxSubjects';
export function useIsInPiPMode() {
const [value, setValue] = useState(() => {
- return RxUtils.getCurrentValue(isInPiPModeAndroid$);
+ return RxUtils.getCurrentValue(isInPiPMode$);
});
useEffect(() => {
- const subscription = isInPiPModeAndroid$.subscribe({
+ const subscription = isInPiPMode$.subscribe({
next: setValue,
error: (err) => {
- console.log('An error occurred while reading isInPiPModeAndroid$', err);
+ console.log('An error occurred while reading isInPiPMode$', err);
setValue(false);
},
});
diff --git a/packages/react-native-sdk/src/providers/StreamCall/AppStateListener.tsx b/packages/react-native-sdk/src/providers/StreamCall/AppStateListener.tsx
index b31e4adc68..e6e7cd8ced 100644
--- a/packages/react-native-sdk/src/providers/StreamCall/AppStateListener.tsx
+++ b/packages/react-native-sdk/src/providers/StreamCall/AppStateListener.tsx
@@ -7,10 +7,7 @@ import {
Platform,
} from 'react-native';
import { shouldDisableIOSLocalVideoOnBackgroundRef } from '../../utils/internal/shouldDisableIOSLocalVideoOnBackground';
-import {
- disablePiPMode$,
- isInPiPModeAndroid$,
-} from '../../utils/internal/rxSubjects';
+import { disablePiPMode$, isInPiPMode$ } from '../../utils/internal/rxSubjects';
import { getLogger, RxUtils } from '@stream-io/video-client';
const PIP_CHANGE_EVENT = 'StreamVideoReactNative_PIP_CHANGE_EVENT';
@@ -35,12 +32,12 @@ export const AppStateListener = () => {
const logger = getLogger(['AppStateListener']);
const initialPipMode =
!disablePiP && AppState.currentState === 'background';
- isInPiPModeAndroid$.next(initialPipMode);
+ isInPiPMode$.next(initialPipMode);
logger('debug', 'Initial PiP mode on mount set to ', initialPipMode);
NativeModules?.StreamVideoReactNative?.isInPiPMode().then(
(isInPiP: boolean | null | undefined) => {
- isInPiPModeAndroid$.next(!!isInPiP);
+ isInPiPMode$.next(!!isInPiP);
logger(
'debug',
'Initial PiP mode on mount (after asking native module) set to ',
@@ -56,7 +53,7 @@ export const AppStateListener = () => {
const subscriptionPiPChange = eventEmitter.addListener(
PIP_CHANGE_EVENT,
(isInPiPMode: boolean) => {
- isInPiPModeAndroid$.next(isInPiPMode);
+ isInPiPMode$.next(isInPiPMode);
},
);
@@ -108,11 +105,11 @@ export const AppStateListener = () => {
if (isAndroid8OrAbove) {
// set with an assumption that its enabled so that UI disabling happens faster
const disablePiP = RxUtils.getCurrentValue(disablePiPMode$);
- isInPiPModeAndroid$.next(!disablePiP);
+ isInPiPMode$.next(!disablePiP);
// if PiP was not enabled anyway, then in the next code we ll set it to false and UI wont be shown anyway
NativeModules?.StreamVideoReactNative?.isInPiPMode().then(
(isInPiP: boolean | null | undefined) => {
- isInPiPModeAndroid$.next(!!isInPiP);
+ isInPiPMode$.next(!!isInPiP);
if (!isInPiP) {
if (AppState.currentState === 'active') {
// this is to handle the case that the app became active as soon as it went to background
diff --git a/packages/react-native-sdk/src/utils/internal/rxSubjects.ts b/packages/react-native-sdk/src/utils/internal/rxSubjects.ts
index 101266fae6..1337118898 100644
--- a/packages/react-native-sdk/src/utils/internal/rxSubjects.ts
+++ b/packages/react-native-sdk/src/utils/internal/rxSubjects.ts
@@ -1,5 +1,5 @@
import { BehaviorSubject } from 'rxjs';
-export const isInPiPModeAndroid$ = new BehaviorSubject(false);
+export const isInPiPMode$ = new BehaviorSubject(false);
export const disablePiPMode$ = new BehaviorSubject(false);
diff --git a/sample-apps/react-native/dogfood/ios/Podfile.lock b/sample-apps/react-native/dogfood/ios/Podfile.lock
index 0d364bdd7a..be63a13018 100644
--- a/sample-apps/react-native/dogfood/ios/Podfile.lock
+++ b/sample-apps/react-native/dogfood/ios/Podfile.lock
@@ -2240,7 +2240,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - stream-io-noise-cancellation-react-native (0.2.4):
+ - stream-io-noise-cancellation-react-native (0.3.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2266,7 +2266,7 @@ PODS:
- stream-react-native-webrtc
- StreamVideoNoiseCancellation
- Yoga
- - stream-io-video-filters-react-native (0.6.3):
+ - stream-io-video-filters-react-native (0.7.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2294,7 +2294,7 @@ PODS:
- stream-react-native-webrtc (125.4.3):
- React-Core
- StreamWebRTC (~> 125.6422.070)
- - stream-video-react-native (1.20.16):
+ - stream-video-react-native (1.21.2):
- DoubleConversion
- glog
- hermes-engine
@@ -2720,10 +2720,10 @@ SPEC CHECKSUMS:
RNVoipPushNotification: 4998fe6724d421da616dca765da7dc421ff54c4e
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
stream-chat-react-native: 1803cedc0bf16361b6762e8345f9256e26c60e6f
- stream-io-noise-cancellation-react-native: a159fcc95df6a8f981641cf906ffe19a52854daa
- stream-io-video-filters-react-native: 4a42aa1c2fa9dc921016b5f047c9c42db166c97d
+ stream-io-noise-cancellation-react-native: 39911e925efffe7cee4462d6bbc4b1d33de5beae
+ stream-io-video-filters-react-native: 6894b6ac20d55f26858a6729d60adf6e73bd2398
stream-react-native-webrtc: b7076764940085a0450a6551f452e7f5a713f42f
- stream-video-react-native: 67ddbfe623e2f1833b5d059d2a55167d011d239d
+ stream-video-react-native: 04b9aa84f92e5f0e5d54529d056a4b40a3c8d902
StreamVideoNoiseCancellation: 41f5a712aba288f9636b64b17ebfbdff52c61490
StreamWebRTC: a50ebd8beba4def8f4e378b4895824c3520f9889
VisionCamera: d19797da4d373ada2c167a6e357e520cc1d9dc56