Skip to content

Commit 68ada56

Browse files
authored
feat: picture in picture support for iOS (#230)
* feat: picture in picture support for iOS * fix: ts error
1 parent 9ecdc21 commit 68ada56

File tree

4 files changed

+225
-121
lines changed

4 files changed

+225
-121
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ In your [AppDelegate.m](https://github.com/livekit/client-sdk-react-native/blob/
8989

9090
```objc
9191
#import "LivekitReactNative.h"
92+
#import "WebRTCModuleOptions.h"
9293

9394
@implementation AppDelegate
9495

@@ -97,6 +98,12 @@ In your [AppDelegate.m](https://github.com/livekit/client-sdk-react-native/blob/
9798
// Place this above any other RN related initialization
9899
[LivekitReactNative setup];
99100

101+
// Uncomment the following lines if you want to use the camera in the background
102+
// Requires voip background mode and iOS 18+.
103+
104+
// WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
105+
// options.enableMultitaskingCameraAccess = YES;
106+
100107
//...
101108
}
102109
```

example/ios/LivekitReactNativeExample/AppDelegate.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
1212
[LivekitReactNative setup];
1313
WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
1414
// Optional for debugging WebRTC issues.
15-
options.loggingSeverity = RTCLoggingSeverityInfo;
15+
// options.loggingSeverity = RTCLoggingSeverityInfo;
16+
options.enableMultitaskingCameraAccess = YES;
1617
self.moduleName = @"LivekitReactNativeExample";
1718

1819
// You can add your custom initial props in the dictionary below.

example/src/ParticipantView.tsx

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as React from 'react';
22

3-
import { Image, StyleSheet, ViewStyle } from 'react-native';
3+
import { Image, StyleSheet, type ViewStyle } from 'react-native';
44
import {
55
isTrackReference,
6-
TrackReferenceOrPlaceholder,
6+
type TrackReferenceOrPlaceholder,
77
useEnsureTrackRef,
88
useIsMuted,
99
useIsSpeaking,
@@ -14,63 +14,71 @@ import { View } from 'react-native';
1414
import { Text } from 'react-native';
1515
import { useTheme } from '@react-navigation/native';
1616
import { Track } from 'livekit-client';
17+
import { Component, forwardRef } from 'react';
1718
export type Props = {
1819
trackRef: TrackReferenceOrPlaceholder;
1920
style?: ViewStyle;
2021
zOrder?: number;
2122
mirror?: boolean;
23+
useIOSPIP?: boolean;
2224
};
23-
export const ParticipantView = ({
24-
style = {},
25-
trackRef,
26-
zOrder,
27-
mirror,
28-
}: Props) => {
29-
const trackReference = useEnsureTrackRef(trackRef);
30-
const { identity, name } = useParticipantInfo({
31-
participant: trackReference.participant,
32-
});
33-
const isSpeaking = useIsSpeaking(trackRef.participant);
34-
const isVideoMuted = useIsMuted(trackRef);
35-
const { colors } = useTheme();
36-
let videoView;
37-
if (isTrackReference(trackRef) && !isVideoMuted) {
38-
videoView = (
39-
<VideoTrack
40-
style={styles.videoView}
41-
trackRef={trackRef}
42-
zOrder={zOrder}
43-
mirror={mirror}
44-
/>
45-
);
46-
} else {
47-
videoView = (
48-
<View style={styles.videoView}>
49-
<View style={styles.spacer} />
50-
<Image
51-
style={styles.icon}
52-
source={require('./icons/baseline_videocam_off_white_24dp.png')}
25+
export const ParticipantView = forwardRef<Component, Props>(
26+
({ style = {}, trackRef, zOrder, mirror, useIOSPIP = false }: Props, ref) => {
27+
const trackReference = useEnsureTrackRef(trackRef);
28+
const { identity, name } = useParticipantInfo({
29+
participant: trackReference.participant,
30+
});
31+
const isSpeaking = useIsSpeaking(trackRef.participant);
32+
const isVideoMuted = useIsMuted(trackRef);
33+
const { colors } = useTheme();
34+
let videoView;
35+
if (isTrackReference(trackRef) && !isVideoMuted) {
36+
videoView = (
37+
<VideoTrack
38+
style={styles.videoView}
39+
trackRef={trackRef}
40+
zOrder={zOrder}
41+
mirror={mirror}
42+
ref={ref}
43+
iosPIP={{
44+
enabled: useIOSPIP,
45+
startAutomatically: true,
46+
preferredSize: {
47+
width: 800,
48+
height: 800,
49+
},
50+
}}
5351
/>
54-
<View style={styles.spacer} />
55-
</View>
56-
);
57-
}
52+
);
53+
} else {
54+
videoView = (
55+
<View style={styles.videoView}>
56+
<View style={styles.spacer} />
57+
<Image
58+
style={styles.icon}
59+
source={require('./icons/baseline_videocam_off_white_24dp.png')}
60+
/>
61+
<View style={styles.spacer} />
62+
</View>
63+
);
64+
}
5865

59-
let displayName = name ? name : identity;
60-
if (trackRef.source === Track.Source.ScreenShare) {
61-
displayName = displayName + "'s screen";
62-
}
66+
let displayName = name ? name : identity;
67+
if (trackRef.source === Track.Source.ScreenShare) {
68+
displayName = displayName + "'s screen";
69+
}
6370

64-
return (
65-
<View style={[styles.container, style]}>
66-
{videoView}
67-
<View style={styles.identityBar}>
68-
<Text style={{ color: colors.text }}>{displayName}</Text>
71+
return (
72+
<View style={[styles.container, style]}>
73+
{videoView}
74+
<View style={styles.identityBar}>
75+
<Text style={{ color: colors.text }}>{displayName}</Text>
76+
</View>
77+
{isSpeaking && <View style={styles.speakingIndicator} />}
6978
</View>
70-
{isSpeaking && <View style={styles.speakingIndicator} />}
71-
</View>
72-
);
73-
};
79+
);
80+
}
81+
);
7482

7583
const styles = StyleSheet.create({
7684
container: {

0 commit comments

Comments
 (0)