Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -102,6 +105,7 @@ import Foundation
public func pictureInPictureControllerDidStartPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
onPiPStateChange?(true)
}

public func pictureInPictureController(
Expand All @@ -119,6 +123,7 @@ import Foundation
public func pictureInPictureControllerDidStopPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
onPiPStateChange?(false)
}

// MARK: - Private helpers
Expand Down
15 changes: 15 additions & 0 deletions packages/react-native-sdk/ios/RTCViewPip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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])
}
}
1 change: 1 addition & 0 deletions packages/react-native-sdk/ios/RTCViewPipManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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 (
<>
<RTCViewPipNative streamURL={streamURL} ref={nativeRef} />
<RTCViewPipNative
streamURL={streamURL}
ref={nativeRef}
onPiPChange={handlePiPChange}
/>
{participantInSpotlight && (
<DimensionsUpdatedRenderless
participant={participantInSpotlight}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import {

const COMPONENT_NAME = 'RTCViewPip';

export type PiPChangeEvent = {
active: boolean;
};

type RTCViewPipNativeProps = {
streamURL?: string;
onPiPChange?: (event: { nativeEvent: PiPChangeEvent }) => void;
};

const NativeComponent: HostComponent<RTCViewPipNativeProps> =
Expand Down Expand Up @@ -48,6 +53,7 @@ export const RTCViewPipNative = React.memo(
React.Ref<any>,
{
streamURL?: string;
onPiPChange?: (event: { nativeEvent: PiPChangeEvent }) => void;
}
>((props, ref) => {
if (Platform.OS !== 'ios') return null;
Expand All @@ -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}
/>
Expand Down
8 changes: 4 additions & 4 deletions packages/react-native-sdk/src/hooks/useIsInPiPMode.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(() => {
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);
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 ',
Expand All @@ -56,7 +53,7 @@ export const AppStateListener = () => {
const subscriptionPiPChange = eventEmitter.addListener(
PIP_CHANGE_EVENT,
(isInPiPMode: boolean) => {
isInPiPModeAndroid$.next(isInPiPMode);
isInPiPMode$.next(isInPiPMode);
},
);

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-sdk/src/utils/internal/rxSubjects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BehaviorSubject } from 'rxjs';

export const isInPiPModeAndroid$ = new BehaviorSubject<boolean>(false);
export const isInPiPMode$ = new BehaviorSubject<boolean>(false);

export const disablePiPMode$ = new BehaviorSubject<boolean>(false);
12 changes: 6 additions & 6 deletions sample-apps/react-native/dogfood/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down