Skip to content

fix: support setting iOS pip window sizes #1876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 4, 2025
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
2 changes: 1 addition & 1 deletion packages/noise-cancellation-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
},
"homepage": "https://github.com/GetStream/stream-video-js#readme",
"devDependencies": {
"@stream-io/react-native-webrtc": "^125.4.0",
"@stream-io/react-native-webrtc": "125.4.1",
"react-native": "^0.79.2",
"react-native-builder-bob": "^0.37.0",
"rimraf": "^6.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ import Foundation
super.init()
}

func setPreferredContentSize(_ size: CGSize) {
contentViewController?.preferredContentSize = size
}

// MARK: - AVPictureInPictureControllerDelegate

func pictureInPictureController(
Expand Down
20 changes: 17 additions & 3 deletions packages/react-native-sdk/ios/RTCViewPip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import React

@objc(RTCViewPip)
class RTCViewPip: UIView {
Expand Down Expand Up @@ -49,7 +50,7 @@ class RTCViewPip: UIView {
}

DispatchQueue.main.async {
NSLog("PiP - Setting video track for streamURL: -\(streamURLString)")
NSLog("PiP - Setting video track for streamURL: -\(streamURLString) trackId: \(videoTrack.trackId)")
self.pictureInPictureController?.track = videoTrack
}
}
Expand All @@ -67,20 +68,33 @@ class RTCViewPip: UIView {
self.pictureInPictureController = nil
}

@objc
func setPreferredContentSize(_ size: CGSize) {
NSLog("PiP - RTCViewPip setPreferredContentSize \(size)")
self.pictureInPictureController?.setPreferredContentSize(size)
}

override func didMoveToSuperview() {
super.didMoveToSuperview()
if self.superview == nil {
print("PiP - RTCViewPip has been removed from its superview.")
NSLog("PiP - RTCViewPip has been removed from its superview.")
NotificationCenter.default.removeObserver(self)
DispatchQueue.main.async {
NSLog("PiP - onCallClosed called due to view detaching")
self.onCallClosed()
}
} else {
print("PiP - RTCViewPip has been added to a superview.")
NSLog("PiP - RTCViewPip has been added to a superview.")
setupNotificationObserver()
DispatchQueue.main.async {
self.pictureInPictureController?.sourceView = self
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) {
NSLog("PiP - Applying cached size \(size) for reactTag \(reactTag)")
self.setPreferredContentSize(size)
}
}
}
}
}
Expand Down
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 @@ -12,5 +12,6 @@ @interface RCT_EXTERN_MODULE(RTCViewPipManager, RCTViewManager)

RCT_EXPORT_VIEW_PROPERTY(streamURL, NSString)
RCT_EXTERN_METHOD(onCallClosed:(nonnull NSNumber*) reactTag)
RCT_EXTERN_METHOD(setPreferredContentSize:(nonnull NSNumber *)reactTag width:(CGFloat)w height:(CGFloat)h);

@end
45 changes: 39 additions & 6 deletions packages/react-native-sdk/ios/RTCViewPipManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import Foundation
@objc(RTCViewPipManager)
class RTCViewPipManager: RCTViewManager {

private var cachedSizes: [NSNumber: CGSize] = [:]

override func view() -> UIView! {
let view = RTCViewPip()
view.setWebRtcModule(self.bridge.module(forName: "WebRTCModule") as! WebRTCModule)
Expand All @@ -20,15 +22,46 @@ class RTCViewPipManager: RCTViewManager {
return true
}

@objc func onCallClosed(_ reactTag: NSNumber) {
self.bridge!.uiManager.addUIBlock { (_: RCTUIManager?, viewRegistry: [NSNumber: UIView]?) in
guard let pipView = viewRegistry?[reactTag] as? RTCViewPip else {
@objc(onCallClosed:)
func onCallClosed(_ reactTag: NSNumber) {

bridge.uiManager.addUIBlock({ (uiManager, viewRegistry) in
let view = uiManager?.view(forReactTag: reactTag)
if let pipView = view as? RTCViewPip {
DispatchQueue.main.async {
pipView.onCallClosed()
}
} else {
NSLog("PiP - onCallClosed cant be called, Invalid view returned from registry, expecting RTCViewPip")
return
}
DispatchQueue.main.async {
pipView.onCallClosed()
})
}


@objc(setPreferredContentSize:width:height:)
func setPreferredContentSize(_ reactTag: NSNumber, width: CGFloat, height: CGFloat) {
let size = CGSize(width: width, height: height)

bridge.uiManager.addUIBlock({ (uiManager, viewRegistry) in
let view = uiManager?.view(forReactTag: reactTag)
if let pipView = view as? RTCViewPip {
DispatchQueue.main.async {
pipView.setPreferredContentSize(size)
}
} else {
// If the view is not found, cache the size.
// this happens when this method is called before the view can attach react super view
NSLog("PiP - View not found for reactTag \(reactTag), caching size.")
self.cachedSizes[reactTag] = size
}
})
}

func getCachedSize(for reactTag: NSNumber) -> CGSize? {
let size = self.cachedSizes.removeValue(forKey: reactTag)
if size != nil {
NSLog("PiP - Found and removed cached size for reactTag \(reactTag).")
}
return size
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#import <React/RCTUIManager.h>
#import <React/RCTView.h>
#import <React/RCTBridge.h>
#import <React/UIView+React.h>

#import <WebRTC/RTCCVPixelBuffer.h>
#import <WebRTC/RTCVideoFrame.h>
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"@react-native-firebase/app": ">=17.5.0",
"@react-native-firebase/messaging": ">=17.5.0",
"@stream-io/noise-cancellation-react-native": ">=0.1.0",
"@stream-io/react-native-webrtc": ">=125.4.0",
"@stream-io/react-native-webrtc": ">=125.4.1",
"@stream-io/video-filters-react-native": ">=0.1.0",
"expo": ">=47.0.0",
"expo-build-properties": "*",
Expand Down Expand Up @@ -126,7 +126,7 @@
"@react-native-firebase/messaging": "^22.1.0",
"@react-native/babel-preset": "^0.79.2",
"@stream-io/noise-cancellation-react-native": "workspace:^",
"@stream-io/react-native-webrtc": "^125.4.0",
"@stream-io/react-native-webrtc": "125.4.1",
"@stream-io/video-filters-react-native": "workspace:^",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "13.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ import {
getLogger,
hasScreenShare,
speakerLayoutSortPreset,
type StreamVideoParticipant,
type VideoTrackType,
} from '@stream-io/video-client';
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
import type { MediaStream } from '@stream-io/react-native-webrtc';
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useCallback } from 'react';
import { findNodeHandle } from 'react-native';
import { onNativeCallClosed, RTCViewPipNative } from './RTCViewPipNative';
import {
onNativeCallClosed,
onNativeDimensionsUpdated,
RTCViewPipNative,
} from './RTCViewPipNative';
import { useDebouncedValue } from '../../../utils/hooks';
import { shouldDisableIOSLocalVideoOnBackgroundRef } from '../../../utils/internal/shouldDisableIOSLocalVideoOnBackground';
import { useTrackDimensions } from '../../../hooks/useTrackDimensions';

type Props = {
includeLocalParticipantVideo?: boolean;
Expand Down Expand Up @@ -80,23 +87,60 @@ export const RTCViewPipIOS = React.memo((props: Props) => {
};
}, [call]);

const streamURL = useMemo(() => {
if (!participantInSpotlight) {
return undefined;
const onDimensionsUpdated = useCallback((width: number, height: number) => {
const node = findNodeHandle(nativeRef.current);
if (node !== null && width > 0 && height > 0) {
onNativeDimensionsUpdated(node, width, height);
}
}, []);

const { videoStream, screenShareStream } = participantInSpotlight;
const { videoStream, screenShareStream } = participantInSpotlight;

const isScreenSharing = hasScreenShare(participantInSpotlight);
const isScreenSharing = hasScreenShare(participantInSpotlight);

const videoStreamToRender = (isScreenSharing
? screenShareStream
: videoStream) as unknown as MediaStream | undefined;
const videoStreamToRender = (isScreenSharing
? screenShareStream
: videoStream) as unknown as MediaStream | undefined;

const streamURL = useMemo(() => {
if (!videoStreamToRender) {
return undefined;
}
return videoStreamToRender?.toURL();
}, [participantInSpotlight]);
}, [videoStreamToRender]);

return <RTCViewPipNative streamURL={streamURL} ref={nativeRef} />;
return (
<>
<RTCViewPipNative streamURL={streamURL} ref={nativeRef} />
<DimensionsUpdatedRenderless
participant={participantInSpotlight}
trackType={isScreenSharing ? 'screenShareTrack' : 'videoTrack'}
onDimensionsUpdated={onDimensionsUpdated}
key={streamURL}
/>
</>
);
});

const DimensionsUpdatedRenderless = React.memo(
({
participant,
trackType,
onDimensionsUpdated,
}: {
participant: StreamVideoParticipant;
trackType: VideoTrackType;
onDimensionsUpdated: (width: number, height: number) => void;
}) => {
const { width, height } = useTrackDimensions(participant, trackType);

useEffect(() => {
onDimensionsUpdated(width, height);
}, [width, height, onDimensionsUpdated]);

return null;
},
);

DimensionsUpdatedRenderless.displayName = 'DimensionsUpdatedRenderless';
RTCViewPipIOS.displayName = 'RTCViewPipIOS';
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ export function onNativeCallClosed(reactTag: number) {
);
}

export function onNativeDimensionsUpdated(
reactTag: number,
width: number,
height: number,
) {
getLogger(['RTCViewPipNative'])('debug', 'onNativeDimensionsUpdated', {
width,
height,
});
UIManager.dispatchViewManagerCommand(
reactTag,
UIManager.getViewManagerConfig(COMPONENT_NAME).Commands
.setPreferredContentSize,
[width, height],
);
}

/** Wrapper for the native view
* meant to stay private and not exposed */
export const RTCViewPipNative = React.memo(
Expand Down
21 changes: 16 additions & 5 deletions packages/react-native-sdk/src/hooks/useTrackDimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function useTrackDimensions(

// Set up videoTrackDimensionChanged event listener for more direct dimension updates
useEffect(() => {
if (!trackId || !NativeModules.WebRTCModule) return;
if (!trackId || !track) return;

const handleVideoTrackDimensionChanged = (eventData: {
pcId: string;
Expand All @@ -46,16 +46,27 @@ export function useTrackDimensions(
if (eventData.trackId === trackId) {
setTrackDimensions((prev) => {
if (
prev.width !== eventData.width ||
prev.height !== eventData.height
prev.width === eventData.width &&
prev.height === eventData.height
) {
return { width: eventData.width, height: eventData.height };
return prev;
}
return prev;
return { width: eventData.width, height: eventData.height };
});
}
};

const { width, height } = track.getSettings();
setTrackDimensions((prev) => {
if (prev.width === width && prev.height === height) {
return prev;
}
return {
width: width ?? 0,
height: height ?? 0,
};
});

const subscription = webRTCEventEmitter.addListener(
'videoTrackDimensionChanged',
handleVideoTrackDimensionChanged,
Expand Down
2 changes: 1 addition & 1 deletion packages/video-filters-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
},
"homepage": "https://github.com/GetStream/stream-video-js#readme",
"devDependencies": {
"@stream-io/react-native-webrtc": "^125.4.0",
"@stream-io/react-native-webrtc": "125.4.1",
"react-native": "0.79.2",
"react-native-builder-bob": "^0.37.0",
"rimraf": "^6.0.1",
Expand Down
8 changes: 4 additions & 4 deletions sample-apps/react-native/dogfood/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2291,10 +2291,10 @@ PODS:
- ReactCommon/turbomodule/core
- stream-react-native-webrtc
- Yoga
- stream-react-native-webrtc (125.4.0):
- stream-react-native-webrtc (125.4.1):
- React-Core
- StreamWebRTC (~> 125.6422.070)
- stream-video-react-native (1.20.0):
- stream-video-react-native (1.20.1):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -2722,8 +2722,8 @@ SPEC CHECKSUMS:
stream-chat-react-native: 1803cedc0bf16361b6762e8345f9256e26c60e6f
stream-io-noise-cancellation-react-native: dfb7e676688de6feab3c8e1a58fa500d17e3554a
stream-io-video-filters-react-native: 96287b284c953821fda3439be5e39a2afcc3cbad
stream-react-native-webrtc: c1fd66eafb078f1ae943b9e0c9c93c66c127348d
stream-video-react-native: 2f2277ef1f2fdd3511e3e6dfcb3a508ba77a1ed3
stream-react-native-webrtc: 3b627461be05c52d860d7b757d72ca7d4324dc9b
stream-video-react-native: 43a769786a1bc329e6aa88af3ffd570161f69131
StreamVideoNoiseCancellation: c936093dfb72540f1205cd5caec1cf31e27f40ce
StreamWebRTC: a50ebd8beba4def8f4e378b4895824c3520f9889
VisionCamera: d19797da4d373ada2c167a6e357e520cc1d9dc56
Expand Down
2 changes: 1 addition & 1 deletion sample-apps/react-native/dogfood/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@react-navigation/native": "^7.0",
"@react-navigation/native-stack": "^7.1",
"@stream-io/noise-cancellation-react-native": "workspace:^",
"@stream-io/react-native-webrtc": "125.4.0",
"@stream-io/react-native-webrtc": "125.4.1",
"@stream-io/video-filters-react-native": "workspace:^",
"@stream-io/video-react-native-sdk": "workspace:^",
"axios": "^1.8.1",
Expand Down
2 changes: 1 addition & 1 deletion sample-apps/react-native/expo-video-sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@react-native-firebase/app": "^22.1.0",
"@react-native-firebase/messaging": "^22.1.0",
"@stream-io/noise-cancellation-react-native": "workspace:^",
"@stream-io/react-native-webrtc": "125.4.0",
"@stream-io/react-native-webrtc": "125.4.1",
"@stream-io/video-filters-react-native": "workspace:^",
"@stream-io/video-react-native-sdk": "workspace:^",
"expo": "^53.0.8",
Expand Down
2 changes: 1 addition & 1 deletion sample-apps/react-native/ringing-tutorial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@react-native-firebase/messaging": "^22.1.0",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@stream-io/react-native-webrtc": "125.4.0",
"@stream-io/react-native-webrtc": "125.4.1",
"@stream-io/video-react-native-sdk": "workspace:^",
"expo": "^53.0.8",
"expo-blur": "~14.1.4",
Expand Down
Loading