add progress component
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user