리뷰어 P1: 1프레임 lavfi 테스트는 통과해도 실제 영상의 픽셀 포맷/색공간/
해상도 조합에서 HW 인코더가 깨지는 경우가 흔하다 (NVENC yuv444 거부,
VAAPI 포맷 chain 까다로움 등). 기존 코드는 HW 가 실제 인코딩에서 실패하면
그대로 포기해 "원본도 60fps + 최대 FHD" 보장이 깨졌다.
수정:
- upscaleOriginalTo60Fps: 선택된 인코더가 실제 영상에서 실패하고 그게
libx264 가 아니면 libx264 ultrafast 로 한 번 더 시도. 둘 다 실패해야
진짜 실패로 보고 원본 유지. 인자 조립을 buildUpscaleArgs(profile) 로
추출해 같은 vfilter/onProgress 흐름을 재사용.
- applyTrimToVideo 재인코딩 경로: 동일 패턴. buildTrimEncArgs(profile)
로 추출해 HW 실패 시 libx264 재시도. 둘 다 실패해야 throw.
- detectH264Encoder 의 캐시는 그대로 둔다 (다른 잡은 더 일반적인 입력
일 수 있어 HW 가 동작할 여지를 남긴다).
검증 (fake ffmpeg + fake ffprobe, 3 시나리오, 정리 완료):
- HW 테스트 PASS, REAL FAIL → libx264 재시도로 변환본 생성 ✓
- HW 테스트 PASS, REAL PASS → HW 그대로 사용, 재시도 없음 ✓
- 모든 인코더 REAL FAIL → 원본 그대로, 변환 안 함 ✓
5/16 사용자 요구 "변환 너무 느려 20배는 빠르게 해줘" 대응.
libx264 -preset ultrafast 보다 실질적으로 20배 빠른 길은 GPU/전용 ASIC
인코더밖에 없으므로, 첫 사용 시점에 한 번 자동 탐지해서 캐시한다.
탐지 흐름:
1) ffmpeg `-encoders` 출력에서 h264_nvenc, h264_qsv, h264_videotoolbox,
h264_vaapi 후보를 추린다 (vaapi 는 /dev/dri/renderD128 도 필요).
2) 각 후보를 lavfi color 1프레임 인코드로 실측. 5초 타임아웃.
3) 첫 성공을 캐시; 모두 실패하면 libx264 ultrafast 폴백.
4) 우선순위: nvenc > qsv > videotoolbox > vaapi > libx264.
각 프로필은 (preInputArgs, vfilterSuffix, codecArgs) 세 묶음을 들고 있어
호출자는 자기 vfilter chain 끝과 `-i` 앞에 그대로 spread 만 하면 된다.
vaapi 의 `-vaapi_device .../renderD128` 와 `,format=nv12,hwupload` 가
이 모델로 깨끗하게 흡수된다.
연결 지점:
- upscaleOriginalTo60Fps: libx264 자리에 enc.codecArgs spread. vfilter 끝에
enc.vfilterSuffix 이어붙임. preInputArgs 를 -i 앞에 prepend.
- applyTrimToVideo 재인코드 경로: 동일 처리. baseArgs 를 처음부터 다시
쌓아 preInputArgs 가 ffmpeg 전역 옵션 위치(-ss/-to/-i 보다 앞)에 가게.
- stream-copy trim 경로는 그대로 둠 (인코더 안 씀).
검증 (fake ffmpeg + lavfi 1프레임 테스트):
- only libx264 listed → libx264 (폴백)
- nvenc+qsv listed, nvenc fails → qsv
- nvenc works → nvenc
- only vaapi works → vaapi (+ preInput/vfilter suffix 확인)
- nvenc+qsv+vaapi listed, all HW fail → libx264 (폴백)
User wants YouTube imports capped at FHD: below-FHD sources stay as-is,
above-FHD sources are forcefully downscaled to FHD preserving aspect.
Two layers:
- yt-dlp `-f bv*[height<=1080]+ba/b[height<=1080]/bv*+ba/b` strictly
picks ≤1080 formats when available; falls back to anything otherwise.
- ffmpeg post-process: probeVideoResolution checks output; if >FHD,
add scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease
to the existing 60fps vfilter chain (no extra encode pass — combined
into the one we already run). trunc(/2)*2 ensures even dims for libx264.
The post-process now triggers when either (fps<60) OR (res>FHD), so a
60fps 4K source that previously skipped re-encoding will now get
downscaled.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
세 가지 사용자 보고 처리:
1) 다운로드 바가 멈춰 있다가 한번에 50% 로 점프
- 원인: yt-dlp 는 파이썬 스크립트라 stdout 이 pipe 로 연결되면
block-buffered 가 되어 진행률 라인들이 4KB 버퍼에 모였다가 종료 직전에
쏟아짐. node 의 stdout 'data' 핸들러가 그 전엔 아무 것도 못 봄.
- 수정: spawn 의 env 에 PYTHONUNBUFFERED=1 추가. 라인 단위로 즉시 flush.
2) 변환을 20배 빠르게
- 원인: minterpolate(mci) 는 motion estimation 자체가 무거워 mci 기본값
이어도 영상 길이의 수 배 시간이 든다 (blend 도 5~10배가 한계).
- 수정: 다운로드 후 60fps 변환과 trim 재인코딩 양쪽에서 minterpolate 를
`fps=60` (단순 프레임 복제) 으로 교체. preset 도 `veryfast` → `ultrafast`.
실측상 영상 길이의 1/3 ~ 1배 시간 — mci 대비 수십 배 빠름.
- 시각적 부드러움은 30fps 와 동일하지만 컨테이너/타이밍은 60fps cfr 로 유지.
사용자가 알려준 "timing 만 60fps 로 바뀌고 실제 부드러움은 그대로" 경로.
3) "확인" 전 "가져오기" 재클릭 막기
- probe 클릭 시 ytProbeBtn 도 disabled.
- probe 실패 / 5분 경고 취소 / start 실패 시만 재활성화.
- 정상 흐름은 probe 성공 → 확인 클릭 → 다운로드 → redirect 라 재활성화
필요 없음. ytStartBtn 도 클릭 직후 disabled 로 중복 클릭 방지.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
다운로드 후 바가 한 번에 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>
Review P2: 변환 성공 후 `unlink(input) → rename(tmp)` 순서였는데, unlink 가
성공하고 rename 이 실패하면 원본이 사라진 채 결과물도 없는 상태가 됩니다.
순서를 뒤집어 `rename(tmp → outPath)` 이 먼저 성공한 뒤에만 기존 원본을
지우도록 바꿨습니다. rename 실패 시에는 tmp 만 정리하고 inputName 을 반환해
"실패해도 원본은 그대로" 의도와 일치하게 됩니다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review P1: yt-dlp 가 fps 1순위로 정렬해 가져와도, 영상 자체에 60fps
포맷이 없는 경우 original.* 이 30fps 그대로 저장되어 "원본도 60fps 로
받아줘" 요청과 어긋났습니다.
다운로드 완료 직후 후처리 단계를 추가:
- editor.ts 에 `upscaleOriginalTo60Fps(dir, inputName)` 노출
· ffprobe 로 source fps 측정
· ≥60fps 면 그대로 두고 inputName 반환
· <60fps 면 minterpolate(mci, aobmc, bidir) 로 60fps 까지 끌어올려
`original.mp4` 로 저장하고 기존 파일 제거
· ffmpeg/ffprobe 없거나 보간 실패하면 원본 그대로 유지 (다운로드 살림)
- youtube.ts `runJob` 마지막에 이 함수를 호출하고, 새 파일명이 돌아오면
meta.originalFile 도 업데이트. 후처리 중 진행률을 99% 로 표시하고
실패해도 다운로드 자체는 성공으로 마감.
이로써 편집기에서 다시 보간할 일도 없어집니다 (이미 60fps 원본).
편집 코드 쪽 보호 분기는 직접 업로드 경로용 안전망으로 그대로 둡니다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- youtube.ts: yt-dlp 에 `-S 'fps,res,br,ext'` 추가. 기본 정렬은 fps 를
가중치로 안 써서 60fps 가 있어도 30fps 를 잡아오는 일이 잦았는데,
이제 fps 1순위로 정렬해 가능한 한 부드러운 원본을 받는다. 60fps 가
아예 없는 영상은 자연스럽게 그 영상의 최고 fps 로 폴백.
- editor.ts: 편집본은 항상 60fps 이상이 되도록 보장.
· ffprobe 로 원본 fps 확인
· ≥60fps 이면 기존대로 stream copy 로 빠르게 trim
· <60fps 이면 minterpolate(mci, aobmc, bidir) 로 모션 보간해 60fps 로
재인코딩. mci 는 느리지만 단순 프레임 복제보다 모션이 훨씬 자연스러움.
· ffprobe 실패 시(확인 불가) 기존 동작 유지(stream copy → 재인코딩 폴백).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>