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
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})) ??
[],