|
@@ -1,40 +1,42 @@
|
|
|
-import React, { useState, useEffect, useRef } from 'react';
|
|
|
-import { View, ActivityIndicator, TouchableOpacity, Platform } from 'react-native';
|
|
|
-import { ResizeMode, Video } from 'expo-av';
|
|
|
+import React, { useState, useEffect, useMemo } from 'react';
|
|
|
+import { View, ActivityIndicator, TouchableOpacity, Platform, Image } from 'react-native';
|
|
|
+import { useEvent } from 'expo';
|
|
|
+import { useVideoPlayer, VideoView, type VideoSource } from 'expo-video';
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
import * as FileSystem from 'expo-file-system/legacy';
|
|
|
import { Colors } from 'src/theme';
|
|
|
import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
|
|
|
import { API_HOST, APP_VERSION } from 'src/constants';
|
|
|
|
|
|
+type RenderMessageVideoProps = {
|
|
|
+ props: any;
|
|
|
+ token: string;
|
|
|
+ currentUserId: number;
|
|
|
+ onLongPress: (currentMessage: any, props: any) => any;
|
|
|
+};
|
|
|
+
|
|
|
+const MAX_RETRY = 3;
|
|
|
+
|
|
|
const RenderMessageVideo = ({
|
|
|
props,
|
|
|
token,
|
|
|
currentUserId,
|
|
|
onLongPress
|
|
|
-}: {
|
|
|
- props: any;
|
|
|
- token: string;
|
|
|
- currentUserId: number;
|
|
|
- onLongPress: (currentMessage: any, props: any) => any;
|
|
|
-}) => {
|
|
|
+}: RenderMessageVideoProps) => {
|
|
|
const { currentMessage } = props;
|
|
|
-
|
|
|
if (!currentMessage?.video) return null;
|
|
|
+
|
|
|
const leftMessage = currentMessage?.user?._id !== currentUserId;
|
|
|
|
|
|
- const videoRef = useRef<Video>(null);
|
|
|
- const [isPlaying, setIsPlaying] = useState(false);
|
|
|
- const [isBuffering, setIsBuffering] = useState(true);
|
|
|
const [videoUri, setVideoUri] = useState<string | null>(null);
|
|
|
- const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
|
|
const [retryCount, setRetryCount] = useState(0);
|
|
|
- const MAX_RETRY = 3;
|
|
|
+
|
|
|
+ const [showPoster, setShowPoster] = useState(true);
|
|
|
+ const [hasStarted, setHasStarted] = useState(false);
|
|
|
|
|
|
const downloadVideo = async (videoUrl: string) => {
|
|
|
if (!videoUrl.startsWith('https')) {
|
|
|
setVideoUri(videoUrl);
|
|
|
- setIsVideoLoaded(true);
|
|
|
return videoUrl;
|
|
|
}
|
|
|
|
|
@@ -44,11 +46,13 @@ const RenderMessageVideo = ({
|
|
|
await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
|
|
|
}
|
|
|
|
|
|
- const videoPath = `${CACHED_ATTACHMENTS_DIR}${currentMessage.attachment.filename}`;
|
|
|
+ const filename =
|
|
|
+ currentMessage?.attachment?.filename ?? `video-${currentMessage?._id ?? ''}.mp4`;
|
|
|
+ const videoPath = `${CACHED_ATTACHMENTS_DIR}${filename}`;
|
|
|
|
|
|
const videoExists = await FileSystem.getInfoAsync(videoPath);
|
|
|
if (videoExists.exists) {
|
|
|
- if (videoExists.size < 1024) {
|
|
|
+ if ((videoExists.size ?? 0) < 1024) {
|
|
|
await FileSystem.deleteAsync(videoPath, { idempotent: true });
|
|
|
} else {
|
|
|
try {
|
|
@@ -56,9 +60,8 @@ const RenderMessageVideo = ({
|
|
|
encoding: FileSystem.EncodingType.Base64
|
|
|
});
|
|
|
setVideoUri(videoPath);
|
|
|
- setIsVideoLoaded(true);
|
|
|
return videoPath;
|
|
|
- } catch (e) {
|
|
|
+ } catch {
|
|
|
await FileSystem.deleteAsync(videoPath, { idempotent: true });
|
|
|
}
|
|
|
}
|
|
@@ -73,8 +76,6 @@ const RenderMessageVideo = ({
|
|
|
});
|
|
|
|
|
|
setVideoUri(downloadResult.uri);
|
|
|
- setIsVideoLoaded(true);
|
|
|
-
|
|
|
return downloadResult.uri;
|
|
|
} catch (error) {
|
|
|
console.error('Error downloading video:', error);
|
|
@@ -85,69 +86,89 @@ const RenderMessageVideo = ({
|
|
|
useEffect(() => {
|
|
|
const loadVideo = async () => {
|
|
|
if (currentMessage?.video && !currentMessage?.isSending) {
|
|
|
+ setShowPoster(true);
|
|
|
await downloadVideo(currentMessage.video);
|
|
|
}
|
|
|
};
|
|
|
-
|
|
|
loadVideo();
|
|
|
}, [currentMessage.video, currentMessage.isSending]);
|
|
|
|
|
|
- const handlePlaybackStatusUpdate = (playbackStatus: any) => {
|
|
|
- if (!playbackStatus.isLoaded) {
|
|
|
- setIsPlaying(false);
|
|
|
- setIsBuffering(false);
|
|
|
- return;
|
|
|
- }
|
|
|
+ const player = useVideoPlayer(null, (p) => {
|
|
|
+ p.loop = false;
|
|
|
+ p.muted = false;
|
|
|
+ p.volume = 1;
|
|
|
+ p.timeUpdateEventInterval = 0.5;
|
|
|
+ });
|
|
|
|
|
|
- setIsPlaying(playbackStatus.isPlaying);
|
|
|
- setIsBuffering(playbackStatus.isBuffering ?? false);
|
|
|
- };
|
|
|
+ useEffect(() => {
|
|
|
+ (async () => {
|
|
|
+ if (videoUri) {
|
|
|
+ const source: VideoSource = { uri: videoUri };
|
|
|
+ await player.replaceAsync(source);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ }, [videoUri, player]);
|
|
|
+
|
|
|
+ const { status, error } = useEvent(player, 'statusChange', { status: player.status });
|
|
|
+ const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
|
|
|
+
|
|
|
+ const isBuffering = status !== 'readyToPlay';
|
|
|
+ const isVideoLoaded = status === 'readyToPlay';
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (status === 'readyToPlay') setShowPoster(false);
|
|
|
+ }, [status]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ (async () => {
|
|
|
+ if ((status === 'error' || !!error) && retryCount < MAX_RETRY && videoUri) {
|
|
|
+ try {
|
|
|
+ await FileSystem.deleteAsync(videoUri, { idempotent: true });
|
|
|
+ } catch {}
|
|
|
+ const newUri = await downloadVideo(currentMessage.video);
|
|
|
+ if (newUri) {
|
|
|
+ setRetryCount((c) => c + 1);
|
|
|
+ setShowPoster(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ }, [status, error]);
|
|
|
+
|
|
|
+ const posterUri = useMemo(
|
|
|
+ () =>
|
|
|
+ currentMessage?.attachment?.attachment_small_url
|
|
|
+ ? API_HOST + currentMessage.attachment.attachment_small_url
|
|
|
+ : null,
|
|
|
+ [currentMessage]
|
|
|
+ );
|
|
|
|
|
|
const handlePlayPress = async () => {
|
|
|
- if (videoRef.current && isVideoLoaded) {
|
|
|
- await videoRef.current.presentFullscreenPlayer();
|
|
|
- await videoRef.current.playAsync();
|
|
|
+ if (isVideoLoaded) {
|
|
|
+ setHasStarted(true);
|
|
|
+ await player.play();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <View
|
|
|
- style={{
|
|
|
- width: 200,
|
|
|
- height: 200,
|
|
|
- padding: 6,
|
|
|
- borderRadius: 10
|
|
|
- }}
|
|
|
- >
|
|
|
+ <View style={{ width: 200, height: 200, padding: 6, borderRadius: 10, overflow: 'hidden' }}>
|
|
|
+ {posterUri && showPoster && (
|
|
|
+ <Image
|
|
|
+ source={{ uri: posterUri }}
|
|
|
+ style={{ position: 'absolute', top: 6, left: 6, right: 6, bottom: 6, borderRadius: 10 }}
|
|
|
+ resizeMode="cover"
|
|
|
+ />
|
|
|
+ )}
|
|
|
+
|
|
|
{videoUri ? (
|
|
|
- <Video
|
|
|
- ref={videoRef}
|
|
|
- source={{ uri: videoUri }}
|
|
|
+ <VideoView
|
|
|
style={{ flex: 1, borderRadius: 10 }}
|
|
|
- resizeMode={ResizeMode.CONTAIN}
|
|
|
- useNativeControls
|
|
|
- isMuted={false}
|
|
|
- volume={1.0}
|
|
|
- shouldCorrectPitch
|
|
|
- onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
|
|
|
- posterStyle={{ resizeMode: 'cover', width: '100%', height: '100%' }}
|
|
|
- usePoster={true}
|
|
|
- posterSource={{ uri: API_HOST + currentMessage.attachment.attachment_small_url }}
|
|
|
- onError={async () => {
|
|
|
- if (retryCount >= MAX_RETRY) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (videoUri) {
|
|
|
- await FileSystem.deleteAsync(videoUri, { idempotent: true });
|
|
|
-
|
|
|
- const newUri = await downloadVideo(currentMessage.video);
|
|
|
- if (newUri) {
|
|
|
- setVideoUri(newUri);
|
|
|
- setRetryCount(retryCount + 1);
|
|
|
- }
|
|
|
- }
|
|
|
+ player={player}
|
|
|
+ contentFit="contain"
|
|
|
+ nativeControls
|
|
|
+ fullscreenOptions={{
|
|
|
+ enable: true
|
|
|
}}
|
|
|
+ allowsPictureInPicture={true}
|
|
|
/>
|
|
|
) : null}
|
|
|
|
|
@@ -170,7 +191,7 @@ const RenderMessageVideo = ({
|
|
|
</View>
|
|
|
)}
|
|
|
|
|
|
- {!isPlaying && !isBuffering && videoUri && (
|
|
|
+ {!hasStarted && !isBuffering && videoUri && (
|
|
|
<TouchableOpacity
|
|
|
style={{
|
|
|
position: 'absolute',
|