feat(drafts): skip re-compression when restoring pre-compressed videos
Update restoreVideo() to: - Use restore_video_to_post action to avoid marking composer dirty - Check compressed flag from stored video metadata - If video is pre-compressed, skip compression and go straight to upload - Pass localRefPath through to preserve video identity This prevents unnecessary re-compression when restoring drafts with videos that were already compressed before saving. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,7 @@ import {
|
|||||||
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
|
import {type CompressedVideo} from '#/lib/media/video/types'
|
||||||
import {mimeToExt} from '#/lib/media/video/util'
|
import {mimeToExt} from '#/lib/media/video/util'
|
||||||
import {useCallOnce} from '#/lib/once'
|
import {useCallOnce} from '#/lib/once'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
@@ -164,6 +165,7 @@ import {
|
|||||||
NO_VIDEO,
|
NO_VIDEO,
|
||||||
type NoVideoState,
|
type NoVideoState,
|
||||||
processVideo,
|
processVideo,
|
||||||
|
uploadPrecompressedVideo,
|
||||||
type VideoState,
|
type VideoState,
|
||||||
} from './state/video'
|
} from './state/video'
|
||||||
import {type TextInputRef} from './text-input/TextInput.types'
|
import {type TextInputRef} from './text-input/TextInput.types'
|
||||||
@@ -362,10 +364,14 @@ export const ComposePost = ({
|
|||||||
postId,
|
postId,
|
||||||
videoUri: videoInfo.uri,
|
videoUri: videoInfo.uri,
|
||||||
altText: videoInfo.altText,
|
altText: videoInfo.altText,
|
||||||
|
compressed: videoInfo.compressed,
|
||||||
captionCount: videoInfo.captions.length,
|
captionCount: videoInfo.captions.length,
|
||||||
})
|
})
|
||||||
|
|
||||||
let asset: ImagePickerAsset
|
let asset: ImagePickerAsset
|
||||||
|
let videoUri = videoInfo.uri
|
||||||
|
let videoSize = 0
|
||||||
|
let videoBytes: ArrayBuffer | undefined
|
||||||
|
|
||||||
if (IS_WEB) {
|
if (IS_WEB) {
|
||||||
// Web: Convert blob URL to a File, then get video metadata (returns data URL)
|
// Web: Convert blob URL to a File, then get video metadata (returns data URL)
|
||||||
@@ -375,8 +381,11 @@ export const ComposePost = ({
|
|||||||
type: videoInfo.mimeType,
|
type: videoInfo.mimeType,
|
||||||
})
|
})
|
||||||
asset = await getVideoMetadata(file)
|
asset = await getVideoMetadata(file)
|
||||||
|
// For web, the data URL is used directly
|
||||||
|
videoUri = asset.uri
|
||||||
|
videoSize = blob.size
|
||||||
|
videoBytes = await blob.arrayBuffer()
|
||||||
} else {
|
} else {
|
||||||
let uri = videoInfo.uri
|
|
||||||
if (IS_ANDROID) {
|
if (IS_ANDROID) {
|
||||||
// Android: expo-file-system double-encodes filenames with special chars.
|
// Android: expo-file-system double-encodes filenames with special chars.
|
||||||
// The file exists, but react-native-compressor's MediaMetadataRetriever
|
// The file exists, but react-native-compressor's MediaMetadataRetriever
|
||||||
@@ -392,79 +401,190 @@ export const ComposePost = ({
|
|||||||
source: videoInfo.uri,
|
source: videoInfo.uri,
|
||||||
temp: tempFile.uri,
|
temp: tempFile.uri,
|
||||||
})
|
})
|
||||||
uri = tempFile.uri
|
videoUri = tempFile.uri
|
||||||
|
videoSize = tempFile.size ?? 0
|
||||||
|
} else {
|
||||||
|
// iOS
|
||||||
|
const sourceFile = new FileSystem.File(videoInfo.uri)
|
||||||
|
videoSize = sourceFile.size ?? 0
|
||||||
}
|
}
|
||||||
asset = await getVideoMetadata(uri)
|
asset = await getVideoMetadata(videoUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start video processing using existing flow
|
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
composerDispatch({
|
|
||||||
type: 'update_post',
|
|
||||||
postId,
|
|
||||||
postAction: {
|
|
||||||
type: 'embed_add_video',
|
|
||||||
asset,
|
|
||||||
abortController,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Restore alt text immediately
|
if (videoInfo.compressed) {
|
||||||
if (videoInfo.altText) {
|
// Video is already compressed - skip compression step
|
||||||
|
logger.debug('restoreVideo: using pre-compressed video', {
|
||||||
|
postId,
|
||||||
|
localRefPath: videoInfo.localRefPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create CompressedVideo object from stored data
|
||||||
|
const compressedVideo: CompressedVideo = {
|
||||||
|
uri: videoUri,
|
||||||
|
mimeType: videoInfo.mimeType,
|
||||||
|
size: videoSize,
|
||||||
|
bytes: videoBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add video to post (at compressing status initially)
|
||||||
composerDispatch({
|
composerDispatch({
|
||||||
type: 'update_post',
|
type: 'restore_video_to_post',
|
||||||
|
postId,
|
||||||
|
postAction: {
|
||||||
|
type: 'embed_add_video',
|
||||||
|
asset,
|
||||||
|
abortController,
|
||||||
|
localRefPath: videoInfo.localRefPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Manually update to uploading state with the compressed video
|
||||||
|
composerDispatch({
|
||||||
|
type: 'restore_video_to_post',
|
||||||
postId,
|
postId,
|
||||||
postAction: {
|
postAction: {
|
||||||
type: 'embed_update_video',
|
type: 'embed_update_video',
|
||||||
videoAction: {
|
videoAction: {
|
||||||
type: 'update_alt_text',
|
type: 'compressing_to_uploading',
|
||||||
altText: videoInfo.altText,
|
video: compressedVideo,
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Restore captions (web only - captions use File objects)
|
// Restore alt text
|
||||||
if (IS_WEB && videoInfo.captions.length > 0) {
|
if (videoInfo.altText) {
|
||||||
const captionTracks = videoInfo.captions.map(c => ({
|
|
||||||
lang: c.lang,
|
|
||||||
file: new File([c.content], `caption-${c.lang}.vtt`, {
|
|
||||||
type: 'text/vtt',
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
composerDispatch({
|
|
||||||
type: 'update_post',
|
|
||||||
postId,
|
|
||||||
postAction: {
|
|
||||||
type: 'embed_update_video',
|
|
||||||
videoAction: {
|
|
||||||
type: 'update_captions',
|
|
||||||
updater: () => captionTracks,
|
|
||||||
signal: abortController.signal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start video compression and upload
|
|
||||||
processVideo(
|
|
||||||
asset,
|
|
||||||
videoAction => {
|
|
||||||
composerDispatch({
|
composerDispatch({
|
||||||
type: 'update_post',
|
type: 'restore_video_to_post',
|
||||||
postId,
|
postId,
|
||||||
postAction: {
|
postAction: {
|
||||||
type: 'embed_update_video',
|
type: 'embed_update_video',
|
||||||
videoAction,
|
videoAction: {
|
||||||
|
type: 'update_alt_text',
|
||||||
|
altText: videoInfo.altText,
|
||||||
|
signal: abortController.signal,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
agent,
|
|
||||||
currentDid,
|
// Restore captions (web only)
|
||||||
abortController.signal,
|
if (IS_WEB && videoInfo.captions.length > 0) {
|
||||||
_,
|
const captionTracks = videoInfo.captions.map(c => ({
|
||||||
)
|
lang: c.lang,
|
||||||
|
file: new File([c.content], `caption-${c.lang}.vtt`, {
|
||||||
|
type: 'text/vtt',
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
composerDispatch({
|
||||||
|
type: 'restore_video_to_post',
|
||||||
|
postId,
|
||||||
|
postAction: {
|
||||||
|
type: 'embed_update_video',
|
||||||
|
videoAction: {
|
||||||
|
type: 'update_captions',
|
||||||
|
updater: () => captionTracks,
|
||||||
|
signal: abortController.signal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start upload (skip compression)
|
||||||
|
uploadPrecompressedVideo(
|
||||||
|
compressedVideo,
|
||||||
|
videoAction => {
|
||||||
|
composerDispatch({
|
||||||
|
type: 'restore_video_to_post',
|
||||||
|
postId,
|
||||||
|
postAction: {
|
||||||
|
type: 'embed_update_video',
|
||||||
|
videoAction,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
agent,
|
||||||
|
currentDid,
|
||||||
|
abortController.signal,
|
||||||
|
_,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Video not compressed yet - start from beginning
|
||||||
|
logger.debug('restoreVideo: starting full compression', {
|
||||||
|
postId,
|
||||||
|
localRefPath: videoInfo.localRefPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
composerDispatch({
|
||||||
|
type: 'restore_video_to_post',
|
||||||
|
postId,
|
||||||
|
postAction: {
|
||||||
|
type: 'embed_add_video',
|
||||||
|
asset,
|
||||||
|
abortController,
|
||||||
|
localRefPath: videoInfo.localRefPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Restore alt text immediately
|
||||||
|
if (videoInfo.altText) {
|
||||||
|
composerDispatch({
|
||||||
|
type: 'restore_video_to_post',
|
||||||
|
postId,
|
||||||
|
postAction: {
|
||||||
|
type: 'embed_update_video',
|
||||||
|
videoAction: {
|
||||||
|
type: 'update_alt_text',
|
||||||
|
altText: videoInfo.altText,
|
||||||
|
signal: abortController.signal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore captions (web only - captions use File objects)
|
||||||
|
if (IS_WEB && videoInfo.captions.length > 0) {
|
||||||
|
const captionTracks = videoInfo.captions.map(c => ({
|
||||||
|
lang: c.lang,
|
||||||
|
file: new File([c.content], `caption-${c.lang}.vtt`, {
|
||||||
|
type: 'text/vtt',
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
composerDispatch({
|
||||||
|
type: 'restore_video_to_post',
|
||||||
|
postId,
|
||||||
|
postAction: {
|
||||||
|
type: 'embed_update_video',
|
||||||
|
videoAction: {
|
||||||
|
type: 'update_captions',
|
||||||
|
updater: () => captionTracks,
|
||||||
|
signal: abortController.signal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start video compression and upload
|
||||||
|
processVideo(
|
||||||
|
asset,
|
||||||
|
videoAction => {
|
||||||
|
composerDispatch({
|
||||||
|
type: 'restore_video_to_post',
|
||||||
|
postId,
|
||||||
|
postAction: {
|
||||||
|
type: 'embed_update_video',
|
||||||
|
videoAction,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
agent,
|
||||||
|
currentDid,
|
||||||
|
abortController.signal,
|
||||||
|
_,
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to restore video from draft', {
|
logger.error('Failed to restore video from draft', {
|
||||||
postId,
|
postId,
|
||||||
|
|||||||
Reference in New Issue
Block a user