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