add progress component

This commit is contained in:
Samuel Newman
2024-06-06 12:14:13 +03:00
parent 1412d2a544
commit 6a54959080
7 changed files with 155 additions and 47 deletions
+33 -6
View File
@@ -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,
}
}
+19 -7
View File
@@ -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<ComposerOpts['quote'] | undefined>(
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<Gif>()
const [labels, setLabels] = useState<string[]>([])
@@ -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({
)}
</View>
) : null}
{video && <VideoPreview video={video} />}
{videoPending && videoProcessingData ? (
<VideoTranscodeProgress
input={videoProcessingData}
progress={videoProcessingProgress}
/>
) : (
video && <VideoPreview video={video} clear={clearVideo} />
)}
</Animated.ScrollView>
<SuggestedLanguage text={richtext.text} />
@@ -612,8 +626,6 @@ export const ComposePost = observer(function ComposePost({
<SelectVideoBtn
onSelectVideo={onSelectVideo}
disabled={!canSelectImages}
pending={videoPending}
setPending={setVideoPending}
/>
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
<SelectGifBtn
@@ -1,13 +1,14 @@
import React from 'react'
import {View} from 'react-native'
import {Text} from '../../util/text/Text'
// @ts-ignore no type definition -prf
import ProgressCircle from 'react-native-progress/Circle'
// @ts-ignore no type definition -prf
import ProgressPie from 'react-native-progress/Pie'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
import {Text} from '../../util/text/Text'
const DANGER_LENGTH = MAX_GRAPHEME_LENGTH
+16 -24
View File
@@ -3,6 +3,7 @@ import {
ImagePickerAsset,
launchImageLibraryAsync,
MediaTypeOptions,
UIImagePickerPreferredAssetRepresentationMode,
} from 'expo-image-picker'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -16,35 +17,26 @@ const VIDEO_MAX_DURATION = 90
type Props = {
onSelectVideo: (video: ImagePickerAsset) => 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}>
<VideoClipIcon
size="lg"
style={(disabled || pending) && t.atoms.text_contrast_low}
style={disabled && t.atoms.text_contrast_low}
/>
</Button>
</>
+21 -2
View File
@@ -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 <Text>{JSON.stringify(video, null, 2)}</Text>
export function VideoPreview({
video,
clear,
}: {
video: FileSystem.FileInfo
clear: () => void
}) {
return (
<>
<Text>{JSON.stringify(video, null, 2)}</Text>
<Button
onPress={clear}
label="Clear"
size="small"
color="primary"
variant="solid">
<ButtonText>Clear</ButtonText>
</Button>
</>
)
}
@@ -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 (
<View style={a.mt_md}>
<View
style={[
a.flex_1,
t.atoms.bg_contrast_50,
a.rounded_md,
a.align_center,
a.justify_center,
a.gap_lg,
{aspectRatio: Math.max(input.width / input.height, 16 / 9)},
]}>
{input.duration ? (
<ProgressPie
size={64}
borderWidth={4}
borderColor={t.atoms.text.color}
color={t.atoms.text.color}
progress={progress}
/>
) : (
<Loader size="xl" />
)}
<Text>Transcoding...</Text>
</View>
</View>
)
}
+15 -5
View File
@@ -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,
}
}