Use the parent clip last frame and new-segment join for extensions, not the stitched file's first frame. Show this account's library storage in Settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 10:54:46 -05:00
co-authored by Cursor
parent 231c59a7a7
commit 0af3cddb14
16 changed files with 571 additions and 70 deletions
+91 -6
View File
@@ -5,7 +5,7 @@ import { spawn } from 'node:child_process'
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto'
import { promisify } from 'node:util'
import type { H3Event } from 'h3'
import { extractFirstFrame, extractLastFrame, remuxFaststart } from '~/server/utils/ffmpeg'
import { extractFirstFrame, extractFrameAt, extractLastFrame, probeDuration, remuxFaststart } from '~/server/utils/ffmpeg'
import { normalizePermanenceRefs, type PermanenceRef } from '~/utils/globalLocks'
const scryptAsync = promisify(scrypt)
@@ -122,7 +122,7 @@ interface Catalog {
const writeChains = new Map<string, Promise<unknown>>()
function libraryRoot() {
export function libraryRoot() {
const config = useRuntimeConfig()
return (config.libraryDir || process.env.LIBRARY_DIR || '/data/library').replace(/\/$/, '')
}
@@ -137,7 +137,7 @@ export function libraryOwnerKey(event: H3Event) {
return createHash('sha256').update(raw).digest('hex').slice(0, 24)
}
function ownerRoot(owner: string) {
export function ownerRoot(owner: string) {
return join(libraryRoot(), 'users', owner)
}
@@ -612,11 +612,69 @@ export async function ensureScaledThumb(source: string, dest: string) {
return build
}
type FrameSourceMeta = {
videoMtime: number
videoSize: number
mode: 'file-start' | 'segment-start' | 'last'
seek?: number
}
function frameMetaPath(dest: string) {
return `${dest}.src.json`
}
function readFrameMeta(dest: string): FrameSourceMeta | null {
try {
const raw = JSON.parse(readFileSync(frameMetaPath(dest), 'utf8')) as FrameSourceMeta
if (!raw || typeof raw.videoMtime !== 'number' || typeof raw.videoSize !== 'number') return null
return raw
} catch {
return null
}
}
function writeFrameMeta(dest: string, video: string, mode: FrameSourceMeta['mode'], seek?: number) {
try {
const info = statSync(video)
const meta: FrameSourceMeta = { videoMtime: info.mtimeMs, videoSize: info.size, mode }
if (typeof seek === 'number') meta.seek = seek
writeFileSync(frameMetaPath(dest), JSON.stringify(meta))
} catch { /* thumbs still work without the sidecar */ }
}
function frameCacheFresh(video: string, dest: string, mode: FrameSourceMeta['mode']) {
if (!existsSync(dest) || statSync(dest).size < 64) return false
try {
const info = statSync(video)
const meta = readFrameMeta(dest)
if (meta) {
return meta.mode === mode && meta.videoMtime === info.mtimeMs && meta.videoSize === info.size
}
return statSync(dest).mtimeMs >= info.mtimeMs && mode === 'file-start'
} catch {
return false
}
}
async function parentVideoDuration(owner: string, parentId?: string) {
if (!parentId) return 0
const video = clipVideoPath(owner, parentId)
if (!existsSync(video)) return 0
try {
return await probeDuration(video)
} catch {
return 0
}
}
export async function ensureClipLastFrame(owner: string, id: string) {
const video = clipVideoPath(owner, id)
const dest = clipLastFramePath(owner, id)
if (!existsSync(video)) return ''
if (!existsSync(dest)) await extractLastFrame(video, dest)
if (!frameCacheFresh(video, dest, 'last')) {
await extractLastFrame(video, dest)
writeFrameMeta(dest, video, 'last')
}
return existsSync(dest) ? dest : ''
}
@@ -624,8 +682,29 @@ export async function ensureClipFirstFrame(owner: string, id: string) {
const video = clipVideoPath(owner, id)
const dest = clipFirstFramePath(owner, id)
if (!existsSync(video)) return ''
if (!existsSync(dest)) await extractFirstFrame(video, dest)
return existsSync(dest) ? dest : ''
const catalog = readCatalog(owner)
const clip = catalog.clips.find(item => item.id === id)
const parentDur = await parentVideoDuration(owner, clip?.parentClipId)
let fileDur = 0
try {
fileDur = await probeDuration(video)
} catch { /* treat as a single segment */ }
const stored = Number(clip?.duration) || 0
const stitched = (parentDur > 0.5 && fileDur > parentDur + 0.4)
|| (stored > 0.5 && fileDur > stored + 0.8)
const seek = parentDur > 0.5 && fileDur > parentDur + 0.4
? parentDur
: (stitched && stored > 0.5 ? Math.max(0, fileDur - stored) : 0)
const mode: FrameSourceMeta['mode'] = stitched ? 'segment-start' : 'file-start'
if (frameCacheFresh(video, dest, mode)) return dest
if (stitched) {
await extractFrameAt(video, dest, seek)
writeFrameMeta(dest, video, 'segment-start', seek)
return dest
}
await extractFirstFrame(video, dest)
writeFrameMeta(dest, video, 'file-start', 0)
return dest
}
export function extendTempDir(owner: string, jobId: string) {
@@ -1313,6 +1392,7 @@ export async function saveClip(params: {
sound?: boolean
globalLocks?: string
permanenceRefs?: PermanenceRef[]
segmentFirstFrame?: Buffer
}) {
const catalog = readCatalog(params.ownerKey)
const folder = catalog.folders.find(item => item.id === params.folderId) || catalog.folders[0]
@@ -1349,6 +1429,11 @@ export async function saveClip(params: {
const videoPath = clipVideoPath(params.ownerKey, clip.id)
writeFileSync(videoPath, params.video)
await remuxFaststart(videoPath)
if (params.segmentFirstFrame?.length && params.segmentFirstFrame.length >= 64) {
const firstPath = clipFirstFramePath(params.ownerKey, clip.id)
writeFileSync(firstPath, params.segmentFirstFrame)
writeFrameMeta(firstPath, videoPath, 'segment-start')
}
if (typeof params.duration === 'number' && params.duration > 0) {
clip.duration = params.duration
} else {