diff --git a/server/utils/ffmpeg.ts b/server/utils/ffmpeg.ts index 4124b64..f18320b 100644 --- a/server/utils/ffmpeg.ts +++ b/server/utils/ffmpeg.ts @@ -70,29 +70,42 @@ export async function extractLastFrame(inputPath: string, outputPath: string) { throw new Error('Source video is missing') } - const attempts: string[][] = [] - try { - const duration = await probeDuration(inputPath) - const seek = Math.max(0, duration - 0.08) - attempts.push(['-y', '-ss', seek.toFixed(3), '-i', inputPath, '-frames:v', '1', '-an', '-q:v', '1', outputPath]) - } catch { - // Truncated files often have no duration index; fall through to decode attempts. - } - attempts.push( - ['-y', '-sseof', '-0.04', '-i', inputPath, '-update', '1', '-frames:v', '1', '-an', '-q:v', '1', outputPath], - ['-y', '-i', inputPath, '-update', '1', '-an', '-q:v', '1', outputPath] - ) + const maxTotalTimeMs = 5000 + const start = Date.now() + let delayMs = 100 - for (const args of attempts) { + while (Date.now() - start < maxTotalTimeMs) { + const attempts: string[][] = [] try { - await runFfmpeg(args, 60000) - if (existsSync(outputPath) && statSync(outputPath).size >= 64) return + const duration = await probeDuration(inputPath) + const seek = Math.max(0, duration - 0.08) + attempts.push(['-y', '-ss', seek.toFixed(3), '-i', inputPath, '-frames:v', '1', '-an', '-q:v', '1', outputPath]) } catch { - // Try the next seek strategy. + // Truncated files often have no duration index; fall through to decode attempts. } + attempts.push( + ['-y', '-sseof', '-0.04', '-i', inputPath, '-update', '1', '-frames:v', '1', '-an', '-q:v', '1', outputPath], + ['-y', '-i', inputPath, '-update', '1', '-an', '-q:v', '1', outputPath] + ) + + for (const args of attempts) { + try { + await runFfmpeg(args, 60000) + if (existsSync(outputPath) && statSync(outputPath).size >= 64) return + } catch { + // Try next strategy + } + } + + const elapsed = Date.now() - start + if (elapsed >= maxTotalTimeMs) break + + const wait = Math.min(delayMs, maxTotalTimeMs - elapsed) + await new Promise(resolve => setTimeout(resolve, wait)) + delayMs = Math.min(delayMs * 2, maxTotalTimeMs - (Date.now() - start)) } - throw new Error('Could not extract the last frame. The video looks incomplete — interrupting Comfy mid-encode usually causes this. Earlier saved parts are still in the library.') + throw new Error('Could not extract the last frame within the timeout. The video may still be encoding or is incomplete.') } export async function extractFirstFrame(inputPath: string, outputPath: string) {