diff --git a/src/editor.ts b/src/editor.ts index 2e4572b..c8626ea 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,5 +1,6 @@ import { spawn, spawnSync } from 'node:child_process' import { promises as fs, existsSync } from 'node:fs' +import { randomUUID } from 'node:crypto' import path from 'node:path' import { getVideoInFolder, updateVideo, videoDiskDir, type VideoTrim } from './storeDb.js' import { binDir } from './paths.js' @@ -335,7 +336,8 @@ export async function upscaleOriginalTo60Fps( // 재인코딩 결과는 항상 mp4 로 통일 (소스가 webm/mkv 여도). const outName = 'original.mp4' const outPath = path.join(dir, outName) - const tmpPath = path.join(dir, 'original.bump.tmp.mp4') + // 동시 호출이 같은 tmp 파일을 덮어쓰지 않도록 호출마다 고유한 임시 경로를 쓴다. + const tmpPath = path.join(dir, `original.bump.${randomUUID()}.tmp.mp4`) // vfilter 조립: // - fps=60 : motion interpolation 대신 단순 프레임 복제 (속도 우선). @@ -422,7 +424,8 @@ export async function applyTrimToVideo( const ext = path.extname(meta.originalFile) || '.mp4' const outName = `edited${ext}` const outPath = path.join(dir, outName) - const tmpPath = outPath + '.tmp' + ext + // 동시 호출이 같은 tmp 파일을 덮어쓰지 않도록 호출마다 고유한 임시 경로를 쓴다. + const tmpPath = `${outPath}.${randomUUID()}.tmp${ext}` const startSec = Math.max(0, Number(trim.startSec) || 0) const endSec = trim.endSec == null ? null : Math.max(startSec, Number(trim.endSec)) @@ -522,7 +525,10 @@ export async function ensureThumbnail(folderId: number, videoId: string): Promis } catch { return null } - const tmpPath = thumbPath + '.tmp.jpg' + // 동시 호출이 같은 tmp 파일을 덮어쓰지 않도록 호출마다 고유한 임시 경로를 쓴다. + // (두 명이 같은 영상을 동시에 열면 thumb 가 아직 없을 때 generation 이 겹쳐 + // 서로의 tmp 를 덮어써 일부 요청이 헛되이 실패하던 레이스를 막는다.) + const tmpPath = `${thumbPath}.${randomUUID()}.tmp.jpg` const vf = "scale='min(480,iw)':-2" // -ss 1 (1초 지점). 영상이 1초보다 짧으면 프레임이 안 나와 실패하므로 0초로 재시도. let ok = await runFfmpeg(bin, [