diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index e077d21af7..e42dce8cd7 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -42,6 +42,7 @@ import Animated, { ZoomOut, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import * as FileSystem from 'expo-file-system' import {type ImagePickerAsset} from 'expo-image-picker' import { AppBskyUnspeccedDefs, @@ -320,7 +321,7 @@ export const ComposePost = ({ onInitVideo() }, [onInitVideo]) - const clearVideo = React.useCallback( + const clearVideo = useCallback( (postId: string) => { composerDispatch({ type: 'update_post', @@ -333,7 +334,7 @@ export const ComposePost = ({ [composerDispatch], ) - const restoreVideo = React.useCallback( + const restoreVideo = useCallback( async (postId: string, videoInfo: RestoredVideo) => { try { logger.debug('restoring video from draft', { @@ -354,15 +355,25 @@ export const ComposePost = ({ }) asset = await getVideoMetadata(file) } else { - // Native: Get video metadata using react-native-compressor - const {getVideoMetaData} = require('react-native-compressor') - const metadata = await getVideoMetaData(videoInfo.uri) - asset = { - uri: videoInfo.uri, - mimeType: videoInfo.mimeType, - width: metadata.width, - height: metadata.height, + let uri = videoInfo.uri + if (IS_ANDROID) { + // Android: expo-file-system double-encodes filenames with special chars. + // The file exists, but react-native-compressor's MediaMetadataRetriever + // can't handle the double-encoded URI. Copy to a temp file with a simple name. + const sourceFile = new FileSystem.File(videoInfo.uri) + const tempFileName = `draft-video-${Date.now()}.${mimeToExt(videoInfo.mimeType)}` + const tempFile = new FileSystem.File( + FileSystem.Paths.cache, + tempFileName, + ) + sourceFile.copy(tempFile) + logger.debug('restoreVideo: copied to temp file', { + source: videoInfo.uri, + temp: tempFile.uri, + }) + uri = tempFile.uri } + asset = await getVideoMetadata(uri) } // Start video processing using existing flow diff --git a/src/view/com/composer/videos/pickVideo.ts b/src/view/com/composer/videos/pickVideo.ts index a55b69c1d8..f1650258ff 100644 --- a/src/view/com/composer/videos/pickVideo.ts +++ b/src/view/com/composer/videos/pickVideo.ts @@ -1,3 +1,4 @@ +import {getVideoMetaData} from 'react-native-compressor' import { type ImagePickerAsset, launchImageLibraryAsync, @@ -5,6 +6,7 @@ import { } from 'expo-image-picker' import {VIDEO_MAX_DURATION_MS} from '#/lib/constants' +import {extToMime} from '#/lib/media/video/util' export async function pickVideo() { return await launchImageLibraryAsync({ @@ -18,6 +20,24 @@ export async function pickVideo() { }) } -export const getVideoMetadata = (_file: File): Promise => { - throw new Error('getVideoMetadata is web only') +/** + * Gets video metadata from a file or uri, depending on the platform + * + * @param file File on web, uri on native + */ +export async function getVideoMetadata( + file: File | string, +): Promise { + if (typeof file !== 'string') + throw new Error( + 'getVideoMetadata was passed a File, when on native it should be a uri', + ) + const metadata = await getVideoMetaData(file) + return { + uri: file, + mimeType: extToMime(metadata.extension), + width: metadata.width, + height: metadata.height, + duration: metadata.duration, + } } diff --git a/src/view/com/composer/videos/pickVideo.web.ts b/src/view/com/composer/videos/pickVideo.web.ts index c358727ef7..aa53702a24 100644 --- a/src/view/com/composer/videos/pickVideo.web.ts +++ b/src/view/com/composer/videos/pickVideo.web.ts @@ -39,7 +39,13 @@ export async function pickVideo(): Promise { // lets us use the ImagePickerAsset type, which the rest of the code expects. // We should unwind this and just pass the ArrayBuffer/objectUrl through the system // instead of a string -sfn -export const getVideoMetadata = (file: File): Promise => { +export function getVideoMetadata( + file: File | string, +): Promise { + if (typeof file === 'string') + throw new Error( + 'getVideoMetadata was passed a uri, when on web it should be a File', + ) return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => {