1import SegmentedControl from '@react-native-segmented-control/segmented-control'; 2import { diff } from 'deep-object-diff'; 3import { Asset } from 'expo-asset'; 4import { AVPlaybackStatus, ResizeMode, Video, VideoFullscreenUpdateEvent } from 'expo-av'; 5import React from 'react'; 6import { Platform, StyleProp, Text, View, ViewStyle } from 'react-native'; 7 8import { Colors } from '../../constants'; 9import { AndroidImplementationSelector } from './AndroidImplementationSelector'; 10import Player from './Player'; 11 12type VideoPlayerSource = 13 | number 14 | { 15 uri: string; 16 overrideFileExtensionAndroid?: string; 17 headers?: { 18 [fieldName: string]: string; 19 }; 20 } 21 | Asset; 22 23export default function VideoPlayer(props: { 24 style?: StyleProp<ViewStyle>; 25 sources: VideoPlayerSource[]; 26}) { 27 const [sourceIndex, setIndex] = React.useState(0); 28 const [errorMessage, setError] = React.useState<undefined | string>(undefined); 29 const [useNativeControls, setUseNativeControls] = React.useState(false); 30 const [resizeMode, setResizeMode] = React.useState<ResizeMode>(ResizeMode.CONTAIN); 31 const prevStatus = React.useRef<AVPlaybackStatus | null>(null); 32 33 const [status, setStatus] = React.useState<AVPlaybackStatus>({ 34 isLoaded: false, 35 }); 36 37 const video = React.useRef<Video>(null); 38 39 const handlePlaybackStatusUpdate = (status: AVPlaybackStatus) => { 40 console.log('onPlaybackStatusUpdate: ', diff(prevStatus.current || {}, status)); 41 prevStatus.current = status; 42 setStatus(status); 43 }; 44 45 const handleFullScreenUpdate = (event: VideoFullscreenUpdateEvent) => 46 console.log('onFullscreenUpdate', event); 47 48 const playAsync = async () => video.current?.playAsync(); 49 50 const pauseAsync = async () => video.current?.pauseAsync(); 51 52 const replayAsync = async () => video.current?.replayAsync(); 53 54 const setPositionAsync = async (position: number) => video.current?.setPositionAsync(position); 55 56 const setIsLoopingAsync = async (isLooping: boolean) => 57 video.current?.setIsLoopingAsync(isLooping); 58 59 const setIsMutedAsync = async (isMuted: boolean) => video.current?.setIsMutedAsync(isMuted); 60 61 const setRateAsync = async (rate: number, shouldCorrectPitch: boolean) => 62 video.current?.setRateAsync(rate, shouldCorrectPitch); 63 64 const toggleNativeControls = () => 65 setUseNativeControls((useNativeControls) => !useNativeControls); 66 67 const openFullscreen = () => video.current?.presentFullscreenPlayer(); 68 69 const changeSource = () => { 70 setIndex((index) => (index + 1) % props.sources.length); 71 }; 72 const isMediaPlayerImplementation = () => status.androidImplementation === 'MediaPlayer'; 73 74 const toggleAndroidImplementation = async () => { 75 if (status.isLoaded) { 76 if (status.isPlaying) { 77 await video.current?.pauseAsync(); 78 } 79 await video.current?.unloadAsync(); 80 } 81 await video.current?.loadAsync(props.sources[sourceIndex], { 82 androidImplementation: isMediaPlayerImplementation() ? 'SimpleExoPlayer' : 'MediaPlayer', 83 }); 84 }; 85 return ( 86 <View> 87 <AndroidImplementationSelector 88 onToggle={toggleAndroidImplementation} 89 title={`Current player: ${ 90 isMediaPlayerImplementation() ? 'MediaPlayer' : 'SimpleExoPlayer' 91 }`} 92 toggled={isMediaPlayerImplementation()} 93 /> 94 95 <Player 96 style={props.style} 97 errorMessage={errorMessage} 98 isLoaded={status.isLoaded} 99 isLooping={status.isLoaded ? status.isLooping : false} 100 rate={status.isLoaded ? status.rate : 1} 101 positionMillis={status.isLoaded ? status.positionMillis : 0} 102 durationMillis={status.isLoaded ? status.durationMillis || 0 : 0} 103 shouldCorrectPitch={status.isLoaded ? status.shouldCorrectPitch : false} 104 isPlaying={status.isLoaded ? status.isPlaying : false} 105 isMuted={status.isLoaded ? status.isMuted : false} 106 volume={status.isLoaded ? status.volume : 1} 107 audioPan={status.isLoaded ? status.audioPan : 0} 108 playAsync={playAsync} 109 pauseAsync={pauseAsync} 110 replayAsync={replayAsync} 111 nextAsync={changeSource} 112 setPositionAsync={setPositionAsync} 113 setIsLoopingAsync={setIsLoopingAsync} 114 setIsMutedAsync={setIsMutedAsync} 115 setRateAsync={setRateAsync} 116 setVolume={(volume, audioPan) => video.current?.setVolumeAsync(volume, audioPan)} 117 extraButtons={[ 118 () => ( 119 <ResizeModeSegmentedControl key="resizeModeControl" onValueChange={setResizeMode} /> 120 ), 121 { 122 iconName: 'options', 123 title: 'Native controls', 124 onPress: toggleNativeControls, 125 active: useNativeControls, 126 }, 127 { 128 iconName: 'resize', 129 title: 'Open full screen', 130 onPress: openFullscreen, 131 active: false, 132 }, 133 ]} 134 header={ 135 <Video 136 useNativeControls={useNativeControls} 137 ref={video} 138 source={props.sources[sourceIndex]} 139 resizeMode={resizeMode} 140 onError={setError} 141 style={{ height: 300 }} 142 progressUpdateIntervalMillis={100} 143 onPlaybackStatusUpdate={handlePlaybackStatusUpdate} 144 onFullscreenUpdate={handleFullScreenUpdate} 145 /> 146 } 147 /> 148 </View> 149 ); 150} 151 152function ResizeModeSegmentedControl({ 153 onValueChange, 154}: { 155 onValueChange: (value: ResizeMode) => void; 156}) { 157 const resizeMap: Record<string, undefined | ResizeMode> = { 158 stretch: ResizeMode.STRETCH, 159 contain: ResizeMode.CONTAIN, 160 cover: ResizeMode.COVER, 161 }; 162 const [index, setIndex] = React.useState(1); 163 let control; 164 if (Platform.OS === 'ios') { 165 control = ( 166 <SegmentedControl 167 values={Object.keys(resizeMap)} 168 fontStyle={{ color: Colors.tintColor }} 169 selectedIndex={index} 170 tintColor="white" 171 onChange={(event) => { 172 setIndex(event.nativeEvent.selectedSegmentIndex); 173 }} 174 onValueChange={(value) => { 175 const mappedValue = resizeMap[value]; 176 if (mappedValue) { 177 onValueChange(mappedValue); 178 } 179 }} 180 /> 181 ); 182 } else { 183 // Segmented control looks broken in this situation outside of iOS, so use text instead 184 control = Object.keys(resizeMap).map((mode, i) => ( 185 <Text 186 onPress={() => { 187 setIndex(i); 188 onValueChange(resizeMap[mode]!); 189 }} 190 key={mode} 191 style={{ 192 textAlign: 'center', 193 color: Colors.tintColor, 194 fontWeight: index === i ? 'bold' : 'normal', 195 marginTop: i === 0 ? 0 : 8, 196 fontSize: 12, 197 }}> 198 {mode} 199 </Text> 200 )); 201 control = <View style={{ marginTop: -5 }}>{control}</View>; 202 } 203 return ( 204 <View 205 style={{ 206 alignItems: 'stretch', 207 paddingBottom: 6, 208 margin: 10, 209 justifyContent: 'flex-end', 210 flex: 1, 211 }}> 212 {control} 213 {Platform.OS === 'ios' ? ( 214 <Text 215 style={{ 216 textAlign: 'center', 217 fontWeight: 'bold', 218 color: Colors.tintColor, 219 marginTop: 8, 220 fontSize: 12, 221 }}> 222 Resize Mode 223 </Text> 224 ) : null} 225 </View> 226 ); 227} 228