From 13390ff174db34583e2186f097df40b648eb31e3 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 30 Jan 2026 20:44:12 +0200 Subject: [PATCH] feat(drafts): reuse localRefPath and encode compressed flag for videos Update video serialization to: - Reuse existing localRefPath when editing a draft (avoid creating new files) - Encode compressed flag in localRefPath: video:mimeType:c:id.ext Add parseVideoLocalRef() to extract mimeType and compressed flag from localRefPath, with backwards compatibility for legacy formats. This enables skipping re-compression when restoring drafts that already have a compressed video stored. Co-Authored-By: Claude Opus 4.5 --- src/view/com/composer/drafts/state/api.ts | 66 ++++++++++++++++++----- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index ffcdb2b5db..ba6423ff27 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -31,21 +31,50 @@ export type RestoredVideo = { altText: string mimeType: string localRefPath: string + /** Whether the stored video is already compressed (can skip compression step) */ + compressed: boolean captions: Array<{lang: string; content: string}> } /** - * Parse mime type from video localRefPath. - * Format: `video:${mimeType}:${nanoid()}` (new) or `video:${nanoid()}` (legacy) + * Parse video metadata from localRefPath. + * Format: `video:${mimeType}:c:${nanoid()}.ext` (compressed, new) + * or `video:${mimeType}:u:${nanoid()}.ext` (uncompressed, new) + * or `video:${mimeType}:${nanoid()}.ext` (legacy format 2) + * or `video:${nanoid()}` (legacy format 1) + * + * Returns mimeType and whether the video was stored compressed. */ -function parseVideoMimeType(localRefPath: string): string { +export function parseVideoLocalRef(localRefPath: string): { + mimeType: string + compressed: boolean +} { const parts = localRefPath.split(':') - // New format: video:video/mp4:abc123 -> parts[1] is mime type - // Legacy format: video:abc123 -> no mime type, default to video/mp4 - if (parts.length >= 3 && parts[1].includes('/')) { - return parts[1] + + // New format with compressed flag: video:mimeType:c|u:id.ext + if (parts.length >= 4 && parts[1].includes('/')) { + const flag = parts[2] + if (flag === 'c' || flag === 'u') { + return { + mimeType: parts[1], + compressed: flag === 'c', + } + } + } + + // Legacy format 2: video:mimeType:id.ext (no compressed flag) + if (parts.length >= 3 && parts[1].includes('/')) { + return { + mimeType: parts[1], + compressed: false, // Assume uncompressed for backwards compat + } + } + + // Legacy format 1: video:id (no mime type, no compressed flag) + return { + mimeType: 'video/mp4', + compressed: false, } - return 'video/mp4' // Default for legacy drafts } /** @@ -187,7 +216,9 @@ function serializeImages( /** * Serialize video to server format with localRef path. - * The localRef path encodes the mime type: `video:${mimeType}:${nanoid()}` + * The localRef path encodes the mime type and compressed flag: + * `video:${mimeType}:c:${nanoid()}.ext` (compressed video stored) + * Reuses existing localRefPath if present (when editing a draft). */ async function serializeVideo( videoState: VideoState, @@ -198,12 +229,21 @@ async function serializeVideo( return undefined } - // Encode mime type in the path for restoration + // Encode mime type and compressed flag in the path for restoration + // Reuse existing localRefPath if present (editing draft), otherwise generate new const mimeType = videoState.video.mimeType || 'video/mp4' const ext = mimeToExt(mimeType) - const localRefPath = `video:${mimeType}:${nanoid()}.${ext}` + // Always mark as compressed since we only save after compression + const localRefPath = + videoState.localRefPath || `video:${mimeType}:c:${nanoid()}.${ext}` localRefPaths.set(localRefPath, videoState.video.uri) + logger.debug('serializing video', { + localRefPath, + isReusing: !!videoState.localRefPath, + sourcePath: videoState.video.uri, + }) + // Read caption file contents as text const captions: AppBskyDraftDefs.DraftEmbedCaption[] = [] for (const caption of videoState.captions) { @@ -501,12 +541,13 @@ export async function draftToComposerPosts( const vid = post.embedVideos[0] const videoUri = loadedMedia.get(vid.localRef.path) if (videoUri) { - const mimeType = parseVideoMimeType(vid.localRef.path) + const {mimeType, compressed} = parseVideoLocalRef(vid.localRef.path) logger.debug('found video to restore', { localRefPath: vid.localRef.path, videoUri, altText: vid.alt, mimeType, + compressed, captionCount: vid.captions?.length ?? 0, }) restoredVideos.set(index, { @@ -514,6 +555,7 @@ export async function draftToComposerPosts( altText: vid.alt || '', mimeType, localRefPath: vid.localRef.path, + compressed, captions: vid.captions?.map(c => ({lang: c.lang, content: c.content})) ?? [],