fix(youtube): split progress 0-50/50-99 and report ffmpeg conversion %
다운로드 후 바가 한 번에 99% 로 튄 뒤 멈춰 있던 문제 두 가지를 같이 고침.
1) 진행률 구간 분리
- 다운로드: 0 ~ 50% (yt-dlp 의 0-100% 를 절반으로 매핑)
- 60fps 변환: 50 ~ 99%
- yt-dlp 가 video+audio 두 스트림을 받으면 각 스트림이 0→100% 를
반복하므로 단조 증가만 인정 (downloadPctRaw = max(...)).
2) ffmpeg minterpolate 진행률 실시간 보고
- editor.ts 에 `probeVideoDuration` 추가, `runFfmpegWithProgress`
도입해 ffmpeg `-progress pipe:1` 의 out_time_us/ms 를 파싱.
- `upscaleOriginalTo60Fps` 에 `onProgress(pct)` 콜백 추가.
- youtube.ts 가 콜백을 받아 50~99% 로 매핑하고 job.message 를
"60fps 변환 NN%" 로 갱신. persistJob 은 2초 throttle.
3) mci 옵션 단순화로 속도 개선
- 기존: `mci:mc_mode=aobmc:me_mode=bidir` (가장 느린 조합)
- 현재: `mci` 기본값 (obmc + bilat). 화질 약간 양보, 속도 수 배 개선.
- 여전히 mci 자체가 무거워 5분 30fps 영상 변환에 수십 분 단위 시간이
걸릴 수 있음 — 이제 그 동안 진행률은 실시간으로 움직임.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -197,6 +197,10 @@ async function runJob(job: DownloadJob, bin: string): Promise<void> {
|
||||
'-o', path.join(dir, 'original.%(ext)s'),
|
||||
job.url
|
||||
]
|
||||
// 다운로드 단계는 전체 진행률의 0~50% 를 차지하고, 60fps 후처리가 50~99%.
|
||||
// (yt-dlp 가 video+audio 두 스트림을 받으면 각 스트림이 0→100 % 를 반복하므로
|
||||
// 단조 증가만 인정해 막대가 역행하지 않게 한다.)
|
||||
let downloadPctRaw = 0
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(bin, args)
|
||||
let outputFile: string | null = null
|
||||
@@ -206,8 +210,13 @@ async function runJob(job: DownloadJob, bin: string): Promise<void> {
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const m = /PROGRESS\s+([\d.]+)%/.exec(line)
|
||||
if (m) {
|
||||
job.progress = Math.min(99, Math.round(Number(m[1])))
|
||||
job.message = `다운로드 ${job.progress}%`
|
||||
const pct = Number(m[1])
|
||||
if (Number.isFinite(pct)) {
|
||||
downloadPctRaw = Math.max(downloadPctRaw, Math.min(100, pct))
|
||||
const mapped = Math.round(downloadPctRaw * 0.5) // 0..50
|
||||
if (mapped > job.progress) job.progress = mapped
|
||||
job.message = `다운로드 ${Math.round(downloadPctRaw)}%`
|
||||
}
|
||||
}
|
||||
const o = /^OUT\s+(.+)$/.exec(line.trim())
|
||||
if (o) outputFile = o[1].trim()
|
||||
@@ -237,14 +246,35 @@ async function runJob(job: DownloadJob, bin: string): Promise<void> {
|
||||
// 없는 영상이 있으니, 다운로드 후 원본을 ffprobe 로 확인해 <60fps 면
|
||||
// minterpolate 로 60fps 까지 끌어올린다. 실패해도 원본은 그대로 둠.
|
||||
try {
|
||||
job.progress = 99
|
||||
job.message = '60fps 변환 확인 중'
|
||||
// 50% 부터 시작해 50~99% 구간을 ffmpeg 진행률이 채우게 한다.
|
||||
job.progress = 50
|
||||
job.message = '60fps 변환 준비 중'
|
||||
await persistJob(job)
|
||||
const bumped = await upscaleOriginalTo60Fps(dir, finalName)
|
||||
// ffmpeg 진행률은 매우 자주 들어오므로 디스크 persist 는 throttle.
|
||||
let lastPersistAt = 0
|
||||
let lastBumpPct = 0
|
||||
const bumped = await upscaleOriginalTo60Fps(dir, finalName, (pct) => {
|
||||
if (pct <= lastBumpPct) return
|
||||
lastBumpPct = pct
|
||||
const mapped = 50 + Math.round((pct / 100) * 49) // 50..99
|
||||
if (mapped > job.progress) job.progress = mapped
|
||||
job.message = `60fps 변환 ${Math.round(pct)}%`
|
||||
const now = Date.now()
|
||||
if (now - lastPersistAt > 2000) {
|
||||
lastPersistAt = now
|
||||
void persistJob(job)
|
||||
}
|
||||
})
|
||||
if (bumped !== finalName) {
|
||||
job.message = '60fps 변환 완료'
|
||||
finalName = bumped
|
||||
} else {
|
||||
// upscaleOriginalTo60Fps 가 inputName 을 그대로 돌려준 경우는
|
||||
// (a) 이미 60fps 이상이거나 (b) ffmpeg 없거나 (c) 보간 실패.
|
||||
// 어느 경우든 후처리 단계는 끝났다고 보고 진행률만 99 까지 채운다.
|
||||
job.progress = 99
|
||||
}
|
||||
await persistJob(job)
|
||||
} catch (err) {
|
||||
// 후처리 실패는 다운로드 자체를 실패시키지 않는다. 원본 보존이 우선.
|
||||
console.error('[youtube] 60fps 후처리 실패:', err)
|
||||
|
||||
Reference in New Issue
Block a user