From 6a54959080f88dfa1e6b7f7169c7abd347d12d04 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 6 Jun 2024 12:14:13 +0300 Subject: [PATCH] add progress component --- src/lib/media/video/compress.ts | 39 ++++++++++++--- src/view/com/composer/Composer.tsx | 26 +++++++--- .../composer/char-progress/CharProgress.tsx | 7 +-- .../com/composer/videos/SelectVideoBtn.tsx | 40 +++++++--------- src/view/com/composer/videos/VideoPreview.tsx | 23 ++++++++- .../videos/VideoTranscodeProgress.tsx | 47 +++++++++++++++++++ src/view/com/composer/videos/state.ts | 20 ++++++-- 7 files changed, 155 insertions(+), 47 deletions(-) create mode 100644 src/view/com/composer/videos/VideoTranscodeProgress.tsx diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts index bafe5dfcb0..47b3d21faa 100644 --- a/src/lib/media/video/compress.ts +++ b/src/lib/media/video/compress.ts @@ -1,16 +1,43 @@ -import {FFmpegKit, ReturnCode} from 'ffmpeg-kit-react-native' +import * as FileSystem from 'expo-file-system' +import { + FFmpegKit, + FFmpegSessionCompleteCallback, + LogCallback, + ReturnCode, + StatisticsCallback, +} from 'ffmpeg-kit-react-native' const PRESET = 'faster' -export async function compressVideo(file: string) { +export async function compressVideo( + file: string, + callbacks?: { + onLog?: LogCallback + onStatistics?: StatisticsCallback + }, +) { + const {onLog, onStatistics} = callbacks || {} + const ext = file.split('.').pop() - const newFile = file.replace(`.${ext}`, 'compressed.mp4') - const result = await FFmpegKit.execute( - `-i ${file} -c:v libx264 -crf 28 -preset ${PRESET} -b:v 4M -vf "scale='if(gt(a,1),min(1920,iw),-1)':'if(gt(a,1),-1,min(1920,ih))'" -t 90 -c:a aac -b:a 128k -movflags +faststart ${newFile}`, + const newFile = file.replace(`.${ext}`, '.compressed.mp4') + + const result = await new Promise((resolve: FFmpegSessionCompleteCallback) => + FFmpegKit.executeAsync( + `-i ${file} -c:v libx264 -crf 25 -preset ${PRESET} -b:v 4M -vf "scale='if(gt(a,1),min(1920,iw),-1)':'if(gt(a,1),-1,min(1920,ih))'" -t 90 -c:a aac -b:a 320k -movflags +faststart ${newFile}`, + resolve, + onLog, + onStatistics, + ), ) + const success = ReturnCode.isSuccess(await result.getReturnCode()) + + if (success) { + await FileSystem.deleteAsync(file) + } + return { - uri: ReturnCode.isSuccess(await result.getReturnCode()) ? newFile : null, + uri: success ? newFile : null, session: result, } } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 820c20f087..97077ec124 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -94,6 +94,7 @@ import hairlineWidth = StyleSheet.hairlineWidth import {SelectVideoBtn} from './videos/SelectVideoBtn' import {useVideoState} from './videos/state' import {VideoPreview} from './videos/VideoPreview' +import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress' type CancelRef = { onPressCancel: () => void @@ -153,9 +154,14 @@ export const ComposePost = observer(function ComposePost({ const [quote, setQuote] = useState( initQuote, ) - const {video, onSelectVideo, setVideoPending, videoPending} = useVideoState({ - setError, - }) + const { + video, + onSelectVideo, + videoPending, + videoProcessingData, + clearVideo, + videoProcessingProgress, + } = useVideoState({setError}) const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) const [extGif, setExtGif] = useState() const [labels, setLabels] = useState([]) @@ -364,7 +370,8 @@ export const ComposePost = observer(function ComposePost({ ? _(msg`Write your reply`) : _(msg`What's up?`) - const canSelectImages = gallery.size < 4 && !extLink && !video + const canSelectImages = + gallery.size < 4 && !extLink && !video && !videoPending const hasMedia = gallery.size > 0 || Boolean(extLink) || Boolean(video) const onEmojiButtonPress = useCallback(() => { @@ -590,7 +597,14 @@ export const ComposePost = observer(function ComposePost({ )} ) : null} - {video && } + {videoPending && videoProcessingData ? ( + + ) : ( + video && + )} @@ -612,8 +626,6 @@ export const ComposePost = observer(function ComposePost({ void disabled?: boolean - pending: boolean - setPending: (pending: boolean) => void } -export function SelectVideoBtn({ - onSelectVideo, - disabled, - pending, - setPending, -}: Props) { +export function SelectVideoBtn({onSelectVideo, disabled}: Props) { const {_} = useLingui() const t = useTheme() const onPressSelectVideo = useCallback(async () => { - try { - setPending(true) - const response = await launchImageLibraryAsync({ - exif: false, - mediaTypes: MediaTypeOptions.Videos, - videoMaxDuration: VIDEO_MAX_DURATION, - quality: 1, - }) - if (response.assets && response.assets.length > 0) { - onSelectVideo(response.assets[0]) - } - } finally { - setPending(false) + const response = await launchImageLibraryAsync({ + exif: false, + mediaTypes: MediaTypeOptions.Videos, + videoMaxDuration: VIDEO_MAX_DURATION, + quality: 1, + legacy: true, + preferredAssetRepresentationMode: + UIImagePickerPreferredAssetRepresentationMode.Current, + }) + if (response.assets && response.assets.length > 0) { + onSelectVideo(response.assets[0]) } - }, [onSelectVideo, setPending]) + }, [onSelectVideo]) return ( <> @@ -57,10 +49,10 @@ export function SelectVideoBtn({ variant="ghost" shape="round" color="primary" - disabled={disabled || pending}> + disabled={disabled}> diff --git a/src/view/com/composer/videos/VideoPreview.tsx b/src/view/com/composer/videos/VideoPreview.tsx index 1adf0750b1..7a9fa0e25e 100644 --- a/src/view/com/composer/videos/VideoPreview.tsx +++ b/src/view/com/composer/videos/VideoPreview.tsx @@ -1,8 +1,27 @@ import React from 'react' import * as FileSystem from 'expo-file-system' +import {Button, ButtonText} from '#/components/Button' import {Text} from '#/components/Typography' -export function VideoPreview({video}: {video: FileSystem.FileInfo}) { - return {JSON.stringify(video, null, 2)} +export function VideoPreview({ + video, + clear, +}: { + video: FileSystem.FileInfo + clear: () => void +}) { + return ( + <> + {JSON.stringify(video, null, 2)} + + + ) } diff --git a/src/view/com/composer/videos/VideoTranscodeProgress.tsx b/src/view/com/composer/videos/VideoTranscodeProgress.tsx new file mode 100644 index 0000000000..fba29a1700 --- /dev/null +++ b/src/view/com/composer/videos/VideoTranscodeProgress.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import {View} from 'react-native' +// @ts-expect-error no type definition +import ProgressPie from 'react-native-progress/Pie' +import {ImagePickerAsset} from 'expo-image-picker' + +import {atoms as a, useTheme} from '#/alf' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +export function VideoTranscodeProgress({ + input, + progress, +}: { + input: ImagePickerAsset + progress: number +}) { + const t = useTheme() + + return ( + + + {input.duration ? ( + + ) : ( + + )} + Transcoding... + + + ) +} diff --git a/src/view/com/composer/videos/state.ts b/src/view/com/composer/videos/state.ts index 2298d3d59e..96449ef0c7 100644 --- a/src/view/com/composer/videos/state.ts +++ b/src/view/com/composer/videos/state.ts @@ -8,12 +8,18 @@ import {useMutation} from '@tanstack/react-query' import {compressVideo} from '#/lib/media/video/compress' export function useVideoState({setError}: {setError: (error: string) => void}) { - const [pending, setVideoPending] = useState(false) const {_} = useLingui() + const [progress, setProgress] = useState(0) - const {mutate, data, isPending, isError, reset} = useMutation({ + const {mutate, data, isPending, isError, reset, variables} = useMutation({ mutationFn: async (asset: ImagePickerAsset) => { - const compressed = await compressVideo(asset.uri) + const compressed = await compressVideo(asset.uri, { + onStatistics: async statistics => { + if (asset.duration) { + setProgress(statistics.getTime() / asset.duration) + } + }, + }) if (!compressed.uri) { throw new Error('Failed to compress video') } @@ -37,14 +43,18 @@ export function useVideoState({setError}: {setError: (error: string) => void}) { console.error('error', error) setError(_(msg`Could not compress video`)) }, + onMutate: () => { + setProgress(0) + }, }) return { video: data, onSelectVideo: mutate, - videoPending: pending || isPending, - setVideoPending, + videoPending: isPending, + videoProcessingData: variables, videoError: isError, clearVideo: reset, + videoProcessingProgress: progress, } }