fix: 동시 영상 처리 시 공유 임시파일 레이스 제거 (고유 tmp 경로)
ensureThumbnail / upscaleOriginalTo60Fps / applyTrimToVideo 가 영상당 고정된 tmp 파일명(thumb.jpg.tmp.jpg, original.bump.tmp.mp4, edited.<ext>.tmp.<ext>) 을 써서, 같은 영상에 대한 요청이 동시에 들어오면 ffmpeg 프로세스들이 서로의 tmp 를 덮어쓰고 rename 이 경합해 일부 요청이 헛되이 실패했다. 증상: 같은 썸네일을 동시 10회 요청하면 6건만 성공하고 4건이 404. 원인: 호출마다 같은 tmp 경로 공유. 해결: randomUUID 로 호출별 고유 tmp 경로. 검증: 수정 후 동일 동시요청 10/10 성공, tmp 잔여물 없음. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { spawn, spawnSync } from 'node:child_process'
|
import { spawn, spawnSync } from 'node:child_process'
|
||||||
import { promises as fs, existsSync } from 'node:fs'
|
import { promises as fs, existsSync } from 'node:fs'
|
||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { getVideoInFolder, updateVideo, videoDiskDir, type VideoTrim } from './storeDb.js'
|
import { getVideoInFolder, updateVideo, videoDiskDir, type VideoTrim } from './storeDb.js'
|
||||||
import { binDir } from './paths.js'
|
import { binDir } from './paths.js'
|
||||||
@@ -335,7 +336,8 @@ export async function upscaleOriginalTo60Fps(
|
|||||||
// 재인코딩 결과는 항상 mp4 로 통일 (소스가 webm/mkv 여도).
|
// 재인코딩 결과는 항상 mp4 로 통일 (소스가 webm/mkv 여도).
|
||||||
const outName = 'original.mp4'
|
const outName = 'original.mp4'
|
||||||
const outPath = path.join(dir, outName)
|
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 조립:
|
// vfilter 조립:
|
||||||
// - fps=60 : motion interpolation 대신 단순 프레임 복제 (속도 우선).
|
// - fps=60 : motion interpolation 대신 단순 프레임 복제 (속도 우선).
|
||||||
@@ -422,7 +424,8 @@ export async function applyTrimToVideo(
|
|||||||
const ext = path.extname(meta.originalFile) || '.mp4'
|
const ext = path.extname(meta.originalFile) || '.mp4'
|
||||||
const outName = `edited${ext}`
|
const outName = `edited${ext}`
|
||||||
const outPath = path.join(dir, outName)
|
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 startSec = Math.max(0, Number(trim.startSec) || 0)
|
||||||
const endSec = trim.endSec == null ? null : Math.max(startSec, Number(trim.endSec))
|
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 {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const tmpPath = thumbPath + '.tmp.jpg'
|
// 동시 호출이 같은 tmp 파일을 덮어쓰지 않도록 호출마다 고유한 임시 경로를 쓴다.
|
||||||
|
// (두 명이 같은 영상을 동시에 열면 thumb 가 아직 없을 때 generation 이 겹쳐
|
||||||
|
// 서로의 tmp 를 덮어써 일부 요청이 헛되이 실패하던 레이스를 막는다.)
|
||||||
|
const tmpPath = `${thumbPath}.${randomUUID()}.tmp.jpg`
|
||||||
const vf = "scale='min(480,iw)':-2"
|
const vf = "scale='min(480,iw)':-2"
|
||||||
// -ss 1 (1초 지점). 영상이 1초보다 짧으면 프레임이 안 나와 실패하므로 0초로 재시도.
|
// -ss 1 (1초 지점). 영상이 1초보다 짧으면 프레임이 안 나와 실패하므로 0초로 재시도.
|
||||||
let ok = await runFfmpeg(bin, [
|
let ok = await runFfmpeg(bin, [
|
||||||
|
|||||||
Reference in New Issue
Block a user