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={`Use ${isMediaPlayerImplementation() ? 'SimpleExoPlayer' : 'MediaPlayer'}`} 90 toggled={isMediaPlayerImplementation()} 91 /> 92 93 <Player 94 style={props.style} 95 errorMessage={errorMessage} 96 isLoaded={status.isLoaded} 97 isLooping={status.isLoaded ? status.isLooping : false} 98 rate={status.isLoaded ? status.rate : 1} 99 positionMillis={status.isLoaded ? status.positionMillis : 0} 100 durationMillis={status.isLoaded ? status.durationMillis || 0 : 0} 101 shouldCorrectPitch={status.isLoaded ? status.shouldCorrectPitch : false} 102 isPlaying={status.isLoaded ? status.isPlaying : false} 103 isMuted={status.isLoaded ? status.isMuted : false} 104 volume={status.isLoaded ? status.volume : 1} 105 audioPan={status.isLoaded ? status.audioPan : 0} 106 playAsync={playAsync} 107 pauseAsync={pauseAsync} 108 replayAsync={replayAsync} 109 nextAsync={changeSource} 110 setPositionAsync={setPositionAsync} 111 setIsLoopingAsync={setIsLoopingAsync} 112 setIsMutedAsync={setIsMutedAsync} 113 setRateAsync={setRateAsync} 114 setVolume={(volume, audioPan) => video.current?.setVolumeAsync(volume, audioPan)} 115 extraButtons={[ 116 () => ( 117 <ResizeModeSegmentedControl key="resizeModeControl" onValueChange={setResizeMode} /> 118 ), 119 { 120 iconName: 'options', 121 title: 'Native controls', 122 onPress: toggleNativeControls, 123 active: useNativeControls, 124 }, 125 { 126 iconName: 'resize', 127 title: 'Open full screen', 128 onPress: openFullscreen, 129 active: false, 130 }, 131 ]} 132 header={ 133 <Video 134 useNativeControls={useNativeControls} 135 ref={video} 136 source={props.sources[sourceIndex]} 137 resizeMode={resizeMode} 138 onError={setError} 139 style={{ height: 300 }} 140 progressUpdateIntervalMillis={100} 141 onPlaybackStatusUpdate={handlePlaybackStatusUpdate} 142 onFullscreenUpdate={handleFullScreenUpdate} 143 /> 144 } 145 /> 146 </View> 147 ); 148} 149 150function ResizeModeSegmentedControl({ 151 onValueChange, 152}: { 153 onValueChange: (value: ResizeMode) => void; 154}) { 155 const resizeMap: Record<string, undefined | ResizeMode> = { 156 stretch: ResizeMode.STRETCH, 157 contain: ResizeMode.CONTAIN, 158 cover: ResizeMode.COVER, 159 }; 160 const [index, setIndex] = React.useState(1); 161 let control; 162 if (Platform.OS === 'ios') { 163 control = ( 164 <SegmentedControl 165 values={Object.keys(resizeMap)} 166 fontStyle={{ color: Colors.tintColor }} 167 selectedIndex={index} 168 tintColor="white" 169 onChange={(event) => { 170 setIndex(event.nativeEvent.selectedSegmentIndex); 171 }} 172 onValueChange={(value) => { 173 const mappedValue = resizeMap[value]; 174 if (mappedValue) { 175 onValueChange(mappedValue); 176 } 177 }} 178 /> 179 ); 180 } else { 181 // Segmented control looks broken in this situation outside of iOS, so use text instead 182 control = Object.keys(resizeMap).map((mode, i) => ( 183 <Text 184 onPress={() => { 185 setIndex(i); 186 onValueChange(resizeMap[mode]!); 187 }} 188 key={mode} 189 style={{ 190 textAlign: 'center', 191 color: Colors.tintColor, 192 fontWeight: index === i ? 'bold' : 'normal', 193 marginTop: i === 0 ? 0 : 8, 194 fontSize: 12, 195 }}> 196 {mode} 197 </Text> 198 )); 199 control = <View style={{ marginTop: -5 }}>{control}</View>; 200 } 201 return ( 202 <View 203 style={{ 204 alignItems: 'stretch', 205 paddingBottom: 6, 206 margin: 10, 207 justifyContent: 'flex-end', 208 flex: 1, 209 }}> 210 {control} 211 {Platform.OS === 'ios' ? ( 212 <Text 213 style={{ 214 textAlign: 'center', 215 fontWeight: 'bold', 216 color: Colors.tintColor, 217 marginTop: 8, 218 fontSize: 12, 219 }}> 220 Resize Mode 221 </Text> 222 ) : null} 223 </View> 224 ); 225} 226