get select video button + compression working
This commit is contained in:
+1
-1
@@ -202,7 +202,7 @@ module.exports = function (config) {
|
|||||||
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
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/withAndroidManifestPlugin.js',
|
||||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||||
'./plugins/withAndroidStylesWindowBackgroundPlugin.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 |
@@ -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',
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,6 +91,9 @@ import {TextInput, TextInputRef} from './text-input/TextInput'
|
|||||||
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
||||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
||||||
import hairlineWidth = StyleSheet.hairlineWidth
|
import hairlineWidth = StyleSheet.hairlineWidth
|
||||||
|
import {SelectVideoBtn} from './videos/SelectVideoBtn'
|
||||||
|
import {useVideoState} from './videos/state'
|
||||||
|
import {VideoPreview} from './videos/VideoPreview'
|
||||||
|
|
||||||
type CancelRef = {
|
type CancelRef = {
|
||||||
onPressCancel: () => void
|
onPressCancel: () => void
|
||||||
@@ -150,6 +153,9 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
|
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
|
||||||
initQuote,
|
initQuote,
|
||||||
)
|
)
|
||||||
|
const {video, onSelectVideo, setVideoPending, videoPending} = useVideoState({
|
||||||
|
setError,
|
||||||
|
})
|
||||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||||
const [extGif, setExtGif] = useState<Gif>()
|
const [extGif, setExtGif] = useState<Gif>()
|
||||||
const [labels, setLabels] = useState<string[]>([])
|
const [labels, setLabels] = useState<string[]>([])
|
||||||
@@ -358,8 +364,8 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
? _(msg`Write your reply`)
|
? _(msg`Write your reply`)
|
||||||
: _(msg`What's up?`)
|
: _(msg`What's up?`)
|
||||||
|
|
||||||
const canSelectImages = gallery.size < 4 && !extLink
|
const canSelectImages = gallery.size < 4 && !extLink && !video
|
||||||
const hasMedia = gallery.size > 0 || Boolean(extLink)
|
const hasMedia = gallery.size > 0 || Boolean(extLink) || Boolean(video)
|
||||||
|
|
||||||
const onEmojiButtonPress = useCallback(() => {
|
const onEmojiButtonPress = useCallback(() => {
|
||||||
openPicker?.(textInput.current?.getCursorPosition())
|
openPicker?.(textInput.current?.getCursorPosition())
|
||||||
@@ -583,7 +589,8 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : null}
|
||||||
|
{video && <VideoPreview video={video} />}
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
<SuggestedLanguage text={richtext.text} />
|
<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]}>
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
|
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
|
||||||
|
<SelectVideoBtn
|
||||||
|
onSelectVideo={onSelectVideo}
|
||||||
|
disabled={!canSelectImages}
|
||||||
|
pending={videoPending}
|
||||||
|
setPending={setVideoPending}
|
||||||
|
/>
|
||||||
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
||||||
<SelectGifBtn
|
<SelectGifBtn
|
||||||
onClose={focusTextInput}
|
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>
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user