diff --git a/app.config.js b/app.config.js
index 9616805231..7826b3f3a2 100644
--- a/app.config.js
+++ b/app.config.js
@@ -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',
diff --git a/assets/icons/videoClip_stroke2_corner0_rounded.svg b/assets/icons/videoClip_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..fd4c08d478
--- /dev/null
+++ b/assets/icons/videoClip_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/icons/VideoClip.tsx b/src/components/icons/VideoClip.tsx
new file mode 100644
index 0000000000..c2c13c4913
--- /dev/null
+++ b/src/components/icons/VideoClip.tsx
@@ -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',
+})
diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts
new file mode 100644
index 0000000000..e9b306d2b5
--- /dev/null
+++ b/src/lib/media/video/compress.ts
@@ -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,
+ }
+}
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 80bce5351c..820c20f087 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -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(
initQuote,
)
+ const {video, onSelectVideo, setVideoPending, videoPending} = useVideoState({
+ setError,
+ })
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
const [extGif, setExtGif] = useState()
const [labels, setLabels] = useState([])
@@ -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({
setQuote(undefined)} />
)}
- ) : undefined}
+ ) : null}
+ {video && }
@@ -602,6 +609,12 @@ export const ComposePost = observer(function ComposePost({
]}>
+
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 (
+ <>
+
+ >
+ )
+}
diff --git a/src/view/com/composer/videos/VideoPreview.tsx b/src/view/com/composer/videos/VideoPreview.tsx
new file mode 100644
index 0000000000..1adf0750b1
--- /dev/null
+++ b/src/view/com/composer/videos/VideoPreview.tsx
@@ -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 {JSON.stringify(video, null, 2)}
+}
diff --git a/src/view/com/composer/videos/state.ts b/src/view/com/composer/videos/state.ts
new file mode 100644
index 0000000000..2298d3d59e
--- /dev/null
+++ b/src/view/com/composer/videos/state.ts
@@ -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,
+ }
+}