mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-05-26 08:01:57 +00:00
feat: add fullscreen & Picture in Picture API (#7)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user