get select video button + compression working

This commit is contained in:
Samuel Newman
2024-06-05 14:57:38 +03:00
parent 4bd0ff8ba2
commit af02488812
8 changed files with 165 additions and 4 deletions
+1 -1
View File
@@ -202,7 +202,7 @@ module.exports = function (config) {
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
},
],
['@config-plugins/ffmpeg-kit-react-native', {package: 'video'}],
['@config-plugins/ffmpeg-kit-react-native', {package: 'min-gpl'}],
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2.444h2V13Zm0 4.444h-2V19h2v-1.556ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 370 B

+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const VideoClip_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2.444h2V13Zm0 4.444h-2V19h2v-1.556ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z',
})
+16
View File
@@ -0,0 +1,16 @@
import {FFmpegKit, ReturnCode} from 'ffmpeg-kit-react-native'
const PRESET = 'faster'
export async function compressVideo(file: string) {
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 1M -vf "scale=-1:720" -t 90 -c:a aac -b:a 128k -movflags +faststart ${newFile}`,
)
return {
uri: ReturnCode.isSuccess(await result.getReturnCode()) ? newFile : null,
session: result,
}
}
+16 -3
View File
@@ -91,6 +91,9 @@ import {TextInput, TextInputRef} from './text-input/TextInput'
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
import {useExternalLinkFetch} from './useExternalLinkFetch'
import hairlineWidth = StyleSheet.hairlineWidth
import {SelectVideoBtn} from './videos/SelectVideoBtn'
import {useVideoState} from './videos/state'
import {VideoPreview} from './videos/VideoPreview'
type CancelRef = {
onPressCancel: () => void
@@ -150,6 +153,9 @@ export const ComposePost = observer(function ComposePost({
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
initQuote,
)
const {video, onSelectVideo, setVideoPending, videoPending} = useVideoState({
setError,
})
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
const [extGif, setExtGif] = useState<Gif>()
const [labels, setLabels] = useState<string[]>([])
@@ -358,8 +364,8 @@ export const ComposePost = observer(function ComposePost({
? _(msg`Write your reply`)
: _(msg`What's up?`)
const canSelectImages = gallery.size < 4 && !extLink
const hasMedia = gallery.size > 0 || Boolean(extLink)
const canSelectImages = gallery.size < 4 && !extLink && !video
const hasMedia = gallery.size > 0 || Boolean(extLink) || Boolean(video)
const onEmojiButtonPress = useCallback(() => {
openPicker?.(textInput.current?.getCursorPosition())
@@ -583,7 +589,8 @@ export const ComposePost = observer(function ComposePost({
<QuoteX onRemove={() => setQuote(undefined)} />
)}
</View>
) : undefined}
) : null}
{video && <VideoPreview video={video} />}
</Animated.ScrollView>
<SuggestedLanguage text={richtext.text} />
@@ -602,6 +609,12 @@ export const ComposePost = observer(function ComposePost({
]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
<SelectVideoBtn
onSelectVideo={onSelectVideo}
disabled={!canSelectImages}
pending={videoPending}
setPending={setVideoPending}
/>
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
<SelectGifBtn
onClose={focusTextInput}
@@ -0,0 +1,68 @@
import React, {useCallback} from 'react'
import {
ImagePickerAsset,
launchImageLibraryAsync,
MediaTypeOptions,
} from 'expo-image-picker'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip'
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) {
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)
}
}, [onSelectVideo, setPending])
return (
<>
<Button
testID="openGifBtn"
onPress={onPressSelectVideo}
label={_(msg`Select GIF`)}
accessibilityHint={_(msg`Opens GIF select dialog`)}
style={a.p_sm}
variant="ghost"
shape="round"
color="primary"
disabled={disabled || pending}>
<VideoClipIcon
size="lg"
style={(disabled || pending) && t.atoms.text_contrast_low}
/>
</Button>
</>
)
}
@@ -0,0 +1,8 @@
import React from 'react'
import * as FileSystem from 'expo-file-system'
import {Text} from '#/components/Typography'
export function VideoPreview({video}: {video: FileSystem.FileInfo}) {
return <Text>{JSON.stringify(video, null, 2)}</Text>
}
+50
View File
@@ -0,0 +1,50 @@
import {useState} from 'react'
import * as FileSystem from 'expo-file-system'
import {ImagePickerAsset} from 'expo-image-picker'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
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 {mutate, data, isPending, isError, reset} = useMutation({
mutationFn: async (asset: ImagePickerAsset) => {
const compressed = await compressVideo(asset.uri)
if (!compressed.uri) {
throw new Error('Failed to compress video')
}
const res = await FileSystem.getInfoAsync(compressed.uri, {size: true})
if (res.exists) {
console.log(
'uncompressed size',
(asset.fileSize! / 1024 / 1024).toFixed(2) + 'mb',
)
console.log(
'compressed size',
(res.size / 1024 / 1024).toFixed(2) + 'mb',
)
return res
} else {
throw new Error('Could not find output video')
}
},
onError: error => {
console.error('error', error)
setError(_(msg`Could not compress video`))
},
})
return {
video: data,
onSelectVideo: mutate,
videoPending: pending || isPending,
setVideoPending,
videoError: isError,
clearVideo: reset,
}
}