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 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 { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; | |||||
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 { 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'; | ||||
▲ Show 20 Lines • Show All 50 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 { uri: videoUri, holder, encryptionKey } = mediaInfo; | ||||
const [videoSource, setVideoSource] = React.useState( | const [videoSource, setVideoSource] = React.useState( | ||||
uri ? { uri } : undefined, | videoUri ? { uri: videoUri } : undefined, | ||||
); | ); | ||||
const mediaCache = React.useContext(MediaCacheContext); | |||||
React.useEffect(() => { | React.useEffect(() => { | ||||
// skip for unencrypted videos | // skip for unencrypted videos | ||||
if (!holder || !encryptionKey) { | if (!holder || !encryptionKey) { | ||||
return; | return; | ||||
} | } | ||||
let isMounted = true; | let isMounted = true; | ||||
let uriToDispose; | let uriToDispose; | ||||
setVideoSource(undefined); | setVideoSource(undefined); | ||||
const loadDecrypted = async () => { | const loadDecrypted = async () => { | ||||
const cached = await mediaCache?.get(holder); | |||||
if (cached && isMounted) { | |||||
setVideoSource({ uri: cached }); | |||||
return; | |||||
} | |||||
const { result } = await decryptMedia(holder, encryptionKey, { | const { result } = await decryptMedia(holder, encryptionKey, { | ||||
destination: 'file', | destination: 'file', | ||||
}); | }); | ||||
if (result.success && isMounted) { | if (result.success) { | ||||
uriToDispose = result.uri; | const { uri } = result; | ||||
setVideoSource({ uri: result.uri }); | const cacheSetPromise = mediaCache?.set(holder, uri); | ||||
if (isMounted) { | |||||
uriToDispose = uri; | |||||
setVideoSource({ uri }); | |||||
} else { | |||||
// dispose of the temporary file immediately when unmounted | |||||
// but wait for the cache to be set | |||||
await cacheSetPromise; | |||||
filesystem.unlink(uri); | |||||
} | |||||
} | } | ||||
}; | }; | ||||
loadDecrypted(); | loadDecrypted(); | ||||
return () => { | return () => { | ||||
isMounted = false; | isMounted = false; | ||||
if (uriToDispose) { | if (uriToDispose) { | ||||
// remove the temporary file created by decryptMedia | // remove the temporary file created by decryptMedia | ||||
filesystem.unlink(uriToDispose); | filesystem.unlink(uriToDispose); | ||||
} | } | ||||
}; | }; | ||||
}, [holder, encryptionKey]); | }, [holder, encryptionKey, mediaCache]); | ||||
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; | ||||
▲ Show 20 Lines • Show All 677 Lines • Show Last 20 Lines |