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 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-01-30 20:44:12 +02:00
parent bb5ba91b2f
commit 13390ff174
+54 -12
View File
@@ -31,21 +31,50 @@ export type RestoredVideo = {
altText: string altText: string
mimeType: string mimeType: string
localRefPath: string localRefPath: string
/** Whether the stored video is already compressed (can skip compression step) */
compressed: boolean
captions: Array<{lang: string; content: string}> captions: Array<{lang: string; content: string}>
} }
/** /**
* Parse mime type from video localRefPath. * Parse video metadata from localRefPath.
* Format: `video:${mimeType}:${nanoid()}` (new) or `video:${nanoid()}` (legacy) * 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(':') 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 // New format with compressed flag: video:mimeType:c|u:id.ext
if (parts.length >= 3 && parts[1].includes('/')) { if (parts.length >= 4 && parts[1].includes('/')) {
return parts[1] 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. * 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( async function serializeVideo(
videoState: VideoState, videoState: VideoState,
@@ -198,12 +229,21 @@ async function serializeVideo(
return undefined 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 mimeType = videoState.video.mimeType || 'video/mp4'
const ext = mimeToExt(mimeType) 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) localRefPaths.set(localRefPath, videoState.video.uri)
logger.debug('serializing video', {
localRefPath,
isReusing: !!videoState.localRefPath,
sourcePath: videoState.video.uri,
})
// Read caption file contents as text // Read caption file contents as text
const captions: AppBskyDraftDefs.DraftEmbedCaption[] = [] const captions: AppBskyDraftDefs.DraftEmbedCaption[] = []
for (const caption of videoState.captions) { for (const caption of videoState.captions) {
@@ -501,12 +541,13 @@ export async function draftToComposerPosts(
const vid = post.embedVideos[0] const vid = post.embedVideos[0]
const videoUri = loadedMedia.get(vid.localRef.path) const videoUri = loadedMedia.get(vid.localRef.path)
if (videoUri) { if (videoUri) {
const mimeType = parseVideoMimeType(vid.localRef.path) const {mimeType, compressed} = parseVideoLocalRef(vid.localRef.path)
logger.debug('found video to restore', { logger.debug('found video to restore', {
localRefPath: vid.localRef.path, localRefPath: vid.localRef.path,
videoUri, videoUri,
altText: vid.alt, altText: vid.alt,
mimeType, mimeType,
compressed,
captionCount: vid.captions?.length ?? 0, captionCount: vid.captions?.length ?? 0,
}) })
restoredVideos.set(index, { restoredVideos.set(index, {
@@ -514,6 +555,7 @@ export async function draftToComposerPosts(
altText: vid.alt || '', altText: vid.alt || '',
mimeType, mimeType,
localRefPath: vid.localRef.path, localRefPath: vid.localRef.path,
compressed,
captions: captions:
vid.captions?.map(c => ({lang: c.lang, content: c.content})) ?? vid.captions?.map(c => ({lang: c.lang, content: c.content})) ??
[], [],