-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
PlaybackContext.tsx
161 lines (145 loc) · 6.65 KB
/
PlaybackContext.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import type {AVPlaybackStatus, AVPlaybackStatusToSet} from 'expo-av';
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import type {View} from 'react-native';
import type {VideoWithOnFullScreenUpdate} from '@components/VideoPlayer/types';
import useCurrentReportID from '@hooks/useCurrentReportID';
import usePrevious from '@hooks/usePrevious';
import Visibility from '@libs/Visibility';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type {PlaybackContext, StatusCallback} from './types';
const Context = React.createContext<PlaybackContext | null>(null);
function PlaybackContextProvider({children}: ChildrenProps) {
const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState<string | null>(null);
const [currentlyPlayingURLReportID, setCurrentlyPlayingURLReportID] = useState<string | undefined>();
const [sharedElement, setSharedElement] = useState<View | HTMLDivElement | null>(null);
const [originalParent, setOriginalParent] = useState<View | HTMLDivElement | null>(null);
const currentVideoPlayerRef = useRef<VideoWithOnFullScreenUpdate | null>(null);
const {currentReportID} = useCurrentReportID() ?? {};
const prevCurrentReportID = usePrevious(currentReportID);
const videoResumeTryNumberRef = useRef<number>(0);
const playVideoPromiseRef = useRef<Promise<AVPlaybackStatus>>();
const isPlayPendingRef = useRef(false);
const pauseVideo = useCallback(() => {
currentVideoPlayerRef.current?.setStatusAsync?.({shouldPlay: false});
}, [currentVideoPlayerRef]);
const stopVideo = useCallback(() => {
currentVideoPlayerRef.current?.setStatusAsync?.({shouldPlay: false, positionMillis: 0});
}, [currentVideoPlayerRef]);
const playVideo = useCallback(() => {
if (!Visibility.isVisible()) {
isPlayPendingRef.current = true;
return;
}
currentVideoPlayerRef.current?.getStatusAsync?.().then((status) => {
const newStatus: AVPlaybackStatusToSet = {shouldPlay: true};
if ('durationMillis' in status && status.durationMillis === status.positionMillis) {
newStatus.positionMillis = 0;
}
playVideoPromiseRef.current = currentVideoPlayerRef.current?.setStatusAsync(newStatus);
});
}, [currentVideoPlayerRef]);
const unloadVideo = useCallback(() => {
currentVideoPlayerRef.current?.unloadAsync?.();
}, [currentVideoPlayerRef]);
const updateCurrentlyPlayingURL = useCallback(
(url: string | null) => {
if (currentlyPlayingURL && url !== currentlyPlayingURL) {
pauseVideo();
}
setCurrentlyPlayingURLReportID(currentReportID);
setCurrentlyPlayingURL(url);
},
[currentlyPlayingURL, currentReportID, pauseVideo],
);
const shareVideoPlayerElements = useCallback(
(ref: VideoWithOnFullScreenUpdate | null, parent: View | HTMLDivElement | null, child: View | HTMLDivElement | null, shouldNotAutoPlay: boolean) => {
currentVideoPlayerRef.current = ref;
setOriginalParent(parent);
setSharedElement(child);
// Prevents autoplay when uploading the attachment
if (!shouldNotAutoPlay) {
playVideo();
}
},
[playVideo],
);
const checkVideoPlaying = useCallback(
(statusCallback: StatusCallback) => {
currentVideoPlayerRef.current?.getStatusAsync?.().then((status) => {
statusCallback('isPlaying' in status && status.isPlaying);
});
},
[currentVideoPlayerRef],
);
const resetVideoPlayerData = useCallback(() => {
// Play video is an async operation and if we call stop video before the promise is completed,
// it will throw a console error. So, we'll wait until the promise is resolved before stopping the video.
(playVideoPromiseRef.current ?? Promise.resolve()).then(stopVideo).finally(() => {
videoResumeTryNumberRef.current = 0;
setCurrentlyPlayingURL(null);
setSharedElement(null);
setOriginalParent(null);
currentVideoPlayerRef.current = null;
unloadVideo();
});
}, [stopVideo, unloadVideo]);
useEffect(() => {
// This logic ensures that resetVideoPlayerData is only called when currentReportID
// changes from one valid value (i.e., not an empty string or '-1') to another valid value.
// This prevents the video that plays when the app opens from being interrupted when currentReportID
// is initially empty or '-1', or when it changes from empty/'-1' to another value
// after the report screen in the central pane is mounted on the large screen.
if (!currentReportID || !prevCurrentReportID || currentReportID === '-1' || prevCurrentReportID === '-1' || currentReportID === prevCurrentReportID) {
return;
}
resetVideoPlayerData();
}, [currentReportID, prevCurrentReportID, resetVideoPlayerData]);
useEffect(() => {
const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => {
if (!Visibility.isVisible() || !isPlayPendingRef.current) {
return;
}
playVideo();
isPlayPendingRef.current = false;
});
return unsubscribeVisibilityListener;
}, [playVideo]);
const contextValue = useMemo(
() => ({
updateCurrentlyPlayingURL,
currentlyPlayingURL,
currentlyPlayingURLReportID,
originalParent,
sharedElement,
currentVideoPlayerRef,
shareVideoPlayerElements,
setCurrentlyPlayingURL,
playVideo,
pauseVideo,
checkVideoPlaying,
videoResumeTryNumberRef,
}),
[
updateCurrentlyPlayingURL,
currentlyPlayingURL,
currentlyPlayingURLReportID,
originalParent,
sharedElement,
shareVideoPlayerElements,
playVideo,
pauseVideo,
checkVideoPlaying,
setCurrentlyPlayingURL,
],
);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
function usePlaybackContext() {
const playbackContext = useContext(Context);
if (!playbackContext) {
throw new Error('usePlaybackContext must be used within a PlaybackContextProvider');
}
return playbackContext;
}
PlaybackContextProvider.displayName = 'PlaybackContextProvider';
export {Context as PlaybackContext, PlaybackContextProvider, usePlaybackContext};