get video restoration working on native

This commit is contained in:
Samuel Newman
2026-01-28 19:13:49 +02:00
parent b36a2e33d3
commit 67ecda2542
3 changed files with 50 additions and 13 deletions
+21 -10
View File
@@ -42,6 +42,7 @@ import Animated, {
ZoomOut, ZoomOut,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import * as FileSystem from 'expo-file-system'
import {type ImagePickerAsset} from 'expo-image-picker' import {type ImagePickerAsset} from 'expo-image-picker'
import { import {
AppBskyUnspeccedDefs, AppBskyUnspeccedDefs,
@@ -320,7 +321,7 @@ export const ComposePost = ({
onInitVideo() onInitVideo()
}, [onInitVideo]) }, [onInitVideo])
const clearVideo = React.useCallback( const clearVideo = useCallback(
(postId: string) => { (postId: string) => {
composerDispatch({ composerDispatch({
type: 'update_post', type: 'update_post',
@@ -333,7 +334,7 @@ export const ComposePost = ({
[composerDispatch], [composerDispatch],
) )
const restoreVideo = React.useCallback( const restoreVideo = useCallback(
async (postId: string, videoInfo: RestoredVideo) => { async (postId: string, videoInfo: RestoredVideo) => {
try { try {
logger.debug('restoring video from draft', { logger.debug('restoring video from draft', {
@@ -354,15 +355,25 @@ export const ComposePost = ({
}) })
asset = await getVideoMetadata(file) asset = await getVideoMetadata(file)
} else { } else {
// Native: Get video metadata using react-native-compressor let uri = videoInfo.uri
const {getVideoMetaData} = require('react-native-compressor') if (IS_ANDROID) {
const metadata = await getVideoMetaData(videoInfo.uri) // Android: expo-file-system double-encodes filenames with special chars.
asset = { // The file exists, but react-native-compressor's MediaMetadataRetriever
uri: videoInfo.uri, // can't handle the double-encoded URI. Copy to a temp file with a simple name.
mimeType: videoInfo.mimeType, const sourceFile = new FileSystem.File(videoInfo.uri)
width: metadata.width, const tempFileName = `draft-video-${Date.now()}.${mimeToExt(videoInfo.mimeType)}`
height: metadata.height, 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 // Start video processing using existing flow
+22 -2
View File
@@ -1,3 +1,4 @@
import {getVideoMetaData} from 'react-native-compressor'
import { import {
type ImagePickerAsset, type ImagePickerAsset,
launchImageLibraryAsync, launchImageLibraryAsync,
@@ -5,6 +6,7 @@ import {
} from 'expo-image-picker' } from 'expo-image-picker'
import {VIDEO_MAX_DURATION_MS} from '#/lib/constants' import {VIDEO_MAX_DURATION_MS} from '#/lib/constants'
import {extToMime} from '#/lib/media/video/util'
export async function pickVideo() { export async function pickVideo() {
return await launchImageLibraryAsync({ return await launchImageLibraryAsync({
@@ -18,6 +20,24 @@ export async function pickVideo() {
}) })
} }
export const getVideoMetadata = (_file: File): Promise<ImagePickerAsset> => { /**
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<ImagePickerAsset> {
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,
}
} }
@@ -39,7 +39,13 @@ export async function pickVideo(): Promise<ImagePickerResult> {
// lets us use the ImagePickerAsset type, which the rest of the code expects. // 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 // We should unwind this and just pass the ArrayBuffer/objectUrl through the system
// instead of a string -sfn // instead of a string -sfn
export const getVideoMetadata = (file: File): Promise<ImagePickerAsset> => { export function getVideoMetadata(
file: File | string,
): Promise<ImagePickerAsset> {
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) => { return new Promise((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = () => { reader.onload = () => {