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