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