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:
+91
-6
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user