Changeset View
Changeset View
Standalone View
Standalone View
native/media/video-playback-modal.react.js
// @flow | // @flow | ||||
import Icon from '@expo/vector-icons/MaterialCommunityIcons.js'; | import Icon from '@expo/vector-icons/MaterialCommunityIcons.js'; | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { useState } from 'react'; | import { useState } from 'react'; | ||||
import { View, Text, TouchableOpacity } from 'react-native'; | import { View, Text, TouchableOpacity } from 'react-native'; | ||||
import filesystem from 'react-native-fs'; | |||||
import { TapGestureHandler } from 'react-native-gesture-handler'; | import { TapGestureHandler } from 'react-native-gesture-handler'; | ||||
import * as Progress from 'react-native-progress'; | import * as Progress from 'react-native-progress'; | ||||
import Animated from 'react-native-reanimated'; | import Animated from 'react-native-reanimated'; | ||||
import { SafeAreaView } from 'react-native-safe-area-context'; | import { SafeAreaView } from 'react-native-safe-area-context'; | ||||
import Video from 'react-native-video'; | import Video from 'react-native-video'; | ||||
import { useIsAppBackgroundedOrInactive } from 'lib/shared/lifecycle-utils.js'; | import { useIsAppBackgroundedOrInactive } from 'lib/shared/lifecycle-utils.js'; | ||||
import type { MediaInfo } from 'lib/types/media-types.js'; | import type { MediaInfo } from 'lib/types/media-types.js'; | ||||
import { decryptMedia } from './encryption-utils.js'; | |||||
import { formatDuration } from './video-utils.js'; | import { formatDuration } from './video-utils.js'; | ||||
import ConnectedStatusBar from '../connected-status-bar.react.js'; | import ConnectedStatusBar from '../connected-status-bar.react.js'; | ||||
import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; | import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; | ||||
import { OverlayContext } from '../navigation/overlay-context.js'; | import { OverlayContext } from '../navigation/overlay-context.js'; | ||||
import type { NavigationRoute } from '../navigation/route-names.js'; | import type { NavigationRoute } from '../navigation/route-names.js'; | ||||
import { useSelector } from '../redux/redux-utils.js'; | import { useSelector } from '../redux/redux-utils.js'; | ||||
import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js'; | import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js'; | ||||
import { useStyles } from '../themes/colors.js'; | import { useStyles } from '../themes/colors.js'; | ||||
▲ Show 20 Lines • Show All 46 Lines • ▼ Show 20 Lines | |||||
type Props = { | type Props = { | ||||
+navigation: AppNavigationProp<'VideoPlaybackModal'>, | +navigation: AppNavigationProp<'VideoPlaybackModal'>, | ||||
+route: NavigationRoute<'VideoPlaybackModal'>, | +route: NavigationRoute<'VideoPlaybackModal'>, | ||||
}; | }; | ||||
function VideoPlaybackModal(props: Props): React.Node { | function VideoPlaybackModal(props: Props): React.Node { | ||||
const { mediaInfo } = props.route.params; | const { mediaInfo } = props.route.params; | ||||
const { uri, holder, encryptionKey } = mediaInfo; | |||||
const [videoSource, setVideoSource] = React.useState( | |||||
uri ? { uri } : undefined, | |||||
); | |||||
React.useEffect(() => { | |||||
// skip for unencrypted videos | |||||
if (!holder || !encryptionKey) { | |||||
return; | |||||
} | |||||
let isMounted = true; | |||||
let uriToDispose; | |||||
setVideoSource(undefined); | |||||
const loadDecrypted = async () => { | |||||
const { result } = await decryptMedia(holder, encryptionKey, { | |||||
destination: 'file', | |||||
}); | |||||
if (result.success && isMounted) { | |||||
uriToDispose = result.uri; | |||||
setVideoSource({ uri: result.uri }); | |||||
} | |||||
}; | |||||
loadDecrypted(); | |||||
return () => { | |||||
isMounted = false; | |||||
if (uriToDispose) { | |||||
// remove the temporary file created by decryptMedia | |||||
filesystem.unlink(uriToDispose); | |||||
} | |||||
}; | |||||
}, [holder, encryptionKey]); | |||||
const closeButtonX = useValue(-1); | const closeButtonX = useValue(-1); | ||||
const closeButtonY = useValue(-1); | const closeButtonY = useValue(-1); | ||||
const closeButtonWidth = useValue(-1); | const closeButtonWidth = useValue(-1); | ||||
const closeButtonHeight = useValue(-1); | const closeButtonHeight = useValue(-1); | ||||
const closeButtonRef = | const closeButtonRef = | ||||
React.useRef<?React.ElementRef<TouchableOpacityInstance>>(); | React.useRef<?React.ElementRef<TouchableOpacityInstance>>(); | ||||
const closeButton = closeButtonRef.current; | const closeButton = closeButtonRef.current; | ||||
const onCloseButtonLayoutCalledRef = React.useRef(false); | const onCloseButtonLayoutCalledRef = React.useRef(false); | ||||
▲ Show 20 Lines • Show All 425 Lines • ▼ Show 20 Lines | function VideoPlaybackModal(props: Props): React.Node { | ||||
const backgroundedOrInactive = useIsAppBackgroundedOrInactive(); | const backgroundedOrInactive = useIsAppBackgroundedOrInactive(); | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
if (backgroundedOrInactive) { | if (backgroundedOrInactive) { | ||||
setPaused(true); | setPaused(true); | ||||
controlsShowing.setValue(1); | controlsShowing.setValue(1); | ||||
} | } | ||||
}, [backgroundedOrInactive, controlsShowing]); | }, [backgroundedOrInactive, controlsShowing]); | ||||
const { | const { navigation } = props; | ||||
navigation, | |||||
route: { | |||||
params: { | |||||
mediaInfo: { uri: videoUri }, | |||||
}, | |||||
}, | |||||
} = props; | |||||
const togglePlayback = React.useCallback(() => { | const togglePlayback = React.useCallback(() => { | ||||
setPaused(!paused); | setPaused(!paused); | ||||
}, [paused]); | }, [paused]); | ||||
const resetVideo = React.useCallback(() => { | const resetVideo = React.useCallback(() => { | ||||
invariant(videoRef.current, 'videoRef.current should be set in resetVideo'); | invariant(videoRef.current, 'videoRef.current should be set in resetVideo'); | ||||
videoRef.current.seek(0); | videoRef.current.seek(0); | ||||
Show All 32 Lines | function VideoPlaybackModal(props: Props): React.Node { | ||||
}, [ | }, [ | ||||
screenDimensions.height, | screenDimensions.height, | ||||
verticalBounds.y, | verticalBounds.y, | ||||
verticalBounds.height, | verticalBounds.height, | ||||
overlayContext.isDismissing, | overlayContext.isDismissing, | ||||
styles.contentContainer, | styles.contentContainer, | ||||
]); | ]); | ||||
const controls = ( | let controls; | ||||
if (videoSource) { | |||||
controls = ( | |||||
<Animated.View | <Animated.View | ||||
style={[styles.controls, { opacity: controlsOpacity }]} | style={[styles.controls, { opacity: controlsOpacity }]} | ||||
pointerEvents={controlsEnabled ? 'box-none' : 'none'} | pointerEvents={controlsEnabled ? 'box-none' : 'none'} | ||||
> | > | ||||
<SafeAreaView style={styles.fill}> | <SafeAreaView style={styles.fill}> | ||||
<View style={styles.fill}> | <View style={styles.fill}> | ||||
<View style={styles.header}> | <View style={styles.header}> | ||||
<View style={styles.closeButton}> | <View style={styles.closeButton}> | ||||
<TouchableOpacity | <TouchableOpacity | ||||
onPress={navigation.goBackOnce} | onPress={navigation.goBackOnce} | ||||
ref={(closeButtonRef: any)} | ref={(closeButtonRef: any)} | ||||
onLayout={onCloseButtonLayout} | onLayout={onCloseButtonLayout} | ||||
> | > | ||||
<Icon name="close" size={30} style={styles.iconButton} /> | <Icon name="close" size={30} style={styles.iconButton} /> | ||||
</TouchableOpacity> | </TouchableOpacity> | ||||
</View> | </View> | ||||
</View> | </View> | ||||
<View style={styles.footer} ref={footerRef} onLayout={onFooterLayout}> | <View | ||||
style={styles.footer} | |||||
ref={footerRef} | |||||
onLayout={onFooterLayout} | |||||
> | |||||
<TouchableOpacity | <TouchableOpacity | ||||
onPress={togglePlayback} | onPress={togglePlayback} | ||||
style={styles.playPauseButton} | style={styles.playPauseButton} | ||||
> | > | ||||
<Icon | <Icon | ||||
name={paused ? 'play' : 'pause'} | name={paused ? 'play' : 'pause'} | ||||
size={28} | size={28} | ||||
style={styles.iconButton} | style={styles.iconButton} | ||||
/> | /> | ||||
</TouchableOpacity> | </TouchableOpacity> | ||||
<View style={styles.progressBar}> | <View style={styles.progressBar}> | ||||
<Progress.Bar | <Progress.Bar | ||||
progress={percentElapsed / 100} | progress={percentElapsed / 100} | ||||
height={4} | height={4} | ||||
width={null} | width={null} | ||||
color={styles.progressBar.color} | color={styles.progressBar.color} | ||||
style={styles.expand} | style={styles.expand} | ||||
/> | /> | ||||
</View> | </View> | ||||
<Text style={styles.durationText}> | <Text style={styles.durationText}> | ||||
{timeElapsed} / {totalDuration} | {timeElapsed} / {totalDuration} | ||||
</Text> | </Text> | ||||
</View> | </View> | ||||
</View> | </View> | ||||
</SafeAreaView> | </SafeAreaView> | ||||
</Animated.View> | </Animated.View> | ||||
); | ); | ||||
} | |||||
let spinner; | let spinner; | ||||
if (spinnerVisible) { | if (spinnerVisible) { | ||||
spinner = ( | spinner = ( | ||||
<Progress.Circle | <Progress.Circle | ||||
size={80} | size={80} | ||||
indeterminate={true} | indeterminate={true} | ||||
color="white" | color="white" | ||||
style={styles.progressCircle} | style={styles.progressCircle} | ||||
/> | /> | ||||
); | ); | ||||
} | } | ||||
return ( | let videoPlayer; | ||||
<TapGestureHandler onHandlerStateChange={singleTapEvent} minPointers={1}> | if (videoSource) { | ||||
<Animated.View style={styles.expand}> | videoPlayer = ( | ||||
{statusBar} | |||||
<Animated.View style={[styles.backdrop, backdropStyle]} /> | |||||
<View style={contentContainerStyle}> | |||||
{spinner} | |||||
<Animated.View style={videoContainerStyle}> | |||||
<Video | <Video | ||||
source={{ uri: videoUri }} | source={videoSource} | ||||
ref={videoRef} | ref={videoRef} | ||||
style={styles.backgroundVideo} | style={styles.backgroundVideo} | ||||
paused={paused} | paused={paused} | ||||
onProgress={progressCallback} | onProgress={progressCallback} | ||||
onEnd={resetVideo} | onEnd={resetVideo} | ||||
onReadyForDisplay={readyForDisplayCallback} | onReadyForDisplay={readyForDisplayCallback} | ||||
/> | /> | ||||
); | |||||
} | |||||
return ( | |||||
<TapGestureHandler onHandlerStateChange={singleTapEvent} minPointers={1}> | |||||
<Animated.View style={styles.expand}> | |||||
{statusBar} | |||||
<Animated.View style={[styles.backdrop, backdropStyle]} /> | |||||
<View style={contentContainerStyle}> | |||||
{spinner} | |||||
<Animated.View style={videoContainerStyle}> | |||||
{videoPlayer} | |||||
</Animated.View> | </Animated.View> | ||||
</View> | </View> | ||||
{controls} | {controls} | ||||
</Animated.View> | </Animated.View> | ||||
</TapGestureHandler> | </TapGestureHandler> | ||||
); | ); | ||||
} | } | ||||
▲ Show 20 Lines • Show All 96 Lines • Show Last 20 Lines |