feat: add fullscreen & Picture in Picture API (#7)

This commit is contained in:
Krzysztof Moch
2025-04-30 15:20:39 +02:00
committed by GitHub
parent 76a60b853a
commit c165dfb3b0
38 changed files with 1933 additions and 224 deletions
@@ -9,8 +9,10 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:supportsPictureInPicture="true"
android:supportsRtl="true">
<activity
android:supportsPictureInPicture="true"
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
+2 -2
View File
@@ -1565,7 +1565,7 @@ PODS:
- React-logger (= 0.77.2)
- React-perflogger (= 0.77.2)
- React-utils (= 0.77.2)
- ReactNativeVideo (7.0.0-dev):
- ReactNativeVideo (7.0.0-dev.0):
- DoubleConversion
- glog
- hermes-engine
@@ -1876,7 +1876,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: f334cebc0beed0a72490492e978007082c03d533
ReactCodegen: 474fbb3e4bb0f1ee6c255d1955db76e13d509269
ReactCommon: 7763e59534d58e15f8f22121cdfe319040e08888
ReactNativeVideo: 62c46517ad52fd59b4a312652cff3fea3ead684b
ReactNativeVideo: c92ba584a9c0b63a1ea48b990775d3773e58766e
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 31a098f74c16780569aebd614a0f37a907de0189
@@ -263,10 +263,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-VideoExample/Pods-VideoExample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-VideoExample/Pods-VideoExample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-VideoExample/Pods-VideoExample-frameworks.sh\"\n";
@@ -302,10 +306,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-VideoExample/Pods-VideoExample-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-VideoExample/Pods-VideoExample-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-VideoExample/Pods-VideoExample-resources.sh\"\n";
@@ -397,6 +405,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QQRD9CKGZW;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = VideoExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
@@ -411,6 +420,7 @@
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.reactjs.native.example.VideoExample.pip;
PRODUCT_NAME = VideoExample;
SWIFT_OBJC_BRIDGING_HEADER = "VideoExample-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -426,6 +436,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QQRD9CKGZW;
INFOPLIST_FILE = VideoExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
@@ -439,6 +450,7 @@
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.reactjs.native.example.VideoExample.pip;
PRODUCT_NAME = VideoExample;
SWIFT_OBJC_BRIDGING_HEADER = "VideoExample-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -515,10 +527,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -587,10 +596,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
+4 -1
View File
@@ -26,7 +26,6 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
@@ -34,6 +33,10 @@
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
+157 -47
View File
@@ -1,14 +1,21 @@
import Slider from '@react-native-community/slider';
import * as React from 'react';
import {
Alert,
SafeAreaView,
ScrollView,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { VideoView, createSource, useVideoPlayer } from 'react-native-video';
import {
createSource,
useVideoPlayer,
VideoView,
type VideoViewRef,
} from 'react-native-video';
const formatTime = (seconds: number) => {
if (isNaN(seconds)) return '--:--';
@@ -18,11 +25,13 @@ const formatTime = (seconds: number) => {
};
const VideoDemo = () => {
const videoViewRef = React.useRef<VideoViewRef>(null);
const [show, setShow] = React.useState(false);
const [volume, setVolume] = React.useState(1);
const [muted, setMuted] = React.useState(false);
const [rate, setRate] = React.useState(1);
const [loop, setLoop] = React.useState(false);
const [showNativeControls, setShowNativeControls] = React.useState(false);
const player = useVideoPlayer(
'https://www.w3schools.com/html/mov_bbb.mp4',
@@ -67,10 +76,20 @@ const VideoDemo = () => {
};
return (
<View style={styles.container}>
<ScrollView
style={styles.container}
contentContainerStyle={{ alignItems: 'center' }}
>
<View style={styles.card}>
{show ? (
<VideoView player={player} style={styles.video} />
<VideoView
player={player}
style={styles.video}
ref={videoViewRef}
controls={showNativeControls}
pictureInPicture={true}
autoEnterPictureInPicture={true}
/>
) : (
<View style={styles.hiddenVideo}>
<Text style={styles.hiddenVideoText}>Video Hidden</Text>
@@ -88,7 +107,7 @@ const VideoDemo = () => {
show ? (isNaN(player.duration) ? 1 : player.duration) : 0
}
value={progress}
onValueChange={handleSeek}
onSlidingComplete={handleSeek}
minimumTrackTintColor="#007aff"
maximumTrackTintColor="#ccc"
thumbTintColor="#007aff"
@@ -148,26 +167,98 @@ const VideoDemo = () => {
{/* Extra actions */}
<View style={styles.extraRow}>
<ActionButton
label="Preload"
onPress={() => player.preload().catch(console.error)}
/>
<ActionButton
label="Replace Source"
onPress={() => {
const newSource = createSource(
'https://www.w3schools.com/html/movie.mp4'
);
newSource.getAssetInformationAsync().then(console.log);
player.replaceSourceAsync(newSource);
}}
/>
<ActionButton
label={(show ? 'Hide' : 'Show') + ' Video'}
onPress={() => setShow((prev) => !prev)}
/>
<Section title="Source">
<ActionButton
label="Preload"
onPress={() => player.preload().catch(console.error)}
/>
<ActionButton
label="Replace Source"
onPress={() => {
const newSource = createSource(
'https://www.w3schools.com/html/movie.mp4'
);
newSource.getAssetInformationAsync().then(console.log);
player.replaceSourceAsync(newSource);
}}
/>
</Section>
<Section title="Video">
<ActionButton
label={(show ? 'Hide' : 'Show') + ' Video'}
onPress={() => setShow((prev) => !prev)}
/>
<View style={styles.setting}>
<Text style={styles.settingLabel}>Native Controls</Text>
<Switch
value={showNativeControls}
onValueChange={setShowNativeControls}
style={styles.switch}
/>
</View>
</Section>
<Section title="Fullscreen">
<ActionButton
label="Enter Fullscreen"
onPress={() => {
if (videoViewRef.current) {
videoViewRef.current.enterFullscreen();
} else {
Alert.alert('No video view found');
}
}}
/>
<ActionButton
label="Enter Fullscreen for 5s then exit"
onPress={() => {
if (videoViewRef.current) {
videoViewRef.current.enterFullscreen();
setTimeout(() => {
videoViewRef.current?.exitFullscreen();
}, 5000);
}
}}
/>
</Section>
<Section title="Picture In Picture">
<ActionButton
label="Enter Picture In Picture"
onPress={() => {
if (videoViewRef.current) {
videoViewRef.current.enterPictureInPicture();
} else {
Alert.alert('No video view found');
}
}}
/>
<ActionButton
label="Can Enter Picture In Picture"
onPress={() => {
if (videoViewRef.current) {
const canEnter =
videoViewRef.current.canEnterPictureInPicture();
Alert.alert(
`${canEnter ? 'Can' : 'Cannot'} Enter Picture In Picture on this device`
);
} else {
Alert.alert('No video view found');
}
}}
/>
<ActionButton
label="Exit Picture In Picture"
onPress={() => {
if (videoViewRef.current) {
videoViewRef.current.exitPictureInPicture();
} else {
Alert.alert('No video view found');
}
}}
/>
</Section>
</View>
</View>
</ScrollView>
);
};
@@ -200,6 +291,19 @@ const ActionButton = ({
</TouchableOpacity>
);
const Section = ({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) => (
<>
<Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.section}>{children}</View>
</>
);
export default function App() {
const [mounted, setMounted] = React.useState(true);
return (
@@ -218,7 +322,6 @@ export default function App() {
const styles = StyleSheet.create({
container: {
width: '100%',
alignItems: 'center',
paddingTop: 16,
},
card: {
@@ -248,13 +351,6 @@ const styles = StyleSheet.create({
fontSize: 20,
fontWeight: 'bold',
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#000a',
justifyContent: 'center',
alignItems: 'center',
zIndex: 2,
},
progressRow: {
flexDirection: 'row',
alignItems: 'center',
@@ -297,19 +393,24 @@ const styles = StyleSheet.create({
},
settingsRow: {
flexDirection: 'row',
width: 340,
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
flexWrap: 'wrap',
alignItems: 'flex-start',
width: '100%',
marginVertical: 8,
gap: 8,
paddingHorizontal: 32,
},
setting: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 4,
backgroundColor: '#fcfcfc',
borderRadius: 8,
padding: 8,
},
settingLabel: {
fontSize: 12,
fontWeight: 'bold',
color: '#555',
marginBottom: 2,
},
@@ -320,27 +421,19 @@ const styles = StyleSheet.create({
settingValue: {
fontSize: 12,
color: '#555',
marginTop: 2,
},
switch: {
width: 40,
height: 22,
marginHorizontal: 8,
borderRadius: 11,
justifyContent: 'center',
padding: 2,
},
switchKnob: {
width: 18,
height: 18,
borderRadius: 9,
backgroundColor: '#fff',
elevation: 1,
},
extraRow: {
flexDirection: 'row',
flexDirection: 'column',
justifyContent: 'center',
gap: 10,
marginVertical: 8,
paddingHorizontal: 32,
},
actionButton: {
backgroundColor: '#007aff',
@@ -353,4 +446,21 @@ const styles = StyleSheet.create({
color: '#fff',
fontWeight: 'bold',
},
section: {
padding: 8,
marginBottom: 8,
borderRadius: 8,
borderColor: 'black',
borderWidth: 1,
elevation: 2,
width: '100%',
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
},
});