Skip to content

Commit 2c553c9

Browse files
authored
fix: support setting iOS pip window sizes (#1876)
### 💡 Overview iOS pip window did not know what size to set. And so size was set by the OS. This was not ideal as it seemed to completely random at times. In this PR we add support for PiP window size based on track size. https://github.com/user-attachments/assets/0875304c-b812-4fc6-b74b-78d3c48c4fd1 ### 📝 Implementation notes Sends track dimensions from JS to Native
1 parent e450ce2 commit 2c553c9

File tree

16 files changed

+173
-48
lines changed

16 files changed

+173
-48
lines changed

packages/noise-cancellation-react-native/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
},
4848
"homepage": "https://github.com/GetStream/stream-video-js#readme",
4949
"devDependencies": {
50-
"@stream-io/react-native-webrtc": "^125.4.0",
50+
"@stream-io/react-native-webrtc": "125.4.1",
5151
"react-native": "^0.79.2",
5252
"react-native-builder-bob": "^0.37.0",
5353
"rimraf": "^6.0.1",

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ import Foundation
8181
super.init()
8282
}
8383

84+
func setPreferredContentSize(_ size: CGSize) {
85+
contentViewController?.preferredContentSize = size
86+
}
87+
8488
// MARK: - AVPictureInPictureControllerDelegate
8589

8690
func pictureInPictureController(

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import React
910

1011
@objc(RTCViewPip)
1112
class RTCViewPip: UIView {
@@ -49,7 +50,7 @@ class RTCViewPip: UIView {
4950
}
5051

5152
DispatchQueue.main.async {
52-
NSLog("PiP - Setting video track for streamURL: -\(streamURLString)")
53+
NSLog("PiP - Setting video track for streamURL: -\(streamURLString) trackId: \(videoTrack.trackId)")
5354
self.pictureInPictureController?.track = videoTrack
5455
}
5556
}
@@ -67,20 +68,33 @@ class RTCViewPip: UIView {
6768
self.pictureInPictureController = nil
6869
}
6970

71+
@objc
72+
func setPreferredContentSize(_ size: CGSize) {
73+
NSLog("PiP - RTCViewPip setPreferredContentSize \(size)")
74+
self.pictureInPictureController?.setPreferredContentSize(size)
75+
}
76+
7077
override func didMoveToSuperview() {
7178
super.didMoveToSuperview()
7279
if self.superview == nil {
73-
print("PiP - RTCViewPip has been removed from its superview.")
80+
NSLog("PiP - RTCViewPip has been removed from its superview.")
7481
NotificationCenter.default.removeObserver(self)
7582
DispatchQueue.main.async {
7683
NSLog("PiP - onCallClosed called due to view detaching")
7784
self.onCallClosed()
7885
}
7986
} else {
80-
print("PiP - RTCViewPip has been added to a superview.")
87+
NSLog("PiP - RTCViewPip has been added to a superview.")
8188
setupNotificationObserver()
8289
DispatchQueue.main.async {
8390
self.pictureInPictureController?.sourceView = self
91+
if let reactTag = self.reactTag, let bridge = self.webRtcModule?.bridge {
92+
if let manager = bridge.module(for: RTCViewPipManager.self) as? RTCViewPipManager,
93+
let size = manager.getCachedSize(for: reactTag) {
94+
NSLog("PiP - Applying cached size \(size) for reactTag \(reactTag)")
95+
self.setPreferredContentSize(size)
96+
}
97+
}
8498
}
8599
}
86100
}

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

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

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

1617
@end

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

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import Foundation
1010
@objc(RTCViewPipManager)
1111
class RTCViewPipManager: RCTViewManager {
1212

13+
private var cachedSizes: [NSNumber: CGSize] = [:]
14+
1315
override func view() -> UIView! {
1416
let view = RTCViewPip()
1517
view.setWebRtcModule(self.bridge.module(forName: "WebRTCModule") as! WebRTCModule)
@@ -20,15 +22,46 @@ class RTCViewPipManager: RCTViewManager {
2022
return true
2123
}
2224

23-
@objc func onCallClosed(_ reactTag: NSNumber) {
24-
self.bridge!.uiManager.addUIBlock { (_: RCTUIManager?, viewRegistry: [NSNumber: UIView]?) in
25-
guard let pipView = viewRegistry?[reactTag] as? RTCViewPip else {
25+
@objc(onCallClosed:)
26+
func onCallClosed(_ reactTag: NSNumber) {
27+
28+
bridge.uiManager.addUIBlock({ (uiManager, viewRegistry) in
29+
let view = uiManager?.view(forReactTag: reactTag)
30+
if let pipView = view as? RTCViewPip {
31+
DispatchQueue.main.async {
32+
pipView.onCallClosed()
33+
}
34+
} else {
2635
NSLog("PiP - onCallClosed cant be called, Invalid view returned from registry, expecting RTCViewPip")
27-
return
2836
}
29-
DispatchQueue.main.async {
30-
pipView.onCallClosed()
37+
})
38+
}
39+
40+
41+
@objc(setPreferredContentSize:width:height:)
42+
func setPreferredContentSize(_ reactTag: NSNumber, width: CGFloat, height: CGFloat) {
43+
let size = CGSize(width: width, height: height)
44+
45+
bridge.uiManager.addUIBlock({ (uiManager, viewRegistry) in
46+
let view = uiManager?.view(forReactTag: reactTag)
47+
if let pipView = view as? RTCViewPip {
48+
DispatchQueue.main.async {
49+
pipView.setPreferredContentSize(size)
50+
}
51+
} else {
52+
// If the view is not found, cache the size.
53+
// this happens when this method is called before the view can attach react super view
54+
NSLog("PiP - View not found for reactTag \(reactTag), caching size.")
55+
self.cachedSizes[reactTag] = size
3156
}
57+
})
58+
}
59+
60+
func getCachedSize(for reactTag: NSNumber) -> CGSize? {
61+
let size = self.cachedSizes.removeValue(forKey: reactTag)
62+
if size != nil {
63+
NSLog("PiP - Found and removed cached size for reactTag \(reactTag).")
3264
}
65+
return size
3366
}
3467
}

packages/react-native-sdk/ios/StreamVideoReactNative-Bridging-Header.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#import <React/RCTUIManager.h>
66
#import <React/RCTView.h>
77
#import <React/RCTBridge.h>
8+
#import <React/UIView+React.h>
89

910
#import <WebRTC/RTCCVPixelBuffer.h>
1011
#import <WebRTC/RTCVideoFrame.h>

packages/react-native-sdk/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"@react-native-firebase/app": ">=17.5.0",
6161
"@react-native-firebase/messaging": ">=17.5.0",
6262
"@stream-io/noise-cancellation-react-native": ">=0.1.0",
63-
"@stream-io/react-native-webrtc": ">=125.4.0",
63+
"@stream-io/react-native-webrtc": ">=125.4.1",
6464
"@stream-io/video-filters-react-native": ">=0.1.0",
6565
"expo": ">=47.0.0",
6666
"expo-build-properties": "*",
@@ -126,7 +126,7 @@
126126
"@react-native-firebase/messaging": "^22.1.0",
127127
"@react-native/babel-preset": "^0.79.2",
128128
"@stream-io/noise-cancellation-react-native": "workspace:^",
129-
"@stream-io/react-native-webrtc": "^125.4.0",
129+
"@stream-io/react-native-webrtc": "125.4.1",
130130
"@stream-io/video-filters-react-native": "workspace:^",
131131
"@testing-library/jest-native": "^5.4.3",
132132
"@testing-library/react-native": "13.2.0",

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

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ import {
33
getLogger,
44
hasScreenShare,
55
speakerLayoutSortPreset,
6+
type StreamVideoParticipant,
7+
type VideoTrackType,
68
} from '@stream-io/video-client';
79
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
810
import type { MediaStream } from '@stream-io/react-native-webrtc';
9-
import React, { useEffect, useMemo } from 'react';
11+
import React, { useEffect, useMemo, useCallback } from 'react';
1012
import { findNodeHandle } from 'react-native';
11-
import { onNativeCallClosed, RTCViewPipNative } from './RTCViewPipNative';
13+
import {
14+
onNativeCallClosed,
15+
onNativeDimensionsUpdated,
16+
RTCViewPipNative,
17+
} from './RTCViewPipNative';
1218
import { useDebouncedValue } from '../../../utils/hooks';
1319
import { shouldDisableIOSLocalVideoOnBackgroundRef } from '../../../utils/internal/shouldDisableIOSLocalVideoOnBackground';
20+
import { useTrackDimensions } from '../../../hooks/useTrackDimensions';
1421

1522
type Props = {
1623
includeLocalParticipantVideo?: boolean;
@@ -80,23 +87,60 @@ export const RTCViewPipIOS = React.memo((props: Props) => {
8087
};
8188
}, [call]);
8289

83-
const streamURL = useMemo(() => {
84-
if (!participantInSpotlight) {
85-
return undefined;
90+
const onDimensionsUpdated = useCallback((width: number, height: number) => {
91+
const node = findNodeHandle(nativeRef.current);
92+
if (node !== null && width > 0 && height > 0) {
93+
onNativeDimensionsUpdated(node, width, height);
8694
}
95+
}, []);
8796

88-
const { videoStream, screenShareStream } = participantInSpotlight;
97+
const { videoStream, screenShareStream } = participantInSpotlight;
8998

90-
const isScreenSharing = hasScreenShare(participantInSpotlight);
99+
const isScreenSharing = hasScreenShare(participantInSpotlight);
91100

92-
const videoStreamToRender = (isScreenSharing
93-
? screenShareStream
94-
: videoStream) as unknown as MediaStream | undefined;
101+
const videoStreamToRender = (isScreenSharing
102+
? screenShareStream
103+
: videoStream) as unknown as MediaStream | undefined;
95104

105+
const streamURL = useMemo(() => {
106+
if (!videoStreamToRender) {
107+
return undefined;
108+
}
96109
return videoStreamToRender?.toURL();
97-
}, [participantInSpotlight]);
110+
}, [videoStreamToRender]);
98111

99-
return <RTCViewPipNative streamURL={streamURL} ref={nativeRef} />;
112+
return (
113+
<>
114+
<RTCViewPipNative streamURL={streamURL} ref={nativeRef} />
115+
<DimensionsUpdatedRenderless
116+
participant={participantInSpotlight}
117+
trackType={isScreenSharing ? 'screenShareTrack' : 'videoTrack'}
118+
onDimensionsUpdated={onDimensionsUpdated}
119+
key={streamURL}
120+
/>
121+
</>
122+
);
100123
});
101124

125+
const DimensionsUpdatedRenderless = React.memo(
126+
({
127+
participant,
128+
trackType,
129+
onDimensionsUpdated,
130+
}: {
131+
participant: StreamVideoParticipant;
132+
trackType: VideoTrackType;
133+
onDimensionsUpdated: (width: number, height: number) => void;
134+
}) => {
135+
const { width, height } = useTrackDimensions(participant, trackType);
136+
137+
useEffect(() => {
138+
onDimensionsUpdated(width, height);
139+
}, [width, height, onDimensionsUpdated]);
140+
141+
return null;
142+
},
143+
);
144+
145+
DimensionsUpdatedRenderless.displayName = 'DimensionsUpdatedRenderless';
102146
RTCViewPipIOS.displayName = 'RTCViewPipIOS';

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ export function onNativeCallClosed(reactTag: number) {
2626
);
2727
}
2828

29+
export function onNativeDimensionsUpdated(
30+
reactTag: number,
31+
width: number,
32+
height: number,
33+
) {
34+
getLogger(['RTCViewPipNative'])('debug', 'onNativeDimensionsUpdated', {
35+
width,
36+
height,
37+
});
38+
UIManager.dispatchViewManagerCommand(
39+
reactTag,
40+
UIManager.getViewManagerConfig(COMPONENT_NAME).Commands
41+
.setPreferredContentSize,
42+
[width, height],
43+
);
44+
}
45+
2946
/** Wrapper for the native view
3047
* meant to stay private and not exposed */
3148
export const RTCViewPipNative = React.memo(

packages/react-native-sdk/src/hooks/useTrackDimensions.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function useTrackDimensions(
3434

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

3939
const handleVideoTrackDimensionChanged = (eventData: {
4040
pcId: string;
@@ -46,16 +46,27 @@ export function useTrackDimensions(
4646
if (eventData.trackId === trackId) {
4747
setTrackDimensions((prev) => {
4848
if (
49-
prev.width !== eventData.width ||
50-
prev.height !== eventData.height
49+
prev.width === eventData.width &&
50+
prev.height === eventData.height
5151
) {
52-
return { width: eventData.width, height: eventData.height };
52+
return prev;
5353
}
54-
return prev;
54+
return { width: eventData.width, height: eventData.height };
5555
});
5656
}
5757
};
5858

59+
const { width, height } = track.getSettings();
60+
setTrackDimensions((prev) => {
61+
if (prev.width === width && prev.height === height) {
62+
return prev;
63+
}
64+
return {
65+
width: width ?? 0,
66+
height: height ?? 0,
67+
};
68+
});
69+
5970
const subscription = webRTCEventEmitter.addListener(
6071
'videoTrackDimensionChanged',
6172
handleVideoTrackDimensionChanged,

0 commit comments

Comments
 (0)