FFmpeg 실전 가이드 | 동영상/오디오 처리

이 글의 핵심

FFmpeg는 옵션 위치와 필터 문법 하나에 결과가 달라져서, 명령어를 복사해 쓰다 보면 왜 동작하는지 모른 채 막히기 쉽습니다. 이 글은 동영상 파일 구조와 코덱 선택, 기본 명령어 구조와 실행 원리를 먼저 설명한 뒤 변환·편집·스트리밍 명령으로 넘어갑니다. 동영상 업로드 서버, 스트리밍 서버, 일괄 변환 도구 예제로 실제 서비스에 붙이는 방법까지 확인할 수 있습니다.

FFmpeg는 옵션을 -i 앞에 두느냐 뒤에 두느냐, 필터를 어떻게 연결하느냐에 따라 결과가 달라져서, 명령어를 복사해 쓰다 보면 왜 동작하는지 모른 채 막히기 쉽습니다. 이 글은 컨테이너와 코덱, 비트레이트와 프레임레이트 같은 기초 개념과 명령어 구조를 먼저 정리한 뒤 변환·인코딩, 오디오 처리, 썸네일, 편집, HLS·RTMP 스트리밍, 필터로 넘어갑니다. 후반부에서는 Python·Node.js·Go 연동, 하드웨어 가속, 자주 만나는 에러와 해결 방법을 다룹니다.


사전 지식 (초보자를 위한 기초)

동영상 파일 구조

동영상 파일을 이해하려면 컨테이너와 코덱의 차이를 알아야 합니다. 많은 사람들이 이 둘을 혼동하는데, 실제로는 완전히 다른 개념입니다.

동영상 파일 구조:
┌─────────────────────────────────┐
│   컨테이너 (Container)          │
│   예: MP4, MKV, AVI, MOV        │
│                                 │
│  ┌──────────────────────────┐  │
│  │  비디오 스트림           │  │
│  │  코덱: H.264, H.265      │  │
│  └──────────────────────────┘  │
│                                 │
│  ┌──────────────────────────┐  │
│  │  오디오 스트림           │  │
│  │  코덱: AAC, MP3          │  │
│  └──────────────────────────┘  │
│                                 │
│  ┌──────────────────────────┐  │
│  │  자막 스트림 (선택)      │  │
│  │  SRT, ASS                │  │
│  └──────────────────────────┘  │
└─────────────────────────────────┘

컨테이너 (Container)

  • 비디오, 오디오, 자막을 담는 “상자” 역할
  • 예: MP4, MKV, AVI, MOV, WebM
  • 같은 코덱이라도 컨테이너에 따라 호환성이 달라집니다
  • MP4는 가장 범용적이며, MKV는 기능이 많지만 일부 기기에서 재생 안 될 수 있습니다

코덱 (Codec)

  • 데이터를 압축/해제하는 “알고리즘”
  • 비디오: H.264 (가장 보편적), H.265 (고효율), VP9 (웹용), AV1 (차세대)
  • 오디오: AAC (범용), MP3 (레거시), Opus (고효율), FLAC (무손실)
  • 같은 화질이라도 코덱 세대에 따라 필요한 비트레이트가 크게 다릅니다(예: MPEG-2와 H.265)

비유로 이해하기

  • 컨테이너 = 택배 상자
  • 코덱 = 물건을 압축하는 방법
  • 같은 물건(영상)이라도 어떻게 압축하고(코덱) 어떤 상자에 담느냐(컨테이너)에 따라 결과가 달라집니다

비트레이트와 해상도

비트레이트 (Bitrate): 초당 전송되는 데이터 양입니다. 단위는 kbps(킬로비트), Mbps(메가비트)를 사용합니다.

  • 높은 비트레이트: 화질은 좋지만 파일 크기가 크고 스트리밍 시 버퍼링 발생 가능
  • 낮은 비트레이트: 파일은 작지만 화질 저하, 특히 빠른 움직임에서 블록 노이즈 발생
  • 적정 비트레이트: 해상도와 프레임레이트, 콘텐츠 특성에 따라 결정됩니다
비디오 비트레이트 가이드:
4K (3840×2160):  20-50 Mbps
1080p (1920×1080): 5-10 Mbps
720p (1280×720):   2.5-5 Mbps
480p (854×480):    1-2 Mbps
360p (640×360):    0.5-1 Mbps
오디오 비트레이트:
320 kbps: 고음질 (음악)
192 kbps: 표준 (음악)
128 kbps: 일반 (팟캐스트)
64 kbps: 저음질 (통화)

해상도 (Resolution): 가로×세로 픽셀 수입니다.

  • 4K (3840×2160): 영화관 수준, 파일 크기 매우 큼
  • 1080p (1920×1080): Full HD, 가장 보편적인 고화질
  • 720p (1280×720): HD, 모바일이나 웹 스트리밍에 적합
  • 480p (854×480): SD, 저사양 기기나 느린 네트워크용 실전 팁: 같은 비트레이트라도 해상도가 높으면 화질이 떨어집니다. 4K를 5Mbps로 인코딩하는 것보다 1080p를 5Mbps로 인코딩하는 게 훨씬 깨끗합니다.

프레임레이트

프레임레이트 (Frame Rate, FPS): 초당 몇 장의 화면을 보여주는지를 나타냅니다. 단위는 fps(frames per second)입니다. 프레임레이트가 높을수록 움직임이 부드럽지만 인코딩할 프레임이 늘어 비트레이트도 올라갑니다. 다만 프레임 간격이 짧을수록 이웃 프레임이 더 비슷해 예측 효율이 좋아지므로, 60fps가 30fps의 정확히 두 배 크기가 되지는 않습니다. 콘텐츠 특성에 맞는 선택이 중요합니다.

일반적인 프레임레이트:
24 fps: 영화 (시네마틱 느낌, 영화관 표준)
30 fps: TV, 유튜브 (일반 영상, 가장 보편적)
60 fps: 게임, 스포츠 (부드러운 움직임, 액션 장면)
120 fps: 슬로우 모션 촬영용 (편집 시 24fps나 30fps로 느리게 재생)
실전 선택 가이드:
- 브이로그, 강의: 30fps (파일 크기와 품질 균형)
- 게임 플레이, 스포츠: 60fps (빠른 움직임 표현)
- 영화 느낌: 24fps (시네마틱 무드)
- 슬로우 모션 필요: 120fps 이상 촬영 후 편집

코덱 선택 가이드

코덱 선택은 호환성, 파일 크기, 인코딩 속도, 화질의 균형을 맞추는 작업입니다. 상황별로 최적의 선택이 다릅니다.

비디오 코덱 비교:
H.264 (AVC) - 가장 안전한 선택
  ✅ 장점: 거의 모든 기기 지원, 빠른 인코딩, 하드웨어 가속 지원
  ❌ 단점: H.265 대비 파일 크기 큼
  📌 추천: 일반 용도, 최대 호환성 필요, 빠른 처리 필요
  
H.265 (HEVC) - 용량 최적화
  ✅ 장점: 같은 화질에서 H.264보다 파일이 상당히 작음, 4K에 적합
  ❌ 단점: 느린 인코딩, 구형 기기 미지원, 라이선스 비용
  📌 추천: 4K 영상, 저장 공간 부족, 최신 기기 타겟
  
VP9 - 웹 스트리밍용
  ✅ 장점: 로열티 프리, YouTube 지원, 웹 브라우저 호환
  ❌ 단점: 매우 느린 인코딩, 하드웨어 지원 제한적
  📌 추천: YouTube 업로드, 웹 전용 콘텐츠
  
AV1 - 미래 대비
  ✅ 장점: 최고의 압축 효율, 로열티 프리
  ❌ 단점: 극도로 느린 인코딩, 제한적 재생 지원
  📌 추천: 장기 보관용, 실험적 프로젝트
오디오 코덱 비교:
AAC - 범용 표준
  ✅ 장점: 우수한 음질/용량 비율, 광범위한 지원
  ❌ 단점: MP3보다 약간 느린 인코딩
  📌 추천: 대부분의 경우 (기본 선택)
  
MP3 - 레거시 호환
  ✅ 장점: 거의 모든 기기·브라우저에서 재생, 빠른 인코딩
  ❌ 단점: AAC 대비 큰 파일 또는 낮은 음질
  📌 추천: 구형 기기 지원 필수, 팟캐스트
  
Opus - 고효율
  ✅ 장점: 최고의 음질/용량 비율, 로열티 프리
  ❌ 단점: 제한적 기기 지원
  📌 추천: 웹 스트리밍, 음성 통화, Discord
FLAC - 무손실
  ✅ 장점: 원음을 손실 없이 보존, 압축 지원
  ❌ 단점: 큰 파일 크기
  📌 추천: 음악 아카이빙, 마스터링
실전 의사결정 트리:
1. 최대 호환성 필요? → H.264 + AAC
2. 파일 크기 최소화? → H.265 + AAC (또는 Opus)
3. YouTube 업로드? → VP9 + Opus (또는 H.264 + AAC)
4. 4K 영상? → H.265 + AAC
5. 웹 전용? → VP9 + Opus
6. 음악/오디오 보관? → FLAC

FFmpeg이란?

FFmpeg는 동영상과 오디오를 디코딩·인코딩·변환·스트리밍하는 오픈소스 멀티미디어 프레임워크입니다. 2000년 Fabrice Bellard가 시작한 프로젝트로, VLC 같은 플레이어가 FFmpeg의 libavcodec을 쓰고 많은 동영상 서비스가 트랜스코딩 파이프라인에 FFmpeg를 활용합니다.

주요 기능

FFmpeg로 할 수 있는 작업은 거의 무한합니다:

기본 기능

  • 동영상/오디오 포맷 변환 (MP4 ↔ AVI ↔ MKV 등)
  • 인코딩/디코딩 (압축 및 압축 해제)
  • 해상도 변경 및 품질 조정
  • 썸네일 및 스크린샷 추출

고급 기능

  • 실시간 스트리밍 (RTMP, HLS, DASH)
  • 필터 적용 (자르기, 회전, 워터마크, 색보정)
  • 오디오 처리 (노이즈 제거, 볼륨 조절, 믹싱)
  • 화면 녹화 및 웹캠 캡처
  • 자막 추가 및 변환
  • 여러 파일 병합 및 분할

실무 활용 예시

  • 유튜브 업로드용 영상 최적화
  • 웹사이트용 다중 해상도 영상 생성
  • 라이브 스트리밍 서버 구축
  • 동영상 자동 처리 파이프라인 구축
  • 모바일 앱용 영상 변환

FFmpeg 구성 요소

FFmpeg는 여러 도구와 라이브러리로 구성되어 있습니다. 각각의 역할을 이해하면 더 효과적으로 활용할 수 있습니다.

명령줄 도구:
ffmpeg
  - 가장 많이 쓰는 핵심 도구
  - 파일 변환, 인코딩, 필터 적용 등 모든 작업 수행
  - 예: ffmpeg -i input.mp4 output.avi
ffprobe
  - 미디어 파일 정보 분석 도구
  - 코덱, 해상도, 비트레이트, 길이 등 메타데이터 추출
  - 스크립트에서 파일 정보 확인 시 필수
  - 예: ffprobe -show_format input.mp4
ffplay
  - 간단한 미디어 재생기
  - 인코딩 결과 빠르게 확인할 때 유용
  - 테스트 및 디버깅용
  - 예: ffplay output.mp4
라이브러리 (프로그래밍 시 사용):
libavcodec
  - 코덱 인코딩/디코딩 라이브러리
  - H.264, H.265, AAC, MP3 등 수백 개 코덱 지원
  - 직접 코딩할 때 가장 핵심적인 라이브러리
libavformat
  - 컨테이너 포맷 처리 라이브러리
  - MP4, MKV, AVI 등 파일 읽기/쓰기
  - 스트리밍 프로토콜 지원 (RTMP, HLS 등)
libavfilter
  - 필터 그래프 처리 라이브러리
  - 영상/오디오 효과 적용
  - 크기 조정, 자르기, 워터마크, 색보정 등
libavutil
  - 공통 유틸리티 라이브러리
  - 수학 함수, 메모리 관리, 로깅 등
  - 다른 라이브러리들의 기반
libswscale
  - 이미지 스케일링 및 색공간 변환
  - 해상도 변경, RGB ↔ YUV 변환
  - 고품질 리사이징 알고리즘 제공
libswresample
  - 오디오 리샘플링 라이브러리
  - 샘플레이트 변환, 채널 믹싱
  - 스테레오 ↔ 모노 변환 등
실전 팁:
- 일반 사용자: ffmpeg 명령어만 알면 충분
- 개발자: Python/Node.js 바인딩 사용 (ffmpeg-python, fluent-ffmpeg)
- 고급 개발자: libav 라이브러리 직접 사용 (C/C++)

설치 및 기본 사용법

설치

macOS

brew install ffmpeg
# Homebrew 공식 포뮬러는 2019년에 --with-* 빌드 옵션을 없앴습니다.
# libfdk_aac 같은 비자유 코덱이 필요하면 서드파티 tap이나 직접 빌드를 써야 합니다.

Ubuntu/Debian

sudo apt update
sudo apt install ffmpeg
# 배포판 패키지는 버전이 오래된 경우가 많습니다.
# 최신 기능이 필요하면 ffmpeg.org 다운로드 페이지에 링크된 정적 빌드를 쓰는 방법이 있습니다.

Windows

# Chocolatey
choco install ffmpeg
# 또는 수동 설치
# 1. https://ffmpeg.org/download.html
# 2. 압축 해제
# 3. 환경 변수 PATH에 bin 폴더 추가

Docker

docker pull linuxserver/ffmpeg
docker run --rm -v $(pwd):/data linuxserver/ffmpeg \
  -i /data/input.mp4 -c:v libx264 /data/output.mp4

버전 확인

ffmpeg -version
# 출력:
# ffmpeg version 6.1.1
# configuration: --enable-gpl --enable-libx264 --enable-libx265 ...
# libavcodec     60.31.102
# libavformat    60.16.100
# ...

기본 명령어 구조 - FFmpeg의 실행 원리

FFmpeg 명령어는 일정한 패턴을 따릅니다. 이 구조를 이해하면 복잡한 명령어도 쉽게 만들 수 있습니다.

ffmpeg [전역 옵션] [입력 옵션] -i [입력 파일] [출력 옵션] [출력 파일]

# 기본 예시
ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4
#      ↑           ↑              ↑          ↑
#      입력        비디오 코덱    품질       출력

# 더 복잡한 예시
ffmpeg -y -i input.mp4 -i audio.mp3 -c:v libx264 -crf 20 -c:a aac -b:a 192k -shortest output.mp4
#      ↑  ↑           ↑             ↑              ↑      ↑          ↑         ↑         ↑
#      덮어쓰기  비디오입력  오디오입력  비디오코덱  비디오품질  오디오코덱  오디오비트  짧은쪽맞춤  출력

FFmpeg의 내부 처리 파이프라인 (상세)

ffmpeg -i input.mp4 -c:v libx264 -crf 23 -vf scale=1280:720 output.mp4

1. Demuxing (디먹싱) - libavformat
   ┌────────────────────────────────┐
   │   input.mp4 Container          │
   │   ┌──────────────────────────┐ │
   │   │ Video Stream #0 (H.264)  │ │ → Packet Queue
   │   │ - Codec: H.264           │ │
   │   │ - Bitrate: 5 Mbps        │ │
   │   │ - Resolution: 1920x1080  │ │
   │   └──────────────────────────┘ │
   │   ┌──────────────────────────┐ │
   │   │ Audio Stream #1 (AAC)    │ │ → Packet Queue
   │   │ - Codec: AAC             │ │
   │   │ - Bitrate: 192 kbps      │ │
   │   │ - Sample Rate: 48000 Hz  │ │
   │   └──────────────────────────┘ │
   │   ┌──────────────────────────┐ │
   │   │ Metadata                 │ │
   │   │ - Duration: 00:05:30     │ │
   │   │ - Creation Time          │ │
   │   └──────────────────────────┘ │
   └────────────────────────────────┘

   Demuxer 동작:
   - MP4 atom 파싱 (moov, mdat, ftyp)
   - Stream 정보 추출
   - Packet 단위로 데이터 읽기
   - Seek 테이블 생성

2. Decoding (디코딩) - libavcodec
   Compressed Packet → Raw Frame
   
   Video Decoding:
   [H.264 Packet] → libavcodec/h264 decoder
   
   내부 동작:
   1) NAL Unit 분리
   2) SPS/PPS 파싱 (해상도, 프로파일)
   3) Slice 디코딩
   4) Reference Frame 관리
   5) Deblocking Filter 적용
   
   결과: YUV420p 프레임 1장 = 1920×1080×1.5바이트 ≈ 3MB
   - Y plane: 1920x1080 (luma)
   - U plane: 960x540 (chroma)
   - V plane: 960x540 (chroma)
   
   Audio Decoding:
   [AAC Packet] → libavcodec/aac decoder

   내부 동작:
   1) 코덱 설정 읽기 (MP4에서는 AudioSpecificConfig, ADTS 스트림이면 ADTS 헤더)
   2) 허프만 디코딩, 역양자화
   3) IMDCT(역 MDCT)로 시간 영역 샘플 복원
   (심리음향 모델은 인코더에서만 쓰입니다)

   결과: 1024 샘플 × 2채널 × float32(4바이트) = 약 8KB의 PCM

3. Filtering (필터링) - libavfilter
   Raw Frame → Filter Graph → Processed Frame
   
   -vf scale=1280:720 실행:
   
   Filter Graph:
   [in] → scale → [out]
   
   scale 필터 내부:
   1) swscale (libswscale) 호출
   2) 리샘플링 알고리즘 선택:
      - Bilinear (빠름)
      - Bicubic (균형)
      - Lanczos (고품질)
   3) Y/U/V plane 개별 스케일링:
      Y: 1920x1080 → 1280x720
      U: 960x540 → 640x360
      V: 960x540 → 640x360
   
   결과: 1280×720×1.5바이트 ≈ 1.4MB
   
   복잡한 Filter Graph 예시:
   -vf "scale=1280:720,fps=30,eq=brightness=0.1"
   
   [in] → scale → fps → eq → [out]
          ↓       ↓     ↓
      swscale  frame  color
              drop   adjust

4. Encoding (인코딩) - libavcodec
   Raw Frame → Compressed Packet
   
   libx264 인코딩 과정:
   
   1) Macroblock 분할 (16x16):
      1280x720 → 80x45 macroblocks
   
   2) Motion Estimation:
      현재 프레임과 Reference Frame 비교
      Motion Vector 계산
      → P-frame, B-frame 생성
   
   3) Transform (정수 DCT 근사):
      예측 후 남은 잔차(residual)를 주파수 영역으로 변환
      (변환 자체는 손실이 없음)

   4) Quantization:
      CRF 23에 맞춰 프레임·블록별 QP 결정
      → 계수를 QP에 따른 간격으로 나눠 반올림 (여기서 정보 손실 발생)

   5) Entropy Coding:
      CABAC(문맥 적응 산술 부호화) 또는 CAVLC(가변 길이 부호)
      libx264는 Baseline 프로파일이 아니면 기본으로 CABAC 사용

   6) 복원 + Deblocking Filter (루프 내 필터):
      인코더도 디코더와 같은 복원 프레임을 만들어 다음 프레임의 참조로 사용

   결과 패킷 크기는 프레임 유형과 장면 복잡도에 따라 크게 다릅니다.
   - I-frame: 다른 프레임을 참조하지 않아 가장 큼 (libx264 기본 최대 간격 250프레임)
   - P-frame: 이전 프레임 참조, 보통 I보다 훨씬 작음
   - B-frame: 양방향 참조, 보통 가장 작음

   Audio Encoding (AAC):
   PCM 1024 샘플(채널당) → libavcodec/aac

   1) MDCT (1024 samples)
   2) 심리음향 모델
      → 들리지 않는(마스킹되는) 성분에 비트를 덜 배분
   3) Quantization
   4) Huffman Coding

   결과: 48kHz에서 1024 샘플은 약 21.3ms이므로,
   192kbps라면 패킷 하나가 평균 약 512바이트
   (48kHz 스테레오 float PCM 약 8KB 대비 약 16:1)

5. Muxing (먹싱) - libavformat
   Compressed Packets → Container
   
   MP4 Muxer 동작:
   
   1) Container 헤더 작성:
      ftyp: 파일 타입
      moov: 메타데이터
      - mvhd: 영상 정보
      - trak: 트랙 정보
        - tkhd: 트랙 헤더
        - mdia: 미디어 정보
   
   2) 데이터 인터리빙:
      Video Packet + Audio Packet 교차 배치
      
      Timeline:
      [V0] [A0] [V1] [A1] [V2] [A2] ...
      
      이유: 순차 재생 최적화
   
   3) mdat Atom 작성:
      실제 압축 데이터 저장
   
   4) Index 생성 (stco, stsc, stsz):
      Seek 테이블
      → 빠른 탐색 가능
   
   5) -movflags +faststart 옵션:
      moov를 파일 앞으로 이동
      → 웹 스트리밍 즉시 시작 가능
   
   최종 output.mp4 구조 (faststart 적용 시):
   ┌───────────────┐
   │ ftyp          │
   ├───────────────┤
   │ moov          │ ← 인덱스·메타데이터 (영상 길이에 비례해 커짐)
   ├───────────────┤
   │ mdat          │ ← 실제 압축 데이터 (대부분의 용량)
   └───────────────┘

파이프라인의 메모리 사용:
- 디먹서·먹서 버퍼, 디코딩된 프레임, 필터 버퍼, 인코더의 참조 프레임과 lookahead 버퍼가 차지합니다
- 해상도, 참조 프레임 수, lookahead 길이, 스레드 수에 따라 크게 달라집니다

병렬 처리:
libx264는 기본적으로 프레임 단위 스레딩을 씁니다. 여러 프레임을 동시에
인코딩하되, 뒤 프레임은 앞 프레임에서 참조할 영역이 복원된 뒤에야 진행할 수 있어
코어 수만큼 선형으로 빨라지지는 않습니다.
libx264는 기본값(-threads 0)으로 이미 코어 수에 맞춰 스레드 수를 정합니다.

명령어 옵션의 위치가 중요한 이유

# ❌ 잘못된 예: 입력 옵션을 -i 뒤에
ffmpeg -i input.mp4 -ss 00:01:00 -t 10 output.mp4
# 동작:
# 1. 전체 파일을 디코딩 (1분까지)
# 2. 1분부터 10초만 인코딩
# 문제: 불필요한 디코딩 (느림)

# ✅ 올바른 예: 입력 옵션을 -i 앞에
ffmpeg -ss 00:01:00 -t 10 -i input.mp4 output.mp4
# 동작:
# 1. 1분 직전의 키프레임으로 바로 이동 (앞부분을 디코딩하지 않음)
# 2. 재인코딩할 때는 키프레임부터 1분 지점까지 디코딩해 정확한 위치에서 시작
# 3. 10초 분량만 인코딩
# 장점: 시작 지점이 뒤일수록 차이가 커짐
# 주의: -c copy와 함께 쓰면 키프레임 단위로만 자를 수 있어 시작점이 약간 앞당겨질 수 있음

-c:v와 -c:a의 의미

# -c:v libx264
# -c: codec (코덱 지정)
# :v: video stream (비디오 스트림)
# libx264: H.264 인코더 라이브러리

# -c:a aac
# :a: audio stream (오디오 스트림)

# 모든 스트림에 적용
-c copy  # 비디오 + 오디오 모두 복사

# 개별 지정
-c:v copy -c:a aac  # 비디오는 복사, 오디오만 재인코딩

# 여러 출력 파일
ffmpeg -i input.mp4 \
  -c:v libx264 -crf 23 1080p.mp4 \
  -c:v libx264 -crf 28 -vf scale=1280:720 720p.mp4
# 한 번의 디코딩으로 여러 해상도 생성 (효율적!)

-map의 강력한 기능

# input.mp4: 비디오 2개 + 오디오 3개 (다국어)
ffprobe input.mp4
# Stream #0:0: Video (비디오 - 주 화면)
# Stream #0:1: Video (비디오 - 주석 화면)
# Stream #0:2: Audio (한국어)
# Stream #0:3: Audio (영어)
# Stream #0:4: Audio (일본어)

# 기본 동작 (자동 선택): 해상도가 가장 높은 비디오 1개 + 채널 수가 가장 많은 오디오 1개
# (조건이 같으면 앞쪽 스트림)
ffmpeg -i input.mp4 output.mp4
# 두 비디오의 해상도와 세 오디오의 채널 수가 같다면 Stream #0:0 + Stream #0:2

# 수동 선택: 주 화면 + 영어 오디오
ffmpeg -i input.mp4 -map 0:0 -map 0:3 -c copy english.mp4
# -map 0:0: 입력 0번의 스트림 0번 (비디오)
# -map 0:3: 입력 0번의 스트림 3번 (영어 오디오)

# 여러 오디오 트랙 포함
ffmpeg -i input.mp4 -map 0:0 -map 0:2 -map 0:3 -c copy multi_audio.mp4
# 비디오 1개 + 오디오 2개 (한국어, 영어)

# 실전 예제: 비디오와 외부 오디오 결합
ffmpeg -i video.mp4 -i audio.mp3 -map 0:v -map 1:a -c copy output.mp4
# -map 0:v: 첫 번째 입력의 모든 비디오 스트림
# -map 1:a: 두 번째 입력의 모든 오디오 스트림

주요 옵션 설명

# 전역 옵션 (입력 파일 앞에 위치)
-y              # 출력 파일 덮어쓰기 (확인 없이)
-n              # 출력 파일 덮어쓰기 금지
-v quiet        # 로그 출력 최소화
-stats          # 진행 상황 표시
# 입력 옵션 (-i 앞에 위치)
-ss 00:01:30    # 시작 시간 (1분 30초부터)
-t 10           # 지속 시간 (10초만)
-to 00:02:00    # 종료 시간 (2분까지)
# 비디오 출력 옵션
-c:v libx264    # 비디오 코덱 (H.264)
-c:v copy       # 비디오 재인코딩 없이 복사
-crf 23         # 품질 (8비트 x264 기준 0=무손실, 51=최저, 기본값 23)
-preset slow    # 인코딩 속도 (slow=고품질)
-b:v 5M         # 비디오 비트레이트 (5Mbps)
-r 30           # 프레임레이트 (30fps)
-vf scale=1280:720  # 비디오 필터 (해상도 변경)
# 오디오 출력 옵션
-c:a aac        # 오디오 코덱 (AAC)
-c:a copy       # 오디오 재인코딩 없이 복사
-b:a 192k       # 오디오 비트레이트 (192kbps)
-ar 48000       # 샘플레이트 (48kHz)
-ac 2           # 채널 수 (2=스테레오)
# 기타 유용한 옵션
-an             # 오디오 제거
-vn             # 비디오 제거
-shortest       # 가장 짧은 스트림에 맞춤
-map 0:v:0      # 첫 번째 비디오 스트림 선택
-map 0:a:1      # 두 번째 오디오 스트림 선택

실전 팁

  1. 옵션 순서가 중요합니다
    • 입력 옵션은 -i 앞에
    • 출력 옵션은 -i 뒤, 출력 파일 앞에
  2. -c:v와 -vcodec은 같습니다
    • -c:v libx264 = -vcodec libx264
    • -c:a aac = -acodec aac
  3. copy는 재인코딩을 건너뜁니다
    • 매우 빠르지만 품질 조정 불가
    • 컨테이너만 바꿀 때 유용
  4. 여러 입력 파일 사용 가능
    ffmpeg -i video.mp4 -i audio.mp3 -c copy output.mp4
  5. 진행 상황 확인
    ffmpeg -i input.mp4 -c:v libx264 output.mp4
    # 실시간으로 frame=, fps=, time= 등이 표시됩니다

동영상 변환 및 인코딩

동영상 변환은 FFmpeg의 가장 기본적이면서도 강력한 기능입니다. 단순 포맷 변환부터 고급 인코딩까지 다룹니다.

기본 변환

가장 간단한 형태의 변환입니다. FFmpeg가 자동으로 적절한 코덱을 선택합니다.

# MP4 → AVI (자동 코덱 선택)
ffmpeg -i input.mp4 output.avi
# MOV → MP4 (자동 코덱 선택)
ffmpeg -i input.mov output.mp4
# MKV → MP4 (재인코딩 없이 복사 - 가장 빠름)
ffmpeg -i input.mkv -c copy output.mp4
# -c copy: 비디오와 오디오를 재인코딩 없이 그대로 복사
# 장점: 디코딩·인코딩을 하지 않아 매우 빠르고(디스크 복사 속도에 가까움), 품질 손실 없음
# 단점: 해상도/품질 조정 불가, 코덱이 컨테이너와 호환되어야 함
# 비디오만 복사, 오디오는 재인코딩
ffmpeg -i input.mkv -c:v copy -c:a aac output.mp4
# MKV의 비디오는 그대로 두고 오디오만 AAC로 변환
# 실전 예시: iPhone 촬영 영상(MOV)을 웹용 MP4로
ffmpeg -i iphone_video.mov -c:v libx264 -crf 23 -c:a aac -b:a 128k web_video.mp4

언제 -c copy를 쓸까?

  • 컨테이너만 바꾸고 싶을 때 (MKV → MP4)
  • 영상 자르기만 할 때 (품질 유지)
  • 빠른 처리가 필요할 때

언제 재인코딩이 필요할까?

  • 해상도를 바꿀 때
  • 파일 크기를 줄일 때
  • 호환성 문제가 있을 때 (예: HEVC → H.264)
  • 필터를 적용할 때 (워터마크, 색보정 등)

H.264 인코딩

H.264는 가장 보편적인 비디오 코덱입니다. 거의 모든 기기에서 재생되며, 품질과 파일 크기의 균형이 우수합니다.

CRF (Constant Rate Factor) 방식 - 권장

CRF는 일정한 품질을 유지하면서 인코딩하는 방식입니다. 파일 크기는 영상의 복잡도에 따라 자동으로 조절됩니다.

# 기본 CRF 인코딩
ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4
# CRF 값 가이드:
# 0  = 무손실 (파일 크기 매우 큼, 거의 사용 안 함)
# 17-18 = 시각적으로 무손실 (전문가용, 편집 소스)
# 19-23 = 고품질 (일반 용도)
# 24-28 = 중간 품질 (웹 스트리밍, 모바일)
# 29-35 = 낮은 품질 (저사양 기기, 프리뷰용)
# 36-51 = 매우 낮은 품질 (거의 사용 안 함)
# 실전 예시들:
# YouTube 업로드용 (고품질)
ffmpeg -i input.mp4 -c:v libx264 -crf 18 -preset slow -c:a aac -b:a 192k youtube.mp4
# 웹사이트 배경 영상 (중간 품질, 작은 파일)
ffmpeg -i input.mp4 -c:v libx264 -crf 28 -preset fast -c:a aac -b:a 128k web_bg.mp4
# 모바일 앱용 (균형잡힌 품질)
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium -vf scale=1280:720 -c:a aac -b:a 128k mobile.mp4
# 아카이브용 (최고 품질)
ffmpeg -i input.mp4 -c:v libx264 -crf 17 -preset veryslow -c:a aac -b:a 320k archive.mp4

CRF 선택 가이드

용도CRF 값설명
편집 소스17-18재편집 시 품질 손실 최소화
YouTube/Vimeo18-20플랫폼 재인코딩 대비
일반 공유21-23대부분의 경우 적합
웹 스트리밍24-26빠른 로딩과 품질 균형
모바일26-28작은 화면, 데이터 절약
프리뷰/테스트28-30빠른 확인용

주의사항

  • CRF 값이 낮을수록 품질은 좋지만 파일 크기가 커집니다
  • 같은 CRF라도 영상의 복잡도에 따라 파일 크기가 다릅니다
    • 정적인 장면: 작은 파일
    • 빠른 움직임, 복잡한 장면: 큰 파일

프리셋 (Preset) - 속도 vs 압축률

프리셋은 인코딩 속도와 압축 효율의 균형을 조절합니다. 느린 프리셋일수록 같은 품질에서 파일 크기가 작아지지만, 인코딩 시간이 오래 걸립니다.

# 프리셋 기본 사용법
ffmpeg -i input.mp4 -c:v libx264 -preset slow -crf 23 output.mp4
# 프리셋 종류 (빠름 → 느림):
ultrafast   # 인코딩: 초고속 (실시간 가능), 파일: 매우 큼
superfast   # 인코딩: 매우 빠름, 파일: 큼
veryfast    # 인코딩: 빠름, 파일: 약간 큼
faster      # 인코딩: 빠른 편, 파일: 보통
fast        # 인코딩: 보통, 파일: 보통
medium      # 인코딩: 보통 (기본값), 파일: 보통
slow        # 인코딩: 느림, 파일: 작음 (★ 권장)
slower      # 인코딩: 매우 느림, 파일: 매우 작음
veryslow    # 인코딩: 극도로 느림, 파일: 최소
# 실전 예시:
# 빠른 테스트용 (품질 확인)
ffmpeg -i input.mp4 -c:v libx264 -preset ultrafast -crf 23 test.mp4
# 일반 용도 (균형)
ffmpeg -i input.mp4 -c:v libx264 -preset medium -crf 23 output.mp4
# 최종 배포용 (최적 품질)
ffmpeg -i input.mp4 -c:v libx264 -preset slow -crf 20 final.mp4
# 아카이브용 (최대 압축)
ffmpeg -i input.mp4 -c:v libx264 -preset veryslow -crf 18 archive.mp4

프리셋 선택 가이드

상황추천 프리셋이유
실시간 스트리밍ultrafast, veryfastCPU 부하 최소화
빠른 프리뷰veryfast, fast빠른 확인
일반 작업medium기본 균형
최종 배포slow최적 품질/크기 비율
아카이브slower, veryslow장기 보관용 최소 크기
배치 처리fast, medium대량 파일 처리

프리셋이 바꾸는 것

CRF를 고정하면 프리셋은 화질이 아니라 같은 화질을 내는 데 드는 시간과 파일 크기를 바꿉니다. 느린 프리셋일수록 더 많은 탐색(모션 추정, 참조 프레임, B프레임 배치)을 해서 같은 CRF에서 파일이 작아지지만, 인코딩 시간은 그보다 훨씬 빠르게 늘어납니다. slow를 넘어가면 크기 감소는 작아지고 시간만 크게 늘어나는 구간에 들어가는 경우가 많습니다. 정확한 차이는 소스와 CPU에 따라 다르므로, 대표 클립 하나로 직접 재 보는 것이 가장 확실합니다.

실전 팁

  1. 개발/테스트 단계: veryfast 사용
    • 빠른 피드백으로 효율적인 작업
  2. 최종 배포: slow 사용
    • 시간을 들여도 최적 결과 확보
  3. 대량 처리: medium 사용
    • 시간과 품질의 균형
  4. 프리셋과 CRF 조합:
    # 빠르게 + 높은 품질 = 큰 파일
    ffmpeg -i input.mp4 -preset veryfast -crf 18 output.mp4
    
    # 느리게 + 낮은 품질 = 작은 파일
    ffmpeg -i input.mp4 -preset slow -crf 28 output.mp4
    
    # 권장 조합 (일반 용도)
    ffmpeg -i input.mp4 -preset slow -crf 22 output.mp4

Preset이 내부적으로 바꾸는 파라미터

프리셋은 libx264의 여러 파라미터를 한 번에 정하는 묶음입니다. 정확한 정의는 x264 소스나 x264 --fullhelp 출력에서 확인할 수 있고, 주요 값은 다음과 같습니다(기본값인 medium을 기준으로 다른 프리셋이 일부를 바꿉니다).

파라미터          ultrafast     medium(기본)    slow          veryslow
────────────────────────────────────────────────────────────────────
ref               1             3               5             16
me                dia           hex             umh           umh
subme             0             7               8             10
me_range          16            16              16            24
bframes           0             3               3             8
b-adapt           0             1               2             2
rc-lookahead      0             40              50            60
trellis           0             1               2             2
partitions        none          i4x4,i8x8,      (medium과 같음) all
                                p8x8,b8x8
direct            spatial       spatial         auto          auto
8x8dct            끔            켬              켬            켬
deblock           끔            켬              켬            켬
cabac             끔            켬              켬            켬
weightp           0             2               2             2
────────────────────────────────────────────────────────────────────
(me=tesa, subme=11은 가장 느린 placebo 프리셋에서만 씁니다)

각 파라미터의 의미는 다음과 같습니다.

  • ref: 움직임 예측에 참조할 수 있는 이전 프레임 수입니다. 늘리면 압축률이 조금 좋아지지만 디코더와 인코더 모두 그만큼 프레임을 메모리에 들고 있어야 하고, H.264 레벨이 허용하는 최대 참조 수(DPB 크기)를 넘으면 일부 하드웨어 디코더가 재생하지 못할 수 있습니다.
  • me: 정수 픽셀 단위 움직임 탐색 방식입니다. dia(다이아몬드)는 가장 좁고 빠르며, hex(육각형), umh(비균일 다중 육각형) 순으로 더 넓게 탐색하고, tesa는 거의 전수 탐색에 가깝습니다. me_range는 umh·tesa에서 탐색할 최대 거리입니다.
  • subme: 서브픽셀 정밀화와 모드 결정의 정밀도입니다. 0은 정수 픽셀만 쓰고, 1 이상부터 H.264가 허용하는 최대 정밀도인 1/4 픽셀까지 정밀화하며, 숫자가 클수록 반복 횟수와 RD(rate-distortion) 최적화 범위가 넓어집니다. 6 이상에서 RD 기반 모드 결정을 쓰고, 10은 trellis=2와 적응형 양자화가 켜져 있어야 동작합니다. 1/8 픽셀 같은 정밀도는 H.264 규격에 없습니다.
  • bframes/b-adapt: bframes는 연속으로 넣을 수 있는 B프레임 최대 개수이고, b-adapt는 실제 개수를 정하는 방법입니다. 0은 항상 최대 개수를 쓰고, 1은 빠른 휴리스틱, 2는 여러 배치를 비교하는 느리지만 정확한 방법입니다. ultrafast가 B프레임을 쓰지 않는 것은 bframes=0이기 때문입니다.
  • rc-lookahead: 비트 배분(MB-tree)과 프레임 유형 결정을 위해 미리 분석하는 프레임 수입니다. 길수록 장면 전환이나 정지 구간에 비트를 더 잘 배분하지만, 그만큼 프레임을 버퍼에 들고 있어 메모리와 지연이 늘어납니다.
  • trellis: 양자화 결과를 rate-distortion 관점에서 다시 고르는 과정입니다. 1은 매크로블록 최종 인코딩 때만, 2는 모드 결정 전반에서 적용합니다.
  • partitions: 움직임 예측과 인트라 예측에 쓸 블록 분할 종류입니다. H.264 매크로블록은 16×16이고, p8x8을 켜면 16×8, 8×16, 8×8 분할을, p4x4를 켜면 그보다 작은 분할까지 씁니다. all은 여기에 p4x4를 더한 것입니다.

프리셋의 일부 값만 바꾸고 싶다면 -x264-params로 덮어씁니다. 예를 들어 slow 프리셋을 쓰면서 참조 프레임만 줄이려면 다음과 같이 합니다.

ffmpeg -i input.mp4 -c:v libx264 -preset slow -crf 22 \
  -x264-params ref=3 \
  output.mp4

경향만 정리하면, 빠른 프리셋은 인코딩 시간이 짧고 같은 CRF에서 파일이 크며 메모리를 적게 씁니다. 느린 프리셋은 인코딩 시간이 길고 파일이 작으며, lookahead와 참조 프레임 때문에 메모리를 더 씁니다. 구체적인 차이는 소스와 CPU마다 다릅니다.

2-Pass 인코딩 - 목표 비트레이트 정확도

2-Pass 인코딩은 영상을 두 번 처리합니다. 첫 번째는 분석, 두 번째는 실제 인코딩입니다. 목표 비트레이트를 정확히 맞춰야 할 때 사용합니다.

# 1단계: 분석 (영상 전체를 스캔하여 통계 수집)
ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -pass 1 -f null /dev/null
# Windows에서는: -f null NUL
# 2단계: 실제 인코딩 (1단계 통계를 바탕으로 최적화)
ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -pass 2 output.mp4
# 완전한 예시 (오디오 포함)
# Pass 1
ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -pass 1 -an -f null /dev/null
# Pass 2
ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -pass 2 -c:a aac -b:a 192k output.mp4

2-Pass vs 1-Pass (CRF) 비교

특성1-Pass (CRF)2-Pass (Bitrate)
인코딩 시간한 번 인코딩두 번 처리 (첫 패스는 분석용)
품질 일관성일정한 품질일정한 비트레이트
파일 크기가변적정확히 목표치
사용 시기일반 용도스트리밍, 방송
복잡한 장면비트레이트 증가품질 약간 저하
단순한 장면비트레이트 감소품질 약간 향상

언제 2-Pass를 사용할까?

✅ 사용해야 할 때:

  • 정확한 파일 크기가 필요할 때 (예: DVD, Blu-ray)
  • 스트리밍 비트레이트 상한이 정해져 있을 때
  • 방송 송출 규격을 맞춰야 할 때
  • 여러 영상의 비트레이트를 동일하게 맞춰야 할 때 ❌ 사용하지 않아도 될 때:
  • 일반 파일 변환 (CRF가 더 간단하고 효율적)
  • 빠른 처리가 필요할 때
  • 파일 크기가 유동적이어도 될 때
  • YouTube 등 업로드 (플랫폼이 다시 인코딩함)

실전 예시

# 웹 스트리밍용 (정확한 비트레이트 필요)
# Pass 1
ffmpeg -i input.mp4 -c:v libx264 -b:v 2M -maxrate 2M -bufsize 4M \
  -pass 1 -an -f null /dev/null
# Pass 2
ffmpeg -i input.mp4 -c:v libx264 -b:v 2M -maxrate 2M -bufsize 4M \
  -pass 2 -c:a aac -b:a 128k streaming.mp4
# DVD 제작용 (MPEG-2, 정확한 크기)
# Pass 1
ffmpeg -i input.mp4 -c:v mpeg2video -b:v 8M -pass 1 -an -f null /dev/null
# Pass 2
ffmpeg -i input.mp4 -c:v mpeg2video -b:v 8M -pass 2 -c:a mp2 -b:a 192k dvd.mpg
# 모바일 스트리밍 (낮은 비트레이트, 정확한 제어)
# Pass 1
ffmpeg -i input.mp4 -c:v libx264 -b:v 500k -maxrate 500k -bufsize 1M \
  -pass 1 -an -f null /dev/null
# Pass 2
ffmpeg -i input.mp4 -c:v libx264 -b:v 500k -maxrate 500k -bufsize 1M \
  -pass 2 -c:a aac -b:a 64k mobile.mp4

2-Pass 최적화 팁

  1. 두 패스의 옵션은 같게:
    # libx264는 -pass 1일 때 자동으로 빠른 첫 패스 설정을 적용합니다
    # preset 등 나머지 옵션은 두 패스에서 동일하게 두는 것이 안전합니다
    ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -preset slow -pass 1 -an -f null /dev/null
    ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -preset slow -pass 2 -c:a aac output.mp4
  2. 로그 파일 정리:
    # 2-Pass는 ffmpeg2pass-0.log 파일을 생성합니다
    # 작업 후 삭제:
    rm ffmpeg2pass-0.log
  3. maxrate와 bufsize 함께 사용:
    # 비트레이트 스파이크 방지
    -b:v 5M -maxrate 5M -bufsize 10M

정리: 목표 크기나 비트레이트 상한이 정해져 있지 않다면 1-Pass CRF가 더 간단하고, 같은 시간에 더 일정한 화질을 줍니다. 2-Pass는 “이 파일은 반드시 N MB 이하” 같은 조건이 있을 때 씁니다.

H.265 (HEVC) 인코딩

# H.265 기본
ffmpeg -i input.mp4 -c:v libx265 -crf 28 output.mp4
# FFmpeg 위키(H.265 인코딩 가이드)는 libx265의 기본값 CRF 28이
# libx264 CRF 23과 시각적으로 비슷하다고 설명합니다 (소스에 따라 다름)
# 프리셋 포함
ffmpeg -i input.mp4 -c:v libx265 -preset medium -crf 28 output.mp4
# 태그 추가 (호환성)
ffmpeg -i input.mp4 -c:v libx265 -crf 28 -tag:v hvc1 output.mp4

해상도 변경

해상도 변경은 모바일 최적화, 웹 스트리밍, 파일 크기 절감 등 다양한 목적으로 사용됩니다.

# 기본 해상도 변경 (1080p → 720p)
ffmpeg -i input.mp4 -vf scale=1280:720 output.mp4
# 비율 유지 (너비 고정, 높이 자동 계산)
ffmpeg -i input.mp4 -vf scale=1280:-1 output.mp4
# -1: 원본 비율 유지하며 자동 계산
# 비율 유지 (높이 고정, 너비 자동 계산)
ffmpeg -i input.mp4 -vf scale=-1:720 output.mp4
# 짝수 픽셀 보장 (H.264 등 일부 코덱 요구사항)
ffmpeg -i input.mp4 -vf scale=1280:-2 output.mp4
# -2: 2의 배수로 자동 조정 (홀수 픽셀 방지)
# 4K → 1080p (고품질 다운스케일)
ffmpeg -i input_4k.mp4 -vf scale=1920:1080 -c:v libx264 -preset slow -crf 18 output_1080p.mp4

실전 예시

# 모바일 최적화 (720p, 작은 파일)
ffmpeg -i input.mp4 -vf scale=1280:-2 -c:v libx264 -crf 26 -preset fast -c:a aac -b:a 96k mobile.mp4
# 웹 스트리밍용 다중 해상도 생성
# 1080p
ffmpeg -i input.mp4 -vf scale=1920:-2 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 192k output_1080p.mp4
# 720p
ffmpeg -i input.mp4 -vf scale=1280:-2 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k output_720p.mp4
# 480p
ffmpeg -i input.mp4 -vf scale=854:-2 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 96k output_480p.mp4
# Instagram 정사각형 (1:1 비율, 크롭)
ffmpeg -i input.mp4 -vf "scale=1080:1080:force_original_aspect_ratio=increase,crop=1080:1080" \
  -c:v libx264 -crf 23 instagram.mp4
# YouTube Shorts / TikTok (9:16 세로 비율)
ffmpeg -i input.mp4 -vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920" \
  -c:v libx264 -crf 23 shorts.mp4
# 4K 다운스케일 (고품질 유지)
ffmpeg -i input_4k.mp4 -vf scale=1920:-2 -c:v libx264 -preset slow -crf 18 \
  -c:a copy output_1080p.mp4

고급 스케일링 옵션

# 고품질 스케일링 알고리즘 (lanczos)
ffmpeg -i input.mp4 -vf scale=1280:-2:flags=lanczos output.mp4
# 스케일링 알고리즘 비교:
# bilinear: 빠름, 품질 보통
# bicubic: 중간 (기본값)
# lanczos: 느림, 품질 최고
# spline: 부드러움
# 예시: 각 알고리즘 비교
ffmpeg -i input.mp4 -vf scale=1280:-2:flags=bilinear bilinear.mp4
ffmpeg -i input.mp4 -vf scale=1280:-2:flags=bicubic bicubic.mp4
ffmpeg -i input.mp4 -vf scale=1280:-2:flags=lanczos lanczos.mp4
# 업스케일링 (권장하지 않음, 품질 향상 없음)
ffmpeg -i 720p.mp4 -vf scale=1920:-2:flags=lanczos 1080p.mp4
# 주의: 해상도만 커지고 실제 화질은 개선되지 않음

비율 맞추기

# 16:9 비율 강제 (패딩 추가)
ffmpeg -i input.mp4 -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" output.mp4
# 4:3 → 16:9 (좌우 검은 바 추가)
ffmpeg -i input_4_3.mp4 -vf "scale=1920:-2,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:black" output_16_9.mp4
# 16:9 → 4:3 (상하 검은 바 추가)
ffmpeg -i input_16_9.mp4 -vf "scale=-2:1080,pad=1440:1080:(ow-iw)/2:(oh-ih)/2:black" output_4_3.mp4

일괄 변환 스크립트

# 폴더 내 모든 영상을 720p로 변환
for file in *.mp4; do
  ffmpeg -i "$file" -vf scale=1280:-2 -c:v libx264 -crf 23 "720p_$file"
done
# 다중 해상도 자동 생성
for file in *.mp4; do
  base="${file%.mp4}"
  ffmpeg -i "$file" -vf scale=1920:-2 -c:v libx264 -crf 23 "${base}_1080p.mp4"
  ffmpeg -i "$file" -vf scale=1280:-2 -c:v libx264 -crf 23 "${base}_720p.mp4"
  ffmpeg -i "$file" -vf scale=854:-2 -c:v libx264 -crf 23 "${base}_480p.mp4"
done

주의사항

  1. 업스케일링은 피하세요
    • 720p → 1080p로 키워도 화질은 개선되지 않습니다
    • 오히려 파일 크기만 커집니다
  2. 홀수 픽셀 문제
    • H.264 등은 짝수 픽셀을 요구합니다
    • -2 사용으로 자동 조정
  3. 비율 왜곡 주의
    • scale=1280:720는 강제로 늘리거나 줄입니다
    • 비율 유지는 -1 또는 -2 사용
  4. 성능 고려
    • 다운스케일은 빠름
    • 업스케일은 느리고 의미 없음

프레임레이트 변경

# 60fps → 30fps
ffmpeg -i input.mp4 -r 30 output.mp4
# 24fps → 60fps (보간)
ffmpeg -i input.mp4 -filter:v "minterpolate=fps=60" output.mp4

비트레이트 설정

# 비디오 비트레이트
ffmpeg -i input.mp4 -b:v 5M output.mp4
# 오디오 비트레이트
ffmpeg -i input.mp4 -b:a 192k output.mp4
# 둘 다
ffmpeg -i input.mp4 -b:v 5M -b:a 192k output.mp4
# 최대 비트레이트 제한
ffmpeg -i input.mp4 -b:v 5M -maxrate 5M -bufsize 10M output.mp4

오디오 처리

오디오 추출

# 동영상에서 오디오만 추출
ffmpeg -i video.mp4 -vn -c:a copy audio.aac
# -vn: 비디오 제외
# MP3로 변환
ffmpeg -i video.mp4 -vn -c:a libmp3lame -b:a 192k audio.mp3
# WAV로 변환 (무손실)
ffmpeg -i video.mp4 -vn audio.wav

오디오 변환

# MP3 → AAC
ffmpeg -i input.mp3 -c:a aac -b:a 192k output.aac
# WAV → MP3
ffmpeg -i input.wav -c:a libmp3lame -b:a 320k output.mp3
# FLAC → MP3 (무손실 → 손실)
ffmpeg -i input.flac -c:a libmp3lame -q:a 0 output.mp3
# -q:a 0: 최고 품질 (VBR)

오디오 편집

# 볼륨 조절 (2배)
ffmpeg -i input.mp3 -af "volume=2.0" output.mp3
# 볼륨 조절 (dB)
ffmpeg -i input.mp3 -af "volume=10dB" output.mp3
# 페이드 인/아웃
ffmpeg -i input.mp3 -af "afade=t=in:st=0:d=3,afade=t=out:st=57:d=3" output.mp3
# t=in: 페이드 인, t=out: 페이드 아웃
# st: 시작 시간(초), d: 지속 시간(초)
# 대역 제한 (200Hz 이하 험·3kHz 이상 고역 제거, 음성 전용 소스에서만)
ffmpeg -i input.mp3 -af "highpass=f=200,lowpass=f=3000" output.mp3
# FFT 기반 노이즈 감소
ffmpeg -i input.mp3 -af "afftdn" output.mp3
# 스테레오 → 모노
ffmpeg -i input.mp3 -ac 1 output.mp3
# 샘플레이트 변경
ffmpeg -i input.mp3 -ar 44100 output.mp3

썸네일 및 이미지 추출

썸네일 추출

# 첫 프레임 추출
ffmpeg -i video.mp4 -vframes 1 thumbnail.jpg
# 특정 시간 (5초)
ffmpeg -i video.mp4 -ss 00:00:05 -vframes 1 thumbnail.jpg
# 고품질
ffmpeg -i video.mp4 -ss 00:00:05 -vframes 1 -q:v 2 thumbnail.jpg
# -q:v 2: 품질 (1=최고, 31=최저)
# 특정 크기
ffmpeg -i video.mp4 -ss 00:00:05 -vf scale=1280:720 -vframes 1 thumbnail.jpg

여러 썸네일 추출

# 1초마다 추출
ffmpeg -i video.mp4 -vf fps=1 thumb_%04d.jpg
# 10초마다 추출
ffmpeg -i video.mp4 -vf fps=1/10 thumb_%04d.jpg
# 특정 구간 (10초~20초, 1초마다)
ffmpeg -i video.mp4 -ss 00:00:10 -to 00:00:20 -vf fps=1 thumb_%04d.jpg

GIF 생성

# 동영상 → GIF
ffmpeg -i video.mp4 -vf "fps=10,scale=480:-1:flags=lanczos" output.gif
# 고품질 GIF (팔레트 생성)
# 1단계: 팔레트 생성
ffmpeg -i video.mp4 -vf "fps=10,scale=480:-1:flags=lanczos,palettegen" palette.png
# 2단계: GIF 생성
ffmpeg -i video.mp4 -i palette.png -filter_complex "fps=10,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse" output.gif
# 특정 구간 (0~5초)
ffmpeg -ss 00:00:00 -t 00:00:05 -i video.mp4 -vf "fps=10,scale=480:-1:flags=lanczos,palettegen" palette.png
ffmpeg -ss 00:00:00 -t 00:00:05 -i video.mp4 -i palette.png -filter_complex "fps=10,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse" output.gif

동영상 편집

자르기 (Trim)

# 시간 기준 자르기
ffmpeg -i input.mp4 -ss 00:00:10 -to 00:00:30 output.mp4
# -ss: 시작 시간
# -to: 종료 시간
# 또는
ffmpeg -i input.mp4 -ss 00:00:10 -t 00:00:20 output.mp4
# -t: 지속 시간 (20초)
# 재인코딩 없이 (빠름, 시작점이 키프레임에 맞춰짐)
ffmpeg -ss 00:00:10 -to 00:00:30 -i input.mp4 -c copy output.mp4
# -ss를 -i 앞에 두면 입력 탐색이라 빠름.
# 이때 -to를 -i 뒤(출력 옵션)에 두면 타임스탬프가 0부터 다시 시작해
# 10~40초 구간이 잘리므로, -to도 -i 앞에 두거나 -t 20을 씁니다

크롭 (Crop)

# 중앙 1280×720 크롭
ffmpeg -i input.mp4 -vf "crop=1280:720" output.mp4
# 특정 위치 크롭
ffmpeg -i input.mp4 -vf "crop=1280:720:0:0" output.mp4
# crop=width:height:x:y
# 16:9 → 1:1 (정사각형, 중앙)
ffmpeg -i input.mp4 -vf "crop=ih:ih" output.mp4
# ih: 입력 높이

회전

# 90도 시계방향
ffmpeg -i input.mp4 -vf "transpose=1" output.mp4
# 90도 반시계방향
ffmpeg -i input.mp4 -vf "transpose=2" output.mp4
# 180도
ffmpeg -i input.mp4 -vf "transpose=1,transpose=1" output.mp4
# 메타데이터로 회전 (재인코딩 없음)
# FFmpeg 6.0 이후에는 rotate 메타데이터 대신 입력 옵션 -display_rotation을 씁니다
ffmpeg -display_rotation 90 -i input.mp4 -c copy output.mp4

병합

동영상 이어붙이기

# 1. 파일 목록 생성
cat > filelist.txt << EOF
file 'video1.mp4'
file 'video2.mp4'
file 'video3.mp4'
EOF
# 2. 병합
ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4
# 재인코딩 포함
ffmpeg -f concat -safe 0 -i filelist.txt -c:v libx264 -crf 23 output.mp4

오디오 추가

# 오디오가 없는 영상에 오디오 추가
# (영상에 오디오가 이미 있으면 자동 선택이 둘 중 하나만 고르므로 -map을 명시)
ffmpeg -i video.mp4 -i audio.mp3 -c:v copy -c:a aac output.mp4
# 기존 오디오 교체
ffmpeg -i video.mp4 -i audio.mp3 -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 output.mp4

스트리밍 및 HLS

HLS 생성

HLS (HTTP Live Streaming)는 Apple이 개발한 스트리밍 프로토콜입니다.

# 기본 HLS
ffmpeg -i input.mp4 \
  -c:v libx264 -c:a aac \
  -hls_time 10 \
  -hls_playlist_type vod \
  -hls_segment_filename "segment_%03d.ts" \
  playlist.m3u8
# 옵션:
# -hls_time 10: 세그먼트 길이 (10초)
# -hls_playlist_type vod: VOD (주문형)
# -hls_segment_filename: 세그먼트 파일명

다중 해상도 (Adaptive Bitrate)

# 1080p
ffmpeg -i input.mp4 \
  -vf scale=1920:1080 -c:v libx264 -b:v 5M -c:a aac -b:a 192k \
  -hls_time 10 -hls_playlist_type vod \
  -hls_segment_filename "1080p_%03d.ts" \
  1080p.m3u8
# 720p
ffmpeg -i input.mp4 \
  -vf scale=1280:720 -c:v libx264 -b:v 3M -c:a aac -b:a 128k \
  -hls_time 10 -hls_playlist_type vod \
  -hls_segment_filename "720p_%03d.ts" \
  720p.m3u8
# 480p
ffmpeg -i input.mp4 \
  -vf scale=854:480 -c:v libx264 -b:v 1.5M -c:a aac -b:a 128k \
  -hls_time 10 -hls_playlist_type vod \
  -hls_segment_filename "480p_%03d.ts" \
  480p.m3u8
# 변형(variant)끼리 세그먼트 경계를 맞추려면 각 명령에 같은 키프레임 간격을 지정합니다
# (예: 30fps에서 -g 60 -keyint_min 60 -sc_threshold 0)
# BANDWIDTH는 오디오를 포함한 최대 비트레이트를 적어야 하므로 실제로는 조금 여유 있게 잡습니다
# Master Playlist 생성
cat > master.m3u8 << EOF
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1280x720
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=854x480
480p.m3u8
EOF

RTMP 스트리밍

# YouTube 라이브
ffmpeg -re -i input.mp4 \
  -c:v libx264 -preset veryfast -b:v 3M -maxrate 3M -bufsize 6M \
  -pix_fmt yuv420p -g 60 -c:a aac -b:a 128k -ar 44100 \
  -f flv rtmp://a.rtmp.youtube.com/live2/YOUR_STREAM_KEY
# Twitch 라이브
ffmpeg -re -i input.mp4 \
  -c:v libx264 -preset veryfast -b:v 3M \
  -c:a aac -b:a 128k \
  -f flv rtmp://live.twitch.tv/app/YOUR_STREAM_KEY
# -re: 실시간 속도로 읽기
# -g 60: GOP 크기 (2초, 30fps 기준)

웹캠 스트리밍

# macOS (웹캠)
ffmpeg -f avfoundation -i "0:0" \
  -c:v libx264 -preset veryfast -b:v 2M \
  -c:a aac -b:a 128k \
  -f flv rtmp://localhost/live/stream
# Linux (웹캠)
ffmpeg -f v4l2 -i /dev/video0 \
  -c:v libx264 -preset veryfast -b:v 2M \
  -f flv rtmp://localhost/live/stream
# Windows (웹캠)
ffmpeg -f dshow -i video="USB Video Device" \
  -c:v libx264 -preset veryfast -b:v 2M \
  -f flv rtmp://localhost/live/stream

필터 및 효과

워터마크

# 이미지 워터마크 (우측 상단)
ffmpeg -i input.mp4 -i logo.png \
  -filter_complex "overlay=W-w-10:10" \
  output.mp4
# 좌측 하단
ffmpeg -i input.mp4 -i logo.png \
  -filter_complex "overlay=10:H-h-10" \
  output.mp4
# 중앙
ffmpeg -i input.mp4 -i logo.png \
  -filter_complex "overlay=(W-w)/2:(H-h)/2" \
  output.mp4
# 투명도 조절
ffmpeg -i input.mp4 -i logo.png \
  -filter_complex "[1:v]format=rgba,colorchannelmixer=aa=0.5[logo];[0:v][logo]overlay=W-w-10:10" \
  output.mp4

텍스트 추가

# 간단한 텍스트
ffmpeg -i input.mp4 \
  -vf "drawtext=text='Hello World':fontsize=48:fontcolor=white:x=10:y=10" \
  output.mp4
# 시간 표시
ffmpeg -i input.mp4 \
  -vf "drawtext=text='%{pts\:hms}':fontsize=32:fontcolor=white:x=10:y=10" \
  output.mp4
# 한글 텍스트 (폰트 지정 필수)
ffmpeg -i input.mp4 \
  -vf "drawtext=text='안녕하세요':fontfile=/System/Library/Fonts/AppleSDGothicNeo.ttc:fontsize=48:fontcolor=white:x=10:y=10" \
  output.mp4
# 배경 추가
ffmpeg -i input.mp4 \
  -vf "drawtext=text='Hello':fontsize=48:fontcolor=white:x=10:y=10:box=1:[email protected]:boxborderw=5" \
  output.mp4

색상 조정

# 밝기 조절
ffmpeg -i input.mp4 -vf "eq=brightness=0.1" output.mp4
# 대비 조절
ffmpeg -i input.mp4 -vf "eq=contrast=1.5" output.mp4
# 채도 조절
ffmpeg -i input.mp4 -vf "eq=saturation=1.5" output.mp4
# 흑백
ffmpeg -i input.mp4 -vf "hue=s=0" output.mp4
# 세피아
ffmpeg -i input.mp4 -vf "colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131" output.mp4

블러 및 샤프닝

# 블러
ffmpeg -i input.mp4 -vf "boxblur=5:1" output.mp4
# 샤프닝
ffmpeg -i input.mp4 -vf "unsharp=5:5:1.0:5:5:0.0" output.mp4
# 특정 시간대(5~10초)에만 전체 화면 블러
ffmpeg -i input.mp4 \
  -vf "boxblur=10:enable='between(t,5,10)'" \
  output.mp4
# 특정 영역만 블러: 해당 영역을 crop해 블러한 뒤 원래 위치에 overlay
ffmpeg -i input.mp4 -filter_complex \
  "[0:v]split[a][b];[b]crop=200:200:100:100,boxblur=10[blur];[a][blur]overlay=100:100" \
  output.mp4

미디어 정보 확인

ffprobe 사용

# 기본 정보
ffprobe input.mp4
# JSON 형식
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4
# 비디오 정보만
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate,bit_rate -of default=noprint_wrappers=1 input.mp4
# 오디오 정보만
ffprobe -v error -select_streams a:0 -show_entries stream=codec_name,sample_rate,channels,bit_rate -of default=noprint_wrappers=1 input.mp4
# 길이 확인
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4

실용적인 정보 추출

# 해상도
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 input.mp4
# 출력: 1920x1080
# 프레임레이트
ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 input.mp4
# 출력: 30/1
# 비트레이트
ffprobe -v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 input.mp4
# 출력: 5000000 (5 Mbps)
# 코덱
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 input.mp4
# 출력: h264

FFmpeg 라이브러리 (libav)

Python (ffmpeg-python)

설치

pip install ffmpeg-python

기본 사용

import ffmpeg
# 동영상 변환
(
    ffmpeg
    .input('input.mp4')
    .output('output.mp4', vcodec='libx264', crf=23)
    .run()
)
# 해상도 변경
(
    ffmpeg
    .input('input.mp4')
    .filter('scale', 1280, 720)
    .output('output.mp4')
    .run()
)
# 썸네일 추출
(
    ffmpeg
    .input('input.mp4', ss=5)
    .output('thumbnail.jpg', vframes=1)
    .run()
)
# 오디오 추출
(
    ffmpeg
    .input('input.mp4')
    .output('audio.mp3', vn=None, acodec='libmp3lame', audio_bitrate='192k')
    .run()
)

고급 예제

import ffmpeg
import sys
from fractions import Fraction
def convert_video(input_file, output_file, resolution='1280:720', crf=23):
    """동영상 변환"""
    try:
        (
            ffmpeg
            .input(input_file)
            .filter('scale', *resolution.split(':'))
            .output(
                output_file,
                vcodec='libx264',
                preset='slow',
                crf=crf,
                acodec='aac',
                audio_bitrate='192k'
            )
            .global_args('-loglevel', 'error')
            .run(capture_stdout=True, capture_stderr=True)
        )
        print(f"✅ Conversion complete: {output_file}")
    except ffmpeg.Error as e:
        print(f"❌ Error: {e.stderr.decode()}", file=sys.stderr)
        raise
def get_video_info(input_file):
    """동영상 정보 추출"""
    try:
        probe = ffmpeg.probe(input_file)
        video_stream = next((s for s in probe['streams'] if s['codec_type'] == 'video'), None)
        audio_stream = next((s for s in probe['streams'] if s['codec_type'] == 'audio'), None)
        
        return {
            'duration': float(probe['format']['duration']),
            'size': int(probe['format']['size']),
            'bitrate': int(probe['format']['bit_rate']),
            'video': {
                'codec': video_stream['codec_name'],
                'width': video_stream['width'],
                'height': video_stream['height'],
                'fps': float(Fraction(video_stream['r_frame_rate']))  # eval 대신 Fraction
            } if video_stream else None,
            'audio': {
                'codec': audio_stream['codec_name'],
                'sample_rate': audio_stream['sample_rate'],
                'channels': audio_stream['channels']
            } if audio_stream else None
        }
    except ffmpeg.Error as e:
        print(f"❌ Error: {e.stderr.decode()}", file=sys.stderr)
        raise
# 사용
info = get_video_info('input.mp4')
print(f"Duration: {info['duration']}s")
print(f"Resolution: {info['video']['width']}x{info['video']['height']}")
print(f"FPS: {info['video']['fps']}")

진행률 표시

import ffmpeg
import sys
def convert_with_progress(input_file, output_file):
    """진행률 표시"""
    # 동영상 길이 확인
    probe = ffmpeg.probe(input_file)
    duration = float(probe['format']['duration'])
    
    # 변환 시작
    process = (
        ffmpeg
        .input(input_file)
        .output(output_file, vcodec='libx264', crf=23)
        .global_args('-progress', 'pipe:1')
        .overwrite_output()
        .run_async(pipe_stdout=True, pipe_stderr=True)
    )
    
    # 진행률 파싱
    for line in process.stdout:
        line = line.decode()
        if line.startswith('out_time_ms='):
            # 이름과 달리 out_time_ms 값은 마이크로초 단위입니다 (FFmpeg의 오래된 특이점)
            value = line.split('=')[1].strip()
            if value == 'N/A':
                continue
            time_s = int(value) / 1000000.0
            progress = (time_s / duration) * 100
            
            sys.stdout.write(f"\rProgress: {progress:.1f}%")
            sys.stdout.flush()
    
    process.wait()
    print("\n✅ Complete!")
convert_with_progress('input.mp4', 'output.mp4')

Node.js (fluent-ffmpeg)

설치

npm install fluent-ffmpeg

기본 사용

const ffmpeg = require('fluent-ffmpeg');
// 동영상 변환
ffmpeg('input.mp4')
  .output('output.mp4')
  .videoCodec('libx264')
  .audioCodec('aac')
  .on('end', () => console.log('✅ Complete!'))
  .on('error', (err) => console.error('❌ Error:', err))
  .run();
// 해상도 변경
ffmpeg('input.mp4')
  .size('1280x720')
  .output('output.mp4')
  .run();
// 썸네일 추출
ffmpeg('input.mp4')
  .screenshots({
    timestamps: ['00:00:05'],
    filename: 'thumbnail.jpg',
    folder: './thumbnails'
  });

고급 예제

const ffmpeg = require('fluent-ffmpeg');
const path = require('path');
// 동영상 정보 추출
function getVideoInfo(inputPath) {
  return new Promise((resolve, reject) => {
    ffmpeg.ffprobe(inputPath, (err, metadata) => {
      if (err) return reject(err);
      
      const videoStream = metadata.streams.find(s => s.codec_type === 'video');
      const audioStream = metadata.streams.find(s => s.codec_type === 'audio');
      
      resolve({
        duration: metadata.format.duration,
        size: metadata.format.size,
        bitrate: metadata.format.bit_rate,
        video: videoStream ? {
          codec: videoStream.codec_name,
          width: videoStream.width,
          height: videoStream.height,
          fps: (([n, d]) => Number(n) / Number(d))(videoStream.r_frame_rate.split('/'))
        } : null,
        audio: audioStream ? {
          codec: audioStream.codec_name,
          sampleRate: audioStream.sample_rate,
          channels: audioStream.channels
        } : null
      });
    });
  });
}
// 진행률 표시
function convertWithProgress(inputPath, outputPath) {
  return new Promise((resolve, reject) => {
    ffmpeg(inputPath)
      .output(outputPath)
      .videoCodec('libx264')
      .videoBitrate('5M')
      .audioCodec('aac')
      .audioBitrate('192k')
      .on('start', (cmd) => {
        console.log('Starting:', cmd);
      })
      .on('progress', (progress) => {
        console.log(`Progress: ${progress.percent?.toFixed(1)}%`);
      })
      .on('end', () => {
        console.log('✅ Complete!');
        resolve();
      })
      .on('error', (err) => {
        console.error('❌ Error:', err);
        reject(err);
      })
      .run();
  });
}
// HLS 생성
function createHLS(inputPath, outputDir) {
  return new Promise((resolve, reject) => {
    ffmpeg(inputPath)
      .outputOptions([
        '-c:v libx264',
        '-c:a aac',
        '-hls_time 10',
        '-hls_playlist_type vod',
        '-hls_segment_filename', path.join(outputDir, 'segment_%03d.ts')
      ])
      .output(path.join(outputDir, 'playlist.m3u8'))
      .on('end', resolve)
      .on('error', reject)
      .run();
  });
}
// 사용
(async () => {
  const info = await getVideoInfo('input.mp4');
  console.log('Video Info:', info);
  
  await convertWithProgress('input.mp4', 'output.mp4');
  await createHLS('input.mp4', './hls');
})();

Go (ffmpeg-go)

설치

go get github.com/u2takey/ffmpeg-go

기본 사용

package main
import (
    "fmt"
    ffmpeg "github.com/u2takey/ffmpeg-go"
)
func main() {
    // 동영상 변환
    err := ffmpeg.Input("input.mp4").
        Output("output.mp4", ffmpeg.KwArgs{
            "c:v": "libx264",
            "crf": 23,
        }).
        Run()
    
    if err != nil {
        panic(err)
    }
    
    fmt.Println("✅ Complete!")
}
// 해상도 변경
func resizeVideo(input, output string) error {
    return ffmpeg.Input(input).
        Filter("scale", ffmpeg.Args{"1280:720"}).
        Output(output).
        Run()
}
// 썸네일 추출
func extractThumbnail(input, output string, timestamp float64) error {
    return ffmpeg.Input(input, ffmpeg.KwArgs{"ss": timestamp}).
        Output(output, ffmpeg.KwArgs{"vframes": 1}).
        Run()
}
// 오디오 추출
func extractAudio(input, output string) error {
    return ffmpeg.Input(input).
        Output(output, ffmpeg.KwArgs{
            "vn":   "",
            "c:a":  "libmp3lame",
            "b:a":  "192k",
        }).
        Run()
}

실전 프로젝트

프로젝트 1: 동영상 업로드 서버

Node.js + Express

const express = require('express');
const multer = require('multer');
const ffmpeg = require('fluent-ffmpeg');
const path = require('path');
const fs = require('fs').promises;
const app = express();
const upload = multer({ dest: 'uploads/' });
// 동영상 업로드 및 처리
app.post('/upload', upload.single('video'), async (req, res) => {
  const inputPath = req.file.path;
  const outputDir = path.join('processed', req.file.filename);
  
  try {
    await fs.mkdir(outputDir, { recursive: true });
    
    // 1. 썸네일 생성
    await new Promise((resolve, reject) => {
      ffmpeg(inputPath)
        .screenshots({
          timestamps: ['00:00:01'],
          filename: 'thumbnail.jpg',
          folder: outputDir
        })
        .on('end', resolve)
        .on('error', reject);
    });
    
    // 2. 다중 해상도 변환
    const resolutions = [
      { name: '1080p', size: '1920x1080', bitrate: '5M' },
      { name: '720p', size: '1280x720', bitrate: '3M' },
      { name: '480p', size: '854x480', bitrate: '1.5M' }
    ];
    
    await Promise.all(resolutions.map(res => {
      return new Promise((resolve, reject) => {
        ffmpeg(inputPath)
          .size(res.size)
          .videoBitrate(res.bitrate)
          .output(path.join(outputDir, `${res.name}.mp4`))
          .on('end', resolve)
          .on('error', reject)
          .run();
      });
    }));
    
    // 3. HLS 생성
    await new Promise((resolve, reject) => {
      ffmpeg(inputPath)
        .outputOptions([
          '-c:v libx264',
          '-c:a aac',
          '-hls_time 10',
          '-hls_playlist_type vod',
          '-hls_segment_filename', path.join(outputDir, 'segment_%03d.ts')
        ])
        .output(path.join(outputDir, 'playlist.m3u8'))
        .on('end', resolve)
        .on('error', reject)
        .run();
    });
    
    // 4. 원본 파일 삭제
    await fs.unlink(inputPath);
    
    res.json({
      success: true,
      thumbnail: `/processed/${req.file.filename}/thumbnail.jpg`,
      videos: resolutions.map(r => ({
        quality: r.name,
        url: `/processed/${req.file.filename}/${r.name}.mp4`
      })),
      hls: `/processed/${req.file.filename}/playlist.m3u8`
    });
    
  } catch (error) {
    console.error('Processing error:', error);
    res.status(500).json({ error: 'Processing failed' });
  }
});
app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

프로젝트 2: 동영상 스트리밍 서버

Python + Flask

from flask import Flask, request, send_file, jsonify
import ffmpeg
import os
import uuid
app = Flask(__name__)
UPLOAD_FOLDER = 'uploads'
PROCESSED_FOLDER = 'processed'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(PROCESSED_FOLDER, exist_ok=True)
@app.route('/upload', methods=['POST'])
def upload_video():
    if 'video' not in request.files:
        return jsonify({'error': 'No video file'}), 400
    
    file = request.files['video']
    video_id = str(uuid.uuid4())
    input_path = os.path.join(UPLOAD_FOLDER, f"{video_id}.mp4")
    output_dir = os.path.join(PROCESSED_FOLDER, video_id)
    
    os.makedirs(output_dir, exist_ok=True)
    file.save(input_path)
    
    try:
        # 1. 동영상 정보 추출
        probe = ffmpeg.probe(input_path)
        duration = float(probe['format']['duration'])
        
        # 2. 썸네일 생성 (3개)
        timestamps = [duration * 0.25, duration * 0.5, duration * 0.75]
        for i, ts in enumerate(timestamps):
            (
                ffmpeg
                .input(input_path, ss=ts)
                .output(os.path.join(output_dir, f'thumb_{i}.jpg'), vframes=1)
                .overwrite_output()
                .run(capture_stdout=True, capture_stderr=True)
            )
        
        # 3. HLS 생성 (다중 해상도)
        resolutions = [
            {'name': '1080p', 'width': 1920, 'height': 1080, 'bitrate': '5M'},
            {'name': '720p', 'width': 1280, 'height': 720, 'bitrate': '3M'},
            {'name': '480p', 'width': 854, 'height': 480, 'bitrate': '1.5M'}
        ]
        
        for res in resolutions:
            res_dir = os.path.join(output_dir, res['name'])
            os.makedirs(res_dir, exist_ok=True)
            
            (
                ffmpeg
                .input(input_path)
                .output(
                    os.path.join(res_dir, 'playlist.m3u8'),
                    **{
                        'c:v': 'libx264',
                        'b:v': res['bitrate'],
                        'c:a': 'aac',
                        'b:a': '128k',
                        'vf': f"scale={res['width']}:{res['height']}",
                        'hls_time': 10,
                        'hls_playlist_type': 'vod',
                        'hls_segment_filename': os.path.join(res_dir, 'segment_%03d.ts')
                    }
                )
                .overwrite_output()
                .run(capture_stdout=True, capture_stderr=True)
            )
        
        # 4. Master Playlist 생성
        master_playlist = "#EXTM3U\n"
        for res in resolutions:
            bandwidth = int(float(res['bitrate'].replace('M', '')) * 1000000)  # '1.5M' 처리
            master_playlist += f"#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},RESOLUTION={res['width']}x{res['height']}\n"
            master_playlist += f"{res['name']}/playlist.m3u8\n"
        
        with open(os.path.join(output_dir, 'master.m3u8'), 'w') as f:
            f.write(master_playlist)
        
        # 5. 원본 삭제
        os.remove(input_path)
        
        return jsonify({
            'video_id': video_id,
            'duration': duration,
            'thumbnails': [f'/processed/{video_id}/thumb_{i}.jpg' for i in range(3)],
            'hls': f'/processed/{video_id}/master.m3u8'
        })
        
    except ffmpeg.Error as e:
        return jsonify({'error': e.stderr.decode()}), 500
@app.route('/processed/<path:filename>')
def serve_processed(filename):
    return send_file(os.path.join(PROCESSED_FOLDER, filename))
if __name__ == '__main__':
    app.run(debug=True, port=5000)

프로젝트 3: 일괄 변환 도구

Python CLI

#!/usr/bin/env python3
import ffmpeg
import argparse
import os
import sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
def convert_video(input_path, output_dir, resolution, crf, preset):
    """단일 동영상 변환"""
    input_name = Path(input_path).stem
    output_path = os.path.join(output_dir, f"{input_name}_{resolution}.mp4")
    
    try:
        width, height = resolution.split('x')
        
        (
            ffmpeg
            .input(input_path)
            .filter('scale', width, height)
            .output(
                output_path,
                vcodec='libx264',
                preset=preset,
                crf=crf,
                acodec='aac',
                audio_bitrate='192k'
            )
            .global_args('-loglevel', 'error')
            .overwrite_output()
            .run(capture_stdout=True, capture_stderr=True)
        )
        
        return f"✅ {input_name} → {output_path}"
    
    except ffmpeg.Error as e:
        return f"❌ {input_name}: {e.stderr.decode()}"
def batch_convert(input_dir, output_dir, resolution='1280x720', crf=23, preset='medium', workers=4):
    """일괄 변환"""
    os.makedirs(output_dir, exist_ok=True)
    
    # 동영상 파일 찾기
    video_extensions = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv']
    video_files = []
    
    for ext in video_extensions:
        video_files.extend(Path(input_dir).glob(f'*{ext}'))
        video_files.extend(Path(input_dir).glob(f'*{ext.upper()}'))
    
    if not video_files:
        print(f"No video files found in {input_dir}")
        return
    
    print(f"Found {len(video_files)} videos")
    print(f"Resolution: {resolution}, CRF: {crf}, Preset: {preset}")
    print(f"Workers: {workers}\n")
    
    # 병렬 처리
    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = {
            executor.submit(convert_video, str(f), output_dir, resolution, crf, preset): f
            for f in video_files
        }
        
        for future in as_completed(futures):
            result = future.result()
            print(result)
    
    print(f"\n✅ All conversions complete!")
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Batch video converter')
    parser.add_argument('input_dir', help='Input directory')
    parser.add_argument('output_dir', help='Output directory')
    parser.add_argument('--resolution', default='1280x720', help='Output resolution (default: 1280x720)')
    parser.add_argument('--crf', type=int, default=23, help='CRF value (default: 23)')
    parser.add_argument('--preset', default='medium', help='Encoding preset (default: medium)')
    parser.add_argument('--workers', type=int, default=4, help='Number of parallel workers (default: 4)')
    
    args = parser.parse_args()
    
    batch_convert(
        args.input_dir,
        args.output_dir,
        resolution=args.resolution,
        crf=args.crf,
        preset=args.preset,
        workers=args.workers
    )

사용

# 기본
python batch_convert.py ./videos ./output
# 720p, CRF 20, slow 프리셋
python batch_convert.py ./videos ./output --resolution 1280x720 --crf 20 --preset slow
# 8개 병렬 처리
python batch_convert.py ./videos ./output --workers 8

고급 필터

복잡한 필터 체인

# 여러 필터 적용
ffmpeg -i input.mp4 \
  -vf "scale=1280:720,eq=brightness=0.1:contrast=1.2,unsharp=5:5:1.0" \
  output.mp4
# 필터 그래프
ffmpeg -i input.mp4 -i logo.png \
  -filter_complex "\
    [0:v]scale=1280:720[scaled];\
    [scaled][1:v]overlay=W-w-10:10[watermarked];\
    [watermarked]drawtext=text='Hello':fontsize=48:x=10:y=10\
  " \
  output.mp4

화면 분할

# 2개 동영상 나란히 (Side by Side)
ffmpeg -i left.mp4 -i right.mp4 \
  -filter_complex "\
    [0:v]scale=960:1080[left];\
    [1:v]scale=960:1080[right];\
    [left][right]hstack\
  " \
  output.mp4
# 4개 동영상 (2×2 그리드)
ffmpeg -i top_left.mp4 -i top_right.mp4 -i bottom_left.mp4 -i bottom_right.mp4 \
  -filter_complex "\
    [0:v]scale=960:540[tl];\
    [1:v]scale=960:540[tr];\
    [2:v]scale=960:540[bl];\
    [3:v]scale=960:540[br];\
    [tl][tr]hstack[top];\
    [bl][br]hstack[bottom];\
    [top][bottom]vstack\
  " \
  output.mp4

슬로우 모션 / 타임랩스

# 슬로우 모션 (0.5배 속도)
ffmpeg -i input.mp4 -filter:v "setpts=2.0*PTS" output.mp4
# 타임랩스 (2배 속도)
ffmpeg -i input.mp4 -filter:v "setpts=0.5*PTS" output.mp4
# 오디오 포함 (속도 동기화)
ffmpeg -i input.mp4 \
  -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" \
  -map "[v]" -map "[a]" \
  output.mp4

트랜지션 효과

# 페이드 인/아웃
ffmpeg -i input.mp4 \
  -vf "fade=t=in:st=0:d=3,fade=t=out:st=57:d=3" \
  output.mp4
# 페이드 아웃 후 페이드 인으로 이어 붙이기 (검은 화면을 거침)
ffmpeg -i video1.mp4 -i video2.mp4 \
  -filter_complex "\
    [0:v]fade=t=out:st=7:d=1[v0];\
    [1:v]fade=t=in:st=0:d=1[v1];\
    [v0][v1]concat=n=2:v=1:a=0\
  " \
  output.mp4
# 진짜 크로스페이드 (두 영상이 겹쳐서 전환, FFmpeg 4.3+의 xfade)
# offset은 첫 영상에서 전환이 시작되는 시각(8초 영상이면 7), 두 입력은 해상도·프레임레이트가 같아야 함
ffmpeg -i video1.mp4 -i video2.mp4 \
  -filter_complex "[0:v][1:v]xfade=transition=fade:duration=1:offset=7" \
  output.mp4

FFmpeg 심화 — libav·필터 그래프·HW·매핑·프로덕션

이 절에서는 명령줄 너머의 내부 데이터 흐름과, 서비스 환경에서 자주 맞닥뜨리는 필터 그래프·하드웨어 파이프라인·스트림 선택을 엔지니어링 관점에서 정리합니다. 앞선 «기본 명령 구조» 절의 파이프라인 설명을 보강하는 목적입니다.

Libav 계열 아키텍처: 디먹서 → 디코더 → 필터 → 인코더 → 먹서

FFmpeg 실행 파일은 libavformat, libavcodec, libavfilter, libavutil, 필요 시 libswscale·libswresample을 조합해 동작합니다. 한 줄의 ffmpeg 명령은 내부적으로 다음과 같은 책임 분리를 따릅니다.

디먹서(Demuxer, libavformat)는 컨테이너를 읽어 스트림 단위 메타데이터와 압축된 패킷(AVPacket)을 생성합니다. MP4의 moov/mdat, MPEG-TS의 PID·PAT/PMT처럼 포맷별 파서가 다르지만, 상위 파이프라인은 동일하게 «스트림 인덱스 → 패킷 큐»로 추상화됩니다.

디코더(Decoder, libavcodec)는 패킷을 디코딩된 프레임(AVFrame) 또는 오디오 샘플 버퍼로 바꿉니다. 여기서 픽셀 포맷(yuv420p, nv12 등)·색역·타임스탬프(PTS/DTS)가 확정되며, 이후 필터·인코더는 이 표현에 맞춰 동작합니다.

필터(Filter graph, libavfilter)는 디코더 출력을 그대로 인코더에 넣지 않고 가공할 때 개입합니다. -vf/-af는 단일 입력·단일 출력 선형 체인이며, -filter_complex는 여러 입력 패드와 분기·합성을 표현합니다. 필터는 프레임 단위로 연결되므로, 해상도·픽셀 포맷·샘플레이트가 어긋나면 FFmpeg가 자동으로 삽입하는 변환 필터(format, fps, aformat 등)가 끼어들 수 있습니다. 예상과 다른 그래프가 만들어졌을 때는 -loglevel debug로 «자동 삽입» 로그를 확인하는 것이 좋습니다.

인코더(Encoder)는 필터 출력 프레임을 다시 압축 패킷으로 만듭니다. 소프트웨어 인코더(libx264 등)와 하드웨어 인코더(NVENC/VAAPI 등)는 입력 버퍼의 메모리 종류(시스템 메모리 vs GPU 서페이스)에서부터 차이가 납니다.

먹서(Muxer)는 패킷을 타임스탬프 순으로 인터리브하며, 컨테이너별 인덱스·헤더를 기록합니다. 웹 배포에서 자주 쓰는 -movflags +faststart는 먹서 단계에서 moov 위치를 조정하는 전형적인 예입니다.

비트스트림 필터(Bitstream filter)는 «디코딩 없이」 바이트열을 변형할 때 씁니다. 예를 들어 MP4에 들어 있는 H.264/HEVC를 Annex B 형태로 꺼내 TS에 실을 때 h264_mp4toannexb, hevc_mp4toannexb가 등장합니다. 패킷 경계와 NAL 구조를 이해하고 있을 때만 안전하게 적용할 수 있습니다.

필터 그래프 처리: 패드, 스케줄링, 병렬

-filter_complex 문자열은 내부적으로 방향 그래프(DAG)로 컴파일됩니다. [0:v]scale=1280:720[v1]처럼 레이블된 패드는 노드의 입·출력 핀에 해당합니다. split, overlay, xfade, concat처럼 다입력·다출력 필터를 쓸 때는 패드 연결을 빠뜨리지 않았는지가 오류의 대부분입니다.

오디오·비디오 동시 처리에서는 [0:v]…[v], [0:a]…[a]를 만든 뒤 -map "[v]" -map "[a]"로 출력 스트림에 묶는 패턴이 표준적입니다. 단순 -vf만 쓰면 비디오 체인만 바뀌고 오디오는 입력 그대로라 싱크가 어긋난 것처럼 보이는 경우가 있으므로, 속도 변경·트림을 할 때는 오디오 atempo·asetpts까지 한 그래프에서 설계하는 것이 안전합니다.

-filter_threads: 일부 필터는 내부적으로 스레드를 쓰며, 입력 해상도가 크면 CPU 병목이 됩니다. 반대로 스레드를 과하게 늘리면 스레드 간 동기화와 캐시 경합 비용이 커지므로, 대표 입력으로 값을 바꿔 가며 확인하는 편이 좋습니다.

하드웨어와 필터의 접점: CPU 메모리에 있는 프레임을 GPU 인코더로 보내려면 hwupload/hwupload_cuda 등으로 업로드해야 하며, GPU 디코더가 만든 서페이스를 CPU 필터(scale 등)로 내리려면 hwdownload가 필요합니다. «전 구간 GPU»를 기대하고도 중간에 CPU scale이 끼면 다운로드·재업로드 비용으로 이득이 사라지는 경우가 많습니다.

# 예: CUDA 디코더 출력을 유지한 채 GPU 쪽에서 크기 조정 후 NVENC로 인코딩
# (빌드·드라이버에 따라 사용 가능한 필터 이름이 다를 수 있음)
ffmpeg -hwaccel cuda -hwaccel_output_format cuda -i input.mp4 \
  -vf "scale_cuda=1280:720" \
  -c:v h264_nvenc -preset p4 -cq 23 \
  -c:a copy output.mp4

하드웨어 가속 API: VAAPI(리눅스)와 NVENC(NVIDIA)

공통 원칙: 하드웨어 가속의 이득은 «디코드·처리·인코드가 같은 메모리 공간에서 이어질 때» 극대화됩니다. 디코드만 GPU, 필터는 CPU, 인코드만 GPU처럼 섞이면 PCIe 대역·포맷 변환이 병목이 됩니다.

VAAPI (Video Acceleration API, 주로 Linux·Intel/AMD)

Intel·AMD GPU가 있는 리눅스 서버에서 흔히 사용됩니다. 디바이스 노드는 보통 /dev/dri/renderD128이며, 컨테이너·클라우드 VM에서는 render 노드 노출 여부가 먼저 확인되어야 합니다.

# 사용 가능한 VAAPI 디바이스 확인 (환경에 따라 패키지 필요)
vainfo

# VAAPI로 H.264 인코딩 (예시 — 드라이버·FFmpeg 빌드에 따라 옵션명이 다를 수 있음)
ffmpeg -init_hw_device vaapi=va:/dev/dri/renderD128 -filter_hw_device va \
  -hwaccel vaapi -hwaccel_output_format vaapi -i input.mp4 \
  -vf 'format=nv12|vaapi,hwupload' -c:v h264_vaapi -qp 23 \
  -c:a copy output.mp4

실무 체크포인트: Wayland/X11과 무관하게 헤드리스 서버에서 VAAPI를 쓰려면 권한(video 그룹)·i965/iris/mesa 드라이버 버전·FFmpeg의 --enable-vaapi 빌드가 맞아야 합니다. 실패 시 -loglevel verbose에서 vaapi 초기화 오류 메시지를 우선 확인합니다.

NVENC (NVIDIA)

NVIDIA GPU의 전용 인코더입니다. 디코딩은 -hwaccel cuda 또는 구형 파이프라인의 cuvid(빌드에 포함된 경우)를 쓰며, 인코딩은 h264_nvenc/hevc_nvenc/av1_nvenc 등으로 지정합니다. 품질 모드는 세대별로 -cq, -rc vbr, -preset p1~p7 등 표기가 나뉘어 와서 드라이버 릴리스 노트와 ffmpeg -h encoder=h264_nvenc 출력을 기준으로 잡는 것이 안전합니다.

# NVENC VBR + 품질 상한 예시 (환경에 따라 인식되지 않는 옵션은 제거)
ffmpeg -hwaccel cuda -i input.mp4 \
  -c:v h264_nvenc -preset p4 -rc vbr -cq 23 -b:v 5M -maxrate 8M -bufsize 16M \
  -c:a aac -b:a 192k output.mp4

NVENC와 VAAPI 선택: 데이터센터 GPU가 있으면 NVENC가 처리량에서 유리한 경우가 많고, CPU 내장 GPU·오픈소스 스택 위주면 VAAPI가 자연스럽습니다. 동시 세션 수는 NVENC 세대별 인코더 세션 제한(제품·드라이버에 따라 상이)이 병목이 되므로, 트랜스코딩 팜 설계 시 세션 수·지연·전력을 함께 적습니다.

스트림 매핑·메타데이터·챕터

-map은 «어떤 입력의 어떤 스트림이 출력의 몇 번째 트랙이 될지」를 명시합니다. 다국어 오디오·자막·여러 각도 영상이 있는 방송·블루레이 계열 소스에서 필수입니다. -map 0:v:0 -map 0:a:2처럼 인덱스는 0부터이며, -map 없이 -c copy만 쓰면 유형마다 스트림 하나씩(해상도가 가장 높은 비디오, 채널이 가장 많은 오디오 등)만 자동 선택되므로 의도한 트랙이 빠질 수 있습니다.

# 모든 스트림을 최대한 보존해 복사 (포맷이 허용할 때)
ffmpeg -i input.mkv -map 0 -c copy output.mkv

# 메타데이터는 컨테이너·스트림 단위로 복사 또는 재작성
ffmpeg -i input.mp4 -map_metadata 0 -map_chapters 0 -c copy with_meta.mp4
ffmpeg -i input.mp4 -c copy \
  -metadata title="제목" -metadata author="작성자" \
  -metadata:s:a:0 language=kor \
  tagged.mp4

-disposition은 트랙의 기본 역할(강제 자막·청각 장애인용 보조 등)을 지정할 때 씁니다. 플레이어·스트리밍 규격이 이 값을 해석하므로, 배포 전 타깃 플랫폼 요구사항과 맞추는 것이 좋습니다.

챕터: ffprobe -show_chapters로 확인한 뒤, -map_chapters로 복사하거나 외부 파일에서 주입할 수 있습니다. 전자는 빠르고, 후자는 편집 도구와의 워크플로에 맞춰 선택합니다.

ffprobe -v error -show_chapters -show_entries format_tags=title -of json input.mp4

프로덕션에서 통하는 FFmpeg 패턴

실패를 줄이는 습관: 배치·큐 워커에서는 -nostdin으로 표준 입력이 우연히 소비되는 상황을 막으며, -hide_banner -loglevel error(필요 시 -stats)로 로그 노이즈를 제어합니다. 재현 가능한 로그를 남기려면 FFmpeg 버전 문자열·전체 명령줄·입력 파일 해시를 함께 저장합니다.

진행률·모니터링: -progress url 또는 파이프로 키·값 형태의 진행 정보를 뽑아 워커가 파싱하기 쉽게 합니다. 대규모 클러스터에서는 이 값을 메트릭으로 넘겨 스톨·큐 적체를 감시합니다.

리소스: HLS·DASH 세그먼트를 만드는 워커는 동시 오픈 파일 수와 디스크 fsync 정책에 걸리기 쉽습니다. 로컬 SSD·tmpfs·세그먼트별 디렉터리 샤딩이 자주 사용됩니다.

입력 검증: 인코딩 전 ffprobe로 코덱·해상도·HDR 메타·가변 프레임레이트 여부를 확인하며, 이상이 있으면 전처리 필터(fps 고정, 색공간 태그 보정)를 명시적으로 넣습니다. «한 번에 긴 명령»보다 프로브 → 조건부 인코더 선택이 운영에 유리합니다.

하드웨어 파이프라인 검증: GPU 인코딩은 동일 설정이라도 드라이버 버전에 따라 화질·파일 크기가 달라질 수 있습니다. 릴리스마다 소수의 골든 샘플에 대해 VMAF·SSIM 또는 육안 검사를 자동화해 두면 회귀를 빨리 잡을 수 있습니다.

보안·견고성: 사용자가 업로드한 임의 파일명·임의 경로를 그대로 쉘에 넣지 말고, 인자 배열으로 FFmpeg를 호출합니다. 서버에서는 입력 길이·해상도 상한, 무한 필터 체인 같은 DoS성 입력에 대한 제한을 두는 것이 좋습니다.

# 배치 워커에서 흔한 «조용한» 래퍼 예시
ffmpeg -nostdin -hide_banner -loglevel error -stats \
  -i "$INPUT" -c:v libx264 -preset medium -crf 23 -c:a aac -b:a 192k \
  -movflags +faststart "$OUTPUT"

이 절의 내용은 아래 «성능 최적화» 절의 NVENC·QSV·AMF 실전 예제와 겹치지 않게 API·파이프라인 관점을 보강한 것입니다. 운영 환경(드라이버, FFmpeg 빌드 플래그)마다 최적 옵션이 달라지므로, 배포 전 ffmpeg -version과 -h encoder=… 출력을 기준선으로 삼으면 됩니다.


성능 최적화

하드웨어 가속 - GPU의 힘 활용하기

왜 하드웨어 가속이 필요한가?

CPU 인코딩 (libx264):
- 범용 프로세서로 소프트웨어 연산
- 높은 품질, 최적 압축률
- 속도: 프리셋과 CPU 코어 수에 크게 좌우됨

GPU 인코딩 (h264_nvenc):
- GPU에 들어 있는 전용 인코더 회로(NVENC)를 사용 (CUDA 코어로 인코딩하는 것이 아님)
- CPU를 거의 쓰지 않고 빠르게 인코딩
- 같은 비트레이트에서 화질은 느린 x264 프리셋보다 대개 조금 낮음
- 같은 화질을 맞추려면 파일이 더 커지는 경향

언제 GPU를 쓸까?
✅ 실시간 스트리밍 (Twitch, YouTube Live)
✅ 대량 파일 변환 (시간이 중요)
✅ 웹캠 녹화 (CPU 부하 최소화)
❌ 최종 아카이브 (최고 품질 필요)
❌ 저사양 서버 (GPU 없음)

NVIDIA GPU (NVENC)

# GPU 사용 가능 여부 확인
ffmpeg -hwaccels
# 출력에 cuda가 있으면 사용 가능

ffmpeg -encoders | grep nvenc
# h264_nvenc: H.264 NVENC 인코더
# hevc_nvenc: H.265 NVENC 인코더

# H.264 GPU 인코딩
ffmpeg -hwaccel cuda -i input.mp4 \
  -c:v h264_nvenc -preset fast -b:v 5M \
  output.mp4

# 옵션 상세:
# -hwaccel cuda: CUDA 디코딩 가속 (디코딩도 GPU 사용)
# -c:v h264_nvenc: NVIDIA H.264 인코더
# -preset: 최근 FFmpeg·드라이버에서는 p1(가장 빠름)~p7(가장 느림, 고품질)을 쓰고
#   fast·slow 같은 예전 이름은 호환용 별칭으로 남아 있습니다
#   정확한 목록은 ffmpeg -h encoder=h264_nvenc 로 확인

# H.265 GPU 인코딩 (4K에 적합)
ffmpeg -hwaccel cuda -i input.mp4 \
  -c:v hevc_nvenc -preset fast -b:v 5M \
  output.mp4

# 품질 설정 (CQP - Constant Quantization Parameter)
ffmpeg -hwaccel cuda -i input.mp4 \
  -c:v h264_nvenc -preset slow -cq 23 \
  output.mp4
# -cq: 품질 목표값 (값이 낮을수록 고품질, 0-51)
#   -rc vbr와 함께 쓰는 것이 일반적이며, libx264의 CRF와 같은 숫자가 같은 화질을 뜻하지는 않습니다

# 2-Pass GPU 인코딩 (최고 품질)
ffmpeg -hwaccel cuda -i input.mp4 \
  -c:v h264_nvenc -preset slow -b:v 5M -multipass fullres \
  output.mp4
# -multipass fullres: GPU 2-pass (품질 향상)

Intel GPU (QSV - Quick Sync Video)

# QSV 지원 확인
ffmpeg -encoders | grep qsv
# h264_qsv: Intel H.264 인코더
# hevc_qsv: Intel H.265 인코더

# H.264 QSV 인코딩
ffmpeg -hwaccel qsv -i input.mp4 \
  -c:v h264_qsv -preset fast -global_quality 23 \
  output.mp4

# -global_quality: QSV의 품질 파라미터
#   값이 낮을수록 고품질 (CRF와 유사)
#   범위: 1-51

# H.265 QSV 인코딩
ffmpeg -hwaccel qsv -i input.mp4 \
  -c:v hevc_qsv -preset veryslow -global_quality 23 \
  output.mp4

# 해상도 변경 (GPU에서 스케일링)
ffmpeg -hwaccel qsv -i input.mp4 \
  -vf scale_qsv=1280:720 -c:v h264_qsv -global_quality 23 \
  output.mp4
# scale_qsv: GPU 스케일링 (CPU 스케일보다 빠름)

AMD GPU (AMF - Advanced Media Framework)

# AMF 지원 확인
ffmpeg -encoders | grep amf
# h264_amf: AMD H.264 인코더
# hevc_amf: AMD H.265 인코더

# H.264 AMF 인코딩
ffmpeg -i input.mp4 \
  -c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23 \
  output.mp4

# -quality: 인코딩 품질 모드
#   - speed: 빠름, 낮은 품질
#   - balanced: 균형 (권장)
#   - quality: 느림, 높은 품질

# -rc: 레이트 컨트롤 모드
#   - cqp: Constant QP (CRF와 유사)
#   - vbr: Variable Bitrate
#   - cbr: Constant Bitrate (스트리밍용)

# -qp_i / -qp_p / -qp_b: 프레임 유형별 Quantization Parameter (0-51)

# H.265 AMF 인코딩
ffmpeg -i input.mp4 \
  -c:v hevc_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23 \
  output.mp4

macOS (VideoToolbox) - Apple Silicon/Intel Mac

# VideoToolbox 지원 확인
ffmpeg -encoders | grep videotoolbox
# h264_videotoolbox: Apple H.264 인코더
# hevc_videotoolbox: Apple H.265 인코더

# H.264 VideoToolbox 인코딩
ffmpeg -i input.mp4 \
  -c:v h264_videotoolbox -b:v 5M \
  output.mp4

# H.265 VideoToolbox 인코딩 (Apple Silicon 권장)
ffmpeg -i input.mp4 \
  -c:v hevc_videotoolbox -b:v 5M \
  output.mp4

# 품질 설정 (q:v)
ffmpeg -i input.mp4 \
  -c:v h264_videotoolbox -q:v 65 \
  output.mp4
# q:v 범위: 1-100 (높을수록 고품질)
# 고정 품질 모드는 Apple Silicon에서만 지원되며, 그 외 환경에서는 -b:v로 비트레이트를 지정합니다

하드웨어 인코더를 고를 때

- 속도: 전용 인코더 회로를 쓰므로 CPU 인코딩보다 대개 훨씬 빠르고, CPU를 다른 작업에 남겨 둡니다
- 품질: 같은 비트레이트라면 libx264의 느린 프리셋이 대개 조금 우수합니다
- 세대 차이: 같은 NVENC·QSV라도 GPU 세대에 따라 화질과 지원 코덱(HEVC, AV1)이 다릅니다
- 결론: 실시간 스트리밍이나 대량 변환은 하드웨어 인코더, 최종 배포본은 CPU 인코더가 흔한 조합입니다
- 실제 차이는 자신의 소스와 장비로 직접 비교하세요 (아래 "인코딩 속도 직접 비교하기" 절의 스크립트 참고)

하드웨어 가속 주의사항

# ❌ 잘못된 사용: 디코딩만 GPU, 인코딩은 CPU
ffmpeg -hwaccel cuda -i input.mp4 -c:v libx264 output.mp4
# 효과: 거의 없음 (디코딩은 빠른데 인코딩이 느림)

# ✅ 디코딩 + 인코딩 모두 GPU, 프레임도 GPU 메모리에 유지
ffmpeg -hwaccel cuda -hwaccel_output_format cuda -i input.mp4 -c:v h264_nvenc output.mp4
# -hwaccel_output_format cuda가 없으면 디코딩된 프레임이 시스템 메모리로 복사됐다가
# 인코더로 다시 업로드되어 PCIe 왕복 비용이 생깁니다

# 드라이버가 FFmpeg 빌드가 요구하는 NVENC API 버전보다 오래되면 인코더 초기화가 실패합니다
# 해결: 드라이버 업데이트
# NVIDIA: https://www.nvidia.com/drivers
# Intel: https://www.intel.com/content/www/us/en/download-center/home.html

하드웨어 인코더 선택 가이드

사용할 인코더는 대부분 가진 하드웨어가 정합니다. NVIDIA GPU가 있으면 NVENC(h264_nvenc, hevc_nvenc, 최신 세대는 av1_nvenc), Intel CPU 내장 그래픽이나 Arc GPU가 있으면 QSV(h264_qsv 등), AMD GPU면 AMF(h264_amf 등, Linux에서는 VAAPI를 더 많이 씁니다), Mac이면 VideoToolbox(h264_videotoolbox, hevc_videotoolbox)를 씁니다. 같은 제조사라도 GPU 세대에 따라 지원 코덱(HEVC 10비트, AV1)과 화질이 달라지므로, “어느 회사 인코더가 가장 좋다”는 일반화보다 대상 장비에서 ffmpeg -encoders로 지원 여부를 확인하고 대표 클립으로 비교하는 편이 정확합니다. 소비자용 NVIDIA GPU는 동시 인코딩 세션 수에 드라이버 제한이 있으므로, 여러 스트림을 동시에 처리하는 서버라면 이 제한도 확인해야 합니다.

# NVENC VBR 예시 (같은 절 상단 NVENC 예제와 옵션 계열이 같음)
ffmpeg -hwaccel cuda -i input.mp4 \
  -c:v h264_nvenc -preset slow -rc vbr -cq 23 \
  output.mp4

멀티스레딩

# 스레드 수 지정
ffmpeg -i input.mp4 -threads 8 -c:v libx264 -crf 23 output.mp4
# 자동 (CPU 코어 수)
ffmpeg -i input.mp4 -threads 0 -c:v libx264 -crf 23 output.mp4
# 타일 인코딩 (AV1)
ffmpeg -i input.mp4 -c:v libaom-av1 -tiles 4x4 -threads 16 output.mp4

비트레이트 상한과 faststart

# CRF에 비트레이트 상한(VBV)을 걸어 순간 비트레이트 폭주 방지 (메모리와는 무관)
ffmpeg -i input.mp4 \
  -bufsize 10M -maxrate 5M \
  -c:v libx264 -crf 23 \
  output.mp4
# 스트리밍 최적화 (faststart)
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -movflags +faststart output.mp4
# moov atom을 파일 앞으로 이동 (웹 스트리밍 필수)

실전 명령어 모음

유튜브 최적화

# 유튜브 업로드용 예시 (YouTube는 업로드 후 다시 인코딩하므로 여유 있는 화질로 올림)
ffmpeg -i input.mp4 \
  -c:v libx264 -preset slow -crf 18 \
  -c:a aac -b:a 192k -ar 48000 \
  -pix_fmt yuv420p \
  -movflags +faststart \
  youtube.mp4
# 4K 유튜브 (원본이 4K일 때. 저해상도 원본을 키워도 화질은 좋아지지 않음)
ffmpeg -i input.mp4 \
  -c:v libx264 -preset slow -crf 18 \
  -vf scale=3840:2160 \
  -c:a aac -b:a 192k \
  -pix_fmt yuv420p \
  -movflags +faststart \
  youtube_4k.mp4

Instagram 최적화

# Instagram 피드 (1:1)
ffmpeg -i input.mp4 \
  -vf "scale=1080:1080:force_original_aspect_ratio=decrease,pad=1080:1080:(ow-iw)/2:(oh-ih)/2" \
  -c:v libx264 -preset medium -crf 23 \
  -c:a aac -b:a 128k \
  instagram_feed.mp4
# 업로드 길이 제한은 플랫폼 정책에 따라 자주 바뀌므로 필요하면 -t로 잘라 냅니다
# Instagram 스토리 (9:16)
ffmpeg -i input.mp4 \
  -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2" \
  -c:v libx264 -preset medium -crf 23 \
  -c:a aac -b:a 128k \
  instagram_story.mp4

웹 최적화

# 웹용 MP4 (빠른 시작)
ffmpeg -i input.mp4 \
  -c:v libx264 -preset medium -crf 23 \
  -c:a aac -b:a 128k \
  -movflags +faststart \
  web.mp4
# WebM (VP9)
ffmpeg -i input.mp4 \
  -c:v libvpx-vp9 -b:v 2M \
  -c:a libopus -b:a 128k \
  web.webm
# 다중 포맷 (호환성)
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -movflags +faststart web.mp4
ffmpeg -i input.mp4 -c:v libvpx-vp9 -b:v 2M web.webm

라이브 스트리밍

RTMP 서버 설정

Nginx-RTMP

# nginx.conf
rtmp {
    server {
        listen 1935;
        
        application live {
            live on;
            record off;
            
            # HLS 생성
            hls on;
            hls_path /tmp/hls;
            hls_fragment 3;
            hls_playlist_length 60;
        }
    }
}
http {
    server {
        listen 8080;
        
        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /tmp;
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;
        }
    }
}

스트리밍 시작

# 파일 스트리밍
ffmpeg -re -i video.mp4 \
  -c:v libx264 -preset veryfast -b:v 3M -maxrate 3M -bufsize 6M \
  -pix_fmt yuv420p -g 60 \
  -c:a aac -b:a 128k -ar 44100 \
  -f flv rtmp://localhost/live/stream
# 웹캠 스트리밍
ffmpeg -f avfoundation -i "0:0" \
  -c:v libx264 -preset veryfast -b:v 2M \
  -c:a aac -b:a 128k \
  -f flv rtmp://localhost/live/webcam
# 화면 녹화 + 스트리밍 (macOS)
ffmpeg -f avfoundation -capture_cursor 1 -i "1:0" \
  -c:v libx264 -preset veryfast -b:v 3M \
  -c:a aac -b:a 128k \
  -f flv rtmp://localhost/live/screen

재스트리밍 (Restreaming)

# RTMP → HLS
ffmpeg -i rtmp://source/live/stream \
  -c:v copy -c:a copy \
  -hls_time 3 -hls_list_size 10 \
  -hls_flags delete_segments \
  output.m3u8
# 다중 플랫폼 동시 스트리밍 (한 번만 인코딩해 tee 먹서로 복제)
ffmpeg -re -i input.mp4 \
  -c:v libx264 -preset veryfast -b:v 3M -g 60 -c:a aac -b:a 128k \
  -map 0:v -map 0:a -f tee \
  "[f=flv:onfail=ignore]rtmp://a.rtmp.youtube.com/live2/YOUTUBE_KEY|[f=flv:onfail=ignore]rtmp://live.twitch.tv/app/TWITCH_KEY"
# 출력마다 인코딩 옵션을 따로 쓰면 같은 영상을 두 번 인코딩하게 됩니다

트러블슈팅

실무에서 자주 마주치는 에러와 해결 방법을 한곳에 모았습니다. 어떤 에러든 첫 단계는 같습니다. ffprobe input.mp4로 입력의 코덱·해상도·타임스탬프를 확인하고, 필요하면 -loglevel debug로 어느 단계(디먹서, 디코더, 필터, 인코더, 먹서)에서 실패하는지 좁힙니다.

문제 1: “Unknown encoder ‘libx264’”

원인: 설치된 FFmpeg 빌드에 해당 인코더가 포함되지 않았습니다. 일부 배포판 패키지나 최소 빌드는 GPL·비자유 라이브러리를 빼고 빌드됩니다.

# 어떤 인코더가 들어 있는지 확인
ffmpeg -hide_banner -encoders | grep -E "264|265|nvenc|qsv|amf"
ffmpeg -version   # configuration 줄에서 --enable-libx264 여부 확인
# macOS
brew reinstall ffmpeg
# Ubuntu/Debian 배포판 패키지는 libx264를 포함해 빌드됩니다
sudo apt install ffmpeg

문제 2: “height not divisible by 2”

[libx264 @ 0x...] height not divisible by 2 (1281x721)

원인: yuv420p는 색차 정보를 2×2 픽셀 단위로 저장하므로 가로·세로가 짝수여야 합니다.

# ❌ 홀수 해상도
ffmpeg -i input.mp4 -vf scale=1281:721 output.mp4
# ✅ -2는 비율을 유지하면서 2의 배수로 맞춤
ffmpeg -i input.mp4 -vf scale=1280:-2 output.mp4

문제 3: 오디오와 비디오 싱크가 맞지 않음

원인: 가변 프레임레이트(VFR) 녹화물, 잘못된 타임스탬프, 원래부터 어긋난 소스 등입니다. 먼저 원인이 “처음부터 일정하게 어긋남”인지 “시간이 지날수록 점점 벌어짐”인지 구분합니다.

# 처음부터 일정하게 어긋남: 오디오를 0.5초 늦추기 (재인코딩 없음)
ffmpeg -i input.mp4 -itsoffset 0.5 -i input.mp4 -map 0:v -map 1:a -c copy output.mp4
# 오디오를 0.5초 앞당기기
ffmpeg -i input.mp4 -itsoffset -0.5 -i input.mp4 -map 0:v -map 1:a -c copy output.mp4

# 점점 벌어짐 (VFR 소스 등): 재인코딩하면서 타임스탬프 정리
ffmpeg -i input.mp4 -c:v libx264 -fps_mode cfr -r 30 \
  -c:a aac -af aresample=async=1 output.mp4

-c copy로는 타임스탬프를 다시 계산할 수 없으므로, 점점 벌어지는 싱크는 재인코딩이 필요합니다. 예전 문서의 -vsync·-async 옵션은 최근 버전에서 각각 -fps_mode와 aresample=async 필터로 대체되었습니다.

문제 4: “moov atom not found” 또는 웹에서 다운로드가 끝나야 재생됨

원인: MP4의 메타데이터(moov)가 파일 끝에 있거나(웹 재생 지연), 녹화 도중 프로세스가 종료되어 moov가 아예 기록되지 않았습니다(파일 손상).

# moov를 앞으로 옮기기 (재인코딩 없음)
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4
# 인코딩 시 처음부터 지정
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -movflags +faststart output.mp4

moov가 아예 없는 파일은 이 방법으로 살릴 수 없습니다. 장시간 녹화라면 MKV나 fragmented MP4(-movflags +frag_keyframe+empty_moov)로 저장해 두면 중간에 끊겨도 앞부분은 재생할 수 있습니다.

문제 5: 인코딩이 너무 느림

# 1. 프리셋을 빠르게 (같은 CRF에서 파일은 커짐)
ffmpeg -i input.mp4 -c:v libx264 -preset veryfast -crf 23 output.mp4
# 2. 해상도 낮추기
ffmpeg -i input.mp4 -vf scale=1280:-2 -c:v libx264 -crf 23 output.mp4
# 3. 하드웨어 인코더 (NVIDIA): 품질은 -crf가 아니라 -cq로 지정
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset p4 -rc vbr -cq 23 -b:v 0 output.mp4
# 4. 하드웨어 인코더 (Intel QSV)
ffmpeg -i input.mp4 -c:v h264_qsv -preset fast -global_quality 23 output.mp4
# 5. 하드웨어 인코더 (AMD)
ffmpeg -i input.mp4 -c:v h264_amf -quality balanced -rc cqp -qp_i 23 -qp_p 23 output.mp4
# 6. 재인코딩이 필요 없다면 복사
ffmpeg -i input.mp4 -c copy output.mp4
# 7. 여러 파일은 병렬로 (코어 수와 메모리를 보고 -j 조절)
parallel -j 4 ffmpeg -i {} -c:v libx264 -crf 23 {.}_output.mp4 ::: *.mp4

libx264는 한 프로세스로도 여러 코어를 쓰므로, 병렬 처리를 과하게 늘리면 오히려 전체 처리량이 떨어질 수 있습니다.

문제 6: 파일 크기가 너무 큼

# 1. CRF 높이기 (품질 낮춤)
ffmpeg -i input.mp4 -c:v libx264 -crf 28 output.mp4
# 2. 해상도·프레임레이트 낮추기
ffmpeg -i input.mp4 -vf scale=1280:-2 -r 24 -c:v libx264 -crf 23 output.mp4
# 3. H.265 사용 (재생 기기 호환성 확인 필요)
ffmpeg -i input.mp4 -c:v libx265 -crf 28 -tag:v hvc1 output.mp4
# 4. 2-Pass로 목표 크기 맞추기
# 목표 100MB, 10분(600초) → 전체 약 1.33Mbps, 오디오 128k를 빼면 비디오 약 1.2Mbps
ffmpeg -y -i input.mp4 -c:v libx264 -b:v 1200k -pass 1 -an -f null /dev/null
ffmpeg -i input.mp4 -c:v libx264 -b:v 1200k -pass 2 -c:a aac -b:a 128k output.mp4

문제 7: “Conversion failed” 또는 “Invalid data found when processing input”

원인: 손상된 파일, 확장자와 실제 포맷이 다른 파일, 빌드에 없는 코덱 등입니다.

# 1. 입력 정보 확인
ffprobe -hide_banner input.mp4
# 2. 상세 로그로 실패 지점 확인
ffmpeg -loglevel debug -i input.mp4 -f null - 2>&1 | tail -50
# 3. 손상된 패킷을 버리고 계속 진행
ffmpeg -fflags +discardcorrupt -err_detect ignore_err -i input.mp4 -c:v libx264 -crf 23 output.mp4
# 4. 재인코딩 없이 컨테이너만 다시 쓰기
ffmpeg -i input.mp4 -c copy output.mp4

문제 8: “Too many packets buffered for output stream”

원인: 여러 스트림을 먹싱할 때 한 스트림의 패킷이 다른 스트림보다 너무 앞서 쌓이는 경우입니다. 스트림 시작 시간이 크게 다르거나 필터가 한쪽 스트림을 오래 붙잡고 있을 때 생깁니다.

ffmpeg -i input.mp4 -c:v libx264 -max_muxing_queue_size 1024 output.mp4

값을 올리면 그만큼 메모리를 더 씁니다. 에러가 반복되면 입력의 타임스탬프나 필터 구성을 먼저 확인하세요.

문제 9: 메모리 사용량이 너무 큼

# 1. 스레드 수 제한 (스레드마다 프레임 버퍼를 가짐)
ffmpeg -i input.mp4 -threads 2 -c:v libx264 output.mp4
# 2. lookahead 줄이기
ffmpeg -i input.mp4 -c:v libx264 -x264-params rc-lookahead=20 output.mp4
# 3. 긴 파일은 나눠서 처리 (-ss를 -i 앞에 두어 빠르게 이동)
ffmpeg -ss 0 -t 600 -i input.mp4 -c:v libx264 part1.mp4
ffmpeg -ss 600 -t 600 -i input.mp4 -c:v libx264 part2.mp4

문제 10: 색상이 이상함 (물빠진 색, 회색빛)

원인: 색공간·색 범위 메타데이터가 빠지거나 잘못 지정되어 플레이어가 다르게 해석하는 경우입니다. 10비트·HDR 소스를 8비트 SDR로 바꿀 때도 흔합니다.

# 8비트 4:2:0으로 명시 (호환성)
ffmpeg -i input.mp4 -vf scale=1280:-2 -pix_fmt yuv420p -c:v libx264 output.mp4
# BT.709 색공간 메타데이터 지정
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -color_primaries bt709 \
  -color_trc bt709 -colorspace bt709 output.mp4

HDR 소스를 SDR로 바꾸는 것은 메타데이터만으로 해결되지 않고 톤 매핑 필터(zscale·tonemap 등)가 필요합니다.

문제 11: 자막이 나오지 않음

# 1. 자막 스트림 확인
ffprobe -hide_banner -show_streams -select_streams s input.mkv
# 2. MP4로 옮길 때는 mov_text로 변환 (MP4는 SRT·ASS를 그대로 담지 못함)
ffmpeg -i input.mkv -map 0 -c:v copy -c:a copy -c:s mov_text output.mp4
# 3. 자막을 영상에 구워 넣기 (재인코딩 필요)
ffmpeg -i video.mp4 -vf subtitles=subtitle.srt output.mp4
# 4. 외부 자막 파일을 트랙으로 추가
ffmpeg -i video.mp4 -i subtitle.srt -map 0 -map 1 -c:v copy -c:a copy -c:s mov_text output.mp4

-map 0을 빼면 FFmpeg는 스트림 유형마다 하나씩만 골라서, 자막이나 두 번째 오디오 트랙이 조용히 빠질 수 있습니다.


실전 스크립트

자동 인코딩 워커

Python (Celery)

from celery import Celery
import ffmpeg
import os
app = Celery('video_worker', broker='redis://localhost:6379/0')
@app.task
def encode_video(input_path, output_path, resolution='1280x720', crf=23):
    """비디오 인코딩 작업"""
    try:
        width, height = resolution.split('x')
        
        (
            ffmpeg
            .input(input_path)
            .filter('scale', width, height)
            .output(
                output_path,
                vcodec='libx264',
                preset='slow',
                crf=crf,
                acodec='aac',
                audio_bitrate='192k',
                movflags='+faststart'
            )
            .overwrite_output()
            .run(capture_stdout=True, capture_stderr=True)
        )
        
        return {'status': 'success', 'output': output_path}
    
    except ffmpeg.Error as e:
        return {'status': 'error', 'message': e.stderr.decode()}
@app.task
def create_thumbnails(input_path, output_dir, count=3):
    """썸네일 생성 작업"""
    try:
        probe = ffmpeg.probe(input_path)
        duration = float(probe['format']['duration'])
        
        os.makedirs(output_dir, exist_ok=True)
        
        thumbnails = []
        for i in range(count):
            timestamp = duration * (i + 1) / (count + 1)
            output_path = os.path.join(output_dir, f'thumb_{i}.jpg')
            
            (
                ffmpeg
                .input(input_path, ss=timestamp)
                .output(output_path, vframes=1, q=2)
                .overwrite_output()
                .run(capture_stdout=True, capture_stderr=True)
            )
            
            thumbnails.append(output_path)
        
        return {'status': 'success', 'thumbnails': thumbnails}
    
    except ffmpeg.Error as e:
        return {'status': 'error', 'message': e.stderr.decode()}
# 사용
if __name__ == '__main__':
    # 작업 큐에 추가
    encode_video.delay('input.mp4', 'output.mp4', resolution='1920x1080', crf=20)
    create_thumbnails.delay('input.mp4', './thumbnails', count=5)

진행률 웹소켓

Node.js + Socket.io

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const ffmpeg = require('fluent-ffmpeg');
const multer = require('multer');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
const upload = multer({ dest: 'uploads/' });
app.post('/convert', upload.single('video'), (req, res) => {
  const inputPath = req.file.path;
  const outputPath = `processed/${req.file.filename}.mp4`;
  const socketId = req.body.socketId;
  
  ffmpeg(inputPath)
    .output(outputPath)
    .videoCodec('libx264')
    .audioCodec('aac')
    .on('start', (cmd) => {
      console.log('Started:', cmd);
    })
    .on('progress', (progress) => {
      // 클라이언트에게 진행률 전송
      io.to(socketId).emit('progress', {
        percent: progress.percent,
        currentTime: progress.timemark,
        fps: progress.currentFps
      });
    })
    .on('end', () => {
      io.to(socketId).emit('complete', {
        url: `/processed/${req.file.filename}.mp4`
      });
    })
    .on('error', (err) => {
      io.to(socketId).emit('error', {
        message: err.message
      });
    })
    .run();
  
  res.json({ message: 'Processing started' });
});
io.on('connection', (socket) => {
  console.log('Client connected:', socket.id);
  
  socket.on('disconnect', () => {
    console.log('Client disconnected:', socket.id);
  });
});
server.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

클라이언트 (HTML)

<!DOCTYPE html>
<html>
<head>
    <title>Video Converter</title>
    <script src="/socket.io/socket.io.js"></script>
</head>
<body>
    <h1>Video Converter</h1>
    
    <input type="file" id="videoFile" accept="video/*">
    <button onclick="uploadVideo()">Upload & Convert</button>
    
    <div id="progress" style="display: none;">
        <p>Progress: <span id="percent">0</span>%</p>
        <progress id="progressBar" value="0" max="100"></progress>
    </div>
    
    <div id="result" style="display: none;">
        <p>✅ Conversion complete!</p>
        <video id="outputVideo" controls width="640"></video>
    </div>
    
    <script>
        const socket = io();
        
        function uploadVideo() {
            const fileInput = document.getElementById('videoFile');
            const file = fileInput.files[0];
            
            if (!file) {
                alert('Please select a video file');
                return;
            }
            
            const formData = new FormData();
            formData.append('video', file);
            formData.append('socketId', socket.id);
            
            document.getElementById('progress').style.display = 'block';
            
            fetch('/convert', {
                method: 'POST',
                body: formData
            });
        }
        
        socket.on('progress', (data) => {
            document.getElementById('percent').textContent = data.percent.toFixed(1);
            document.getElementById('progressBar').value = data.percent;
        });
        
        socket.on('complete', (data) => {
            document.getElementById('progress').style.display = 'none';
            document.getElementById('result').style.display = 'block';
            document.getElementById('outputVideo').src = data.url;
        });
        
        socket.on('error', (data) => {
            alert('Error: ' + data.message);
        });
    </script>
</body>
</html>

고급 활용

자막 처리

# 자막 추가 (하드서브)
ffmpeg -i video.mp4 -vf subtitles=subtitle.srt output.mp4
# 자막 추가 (소프트서브)
ffmpeg -i video.mp4 -i subtitle.srt -c copy -c:s mov_text output.mp4
# 자막 추출
ffmpeg -i video.mp4 -map 0:s:0 subtitle.srt
# 자막 스타일 변경
ffmpeg -i video.mp4 \
  -vf "subtitles=subtitle.srt:force_style='FontName=Arial,FontSize=24,PrimaryColour=&H00FFFF'" \
  output.mp4

메타데이터 편집

# 메타데이터 확인
ffprobe -show_format input.mp4
# 메타데이터 추가
ffmpeg -i input.mp4 -c copy \
  -metadata title="My Video" \
  -metadata author="Alice" \
  -metadata comment="Created with FFmpeg" \
  output.mp4
# 메타데이터 제거
ffmpeg -i input.mp4 -c copy -map_metadata -1 output.mp4

화면 녹화

# macOS (화면 + 오디오)
ffmpeg -f avfoundation -capture_cursor 1 -i "1:0" \
  -c:v libx264 -preset ultrafast -crf 23 \
  -c:a aac -b:a 128k \
  screen_recording.mp4
# Linux (X11)
ffmpeg -f x11grab -s 1920x1080 -i :0.0 \
  -c:v libx264 -preset ultrafast -crf 23 \
  screen_recording.mp4
# Windows (GDI)
ffmpeg -f gdigrab -i desktop \
  -c:v libx264 -preset ultrafast -crf 23 \
  screen_recording.mp4

프로덕션 팁

에러 처리

import ffmpeg
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def safe_convert(input_path, output_path, max_retries=3):
    """재시도 로직 포함 변환"""
    for attempt in range(max_retries):
        try:
            logger.info(f"Attempt {attempt + 1}/{max_retries}")
            
            (
                ffmpeg
                .input(input_path)
                .output(output_path, vcodec='libx264', crf=23)
                .global_args('-loglevel', 'error')
                .overwrite_output()
                .run(capture_stdout=True, capture_stderr=True)
            )
            
            logger.info("✅ Success!")
            return True
            
        except ffmpeg.Error as e:
            logger.error(f"❌ Attempt {attempt + 1} failed: {e.stderr.decode()}")
            
            if attempt == max_retries - 1:
                logger.error("All attempts failed")
                raise
    
    return False

큐 시스템

import ffmpeg
import redis
import json
import time
from threading import Thread
class VideoQueue:
    def __init__(self):
        self.redis = redis.Redis(host='localhost', port=6379, db=0)
        self.queue_key = 'video:queue'
        self.processing_key = 'video:processing'
    
    def add_job(self, input_path, output_path, options=None):
        """작업 추가"""
        job = {
            'input': input_path,
            'output': output_path,
            'options': options or {},
            'status': 'queued',
            'created_at': time.time()
        }
        
        job_id = f"job:{time.time()}"
        self.redis.lpush(self.queue_key, json.dumps(job))
        
        return job_id
    
    def process_job(self, job_data):
        """작업 처리"""
        job = json.loads(job_data)
        
        try:
            # FFmpeg 실행
            stream = ffmpeg.input(job['input'])
            
            # 옵션 적용
            if 'scale' in job['options']:
                stream = stream.filter('scale', *job['options']['scale'].split('x'))
            
            stream.output(
                job['output'],
                vcodec='libx264',
                crf=job['options'].get('crf', 23),
                preset=job['options'].get('preset', 'medium')
            ).overwrite_output().run(capture_stdout=True, capture_stderr=True)
            
            print(f"✅ Job complete: {job['output']}")
            
        except ffmpeg.Error as e:
            # run()에서 stderr를 캡처해야 e.stderr가 None이 아님
            print(f"❌ Job failed: {e.stderr.decode()}")
    
    def worker(self):
        """워커 스레드"""
        print("Worker started")
        
        while True:
            # 큐에서 작업 가져오기
            job_data = self.redis.brpop(self.queue_key, timeout=5)
            
            if job_data:
                _, job_json = job_data
                self.redis.lpush(self.processing_key, job_json)
                
                self.process_job(job_json)
                
                self.redis.lrem(self.processing_key, 1, job_json)
    
    def start_workers(self, num_workers=4):
        """워커 시작"""
        threads = []
        for i in range(num_workers):
            t = Thread(target=self.worker, daemon=True)
            t.start()
            threads.append(t)
        
        return threads
# 사용
queue = VideoQueue()
# 작업 추가
queue.add_job('input1.mp4', 'output1.mp4', {'scale': '1280x720', 'crf': 20})
queue.add_job('input2.mp4', 'output2.mp4', {'scale': '1920x1080', 'crf': 18})
# 워커 시작 (4개)
workers = queue.start_workers(num_workers=4)
# 메인 스레드 유지
for worker in workers:
    worker.join()

인코딩 속도 직접 비교하기

비교 스크립트

# 테스트 스크립트
#!/bin/bash
INPUT="test_4k.mp4"
echo "=== FFmpeg Encoding Benchmark ==="
echo
# H.264 프리셋 비교
for preset in ultrafast superfast veryfast faster fast medium slow slower veryslow; do
    echo "Testing preset: $preset"
    
    start=$(date +%s)
    ffmpeg -i "$INPUT" -c:v libx264 -preset "$preset" -crf 23 -an -f null - 2>&1 | grep "frame="
    end=$(date +%s)
    
    duration=$((end - start))
    echo "Time: ${duration}s"
    echo
done
# 코덱 비교
for codec in libx264 libx265 libvpx-vp9; do
    echo "Testing codec: $codec"
    
    start=$(date +%s)
    ffmpeg -i "$INPUT" -c:v "$codec" -b:v 5M -an -f null - 2>&1 | grep "frame="
    end=$(date +%s)
    
    duration=$((end - start))
    echo "Time: ${duration}s"
    echo
done

실제 시간과 파일 크기는 CPU, 원본 영상의 내용(움직임이 많은지), FFmpeg 빌드에 따라 크게 달라지므로 자신의 장비에서 이 스크립트를 직접 돌려 보는 것이 가장 정확합니다. 일반적인 경향만 말하면, 같은 CRF에서 프리셋을 느리게 할수록 인코딩 시간은 크게 늘고 파일 크기는 줄어들지만 medium 이후로는 줄어드는 폭이 점점 작아집니다. 코덱 비교에서는 libx265와 libvpx-vp9가 libx264보다 훨씬 오래 걸리는 대신, 비슷한 화질을 더 작은 파일로 얻는 경우가 많습니다. 위 스크립트처럼 -b:v 5M으로 비트레이트를 고정하면 파일 크기는 거의 같아지므로, 이때는 크기가 아니라 화질을 눈으로(또는 VMAF 같은 지표로) 비교해야 합니다.


유용한 원라이너

# 모든 MP4를 720p로 일괄 변환
for f in *.mp4; do ffmpeg -i "$f" -vf scale=1280:720 -c:v libx264 -crf 23 "720p_$f"; done
# 모든 동영상에서 오디오 추출
for f in *.mp4; do ffmpeg -i "$f" -vn -c:a libmp3lame -b:a 192k "${f%.mp4}.mp3"; done
# 모든 동영상의 첫 프레임 추출
for f in *.mp4; do ffmpeg -i "$f" -vframes 1 "${f%.mp4}.jpg"; done
# 모든 이미지를 동영상으로 (슬라이드쇼)
ffmpeg -framerate 1 -pattern_type glob -i '*.jpg' -c:v libx264 -pix_fmt yuv420p slideshow.mp4
# 오디오 파일들을 하나로 병합
ffmpeg -i "concat:audio1.mp3|audio2.mp3|audio3.mp3" -c copy output.mp3
# 동영상 정보 CSV로 저장
for f in *.mp4; do echo "$f,$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$f")"; done > videos.csv

Docker로 FFmpeg 사용

Dockerfile

FROM ubuntu:22.04
# FFmpeg 설치
RUN apt-get update && \
    apt-get install -y ffmpeg && \
    rm -rf /var/lib/apt/lists/*
WORKDIR /data
ENTRYPOINT ["ffmpeg"]

빌드 및 사용

# 빌드
docker build -t my-ffmpeg .
# 사용
docker run --rm -v $(pwd):/data my-ffmpeg \
  -i /data/input.mp4 \
  -c:v libx264 -crf 23 \
  /data/output.mp4
# 별칭 생성
alias dffmpeg='docker run --rm -v $(pwd):/data my-ffmpeg'
# 사용
dffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4

FAQ

Q1. FFmpeg와 HandBrake는 어떻게 나누면 되나?

  • FFmpeg: 명령줄 기반, 자동화·스크립트·서버에 적합, 세밀한 제어 가능
  • HandBrake: GUI 기반, 직관적, 빠른 설정, 개인 사용에 편리

선택 가이드

  • 한두 개 파일 변환: HandBrake
  • 배치 처리, 자동화: FFmpeg
  • 서버/CI/CD 파이프라인: FFmpeg
  • 복잡한 필터, 스트리밍: FFmpeg
  • 처음 시작하는 초보자: HandBrake

Q2. 코덱은 무엇을 쓰면 되나?

상황에 따라 다릅니다:

상황추천 코덱이유
최대 호환성H.264모든 기기 지원
파일 크기 최소화H.265같은 화질에서 비트레이트 절감 (표준화 목표는 H.264 대비 약 절반)
웹 스트리밍H.264 또는 VP9브라우저 지원
YouTube 업로드H.264플랫폼 권장
4K 영상H.265효율적 압축
미래 대비AV1차세대 표준

실전 팁

  • 일반 용도: H.264 (libx264)
  • 저장 공간 부족: H.265 (libx265)
  • 웹 전용: VP9 (libvpx-vp9)

Q3. CRF와 비트레이트 중 무엇을 쓰나?

대부분의 경우 CRF를 권장합니다. CRF 사용 (권장): 장면마다 필요한 만큼 비트를 배분해 품질을 일정하게 유지하고, 값 하나만 정하면 되므로 쓰기 쉽습니다. 대신 결과 파일 크기를 미리 알기 어렵습니다.

ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4

비트레이트 사용: 파일 크기와 스트리밍 대역폭을 예측하고 제어할 수 있습니다. 대신 복잡한 장면에서는 할당된 비트가 부족해 품질이 떨어질 수 있고, 목표 크기를 정확히 맞추려고 2-Pass를 쓰면 인코딩을 두 번 해야 합니다.

ffmpeg -i input.mp4 -c:v libx264 -b:v 5M output.mp4

선택 가이드

  • 일반 파일 변환: CRF 23
  • YouTube 업로드: CRF 18-20
  • 웹 스트리밍: 비트레이트 2-5M
  • 방송 송출: 비트레이트 (2-Pass)
  • 모바일: CRF 26-28

Q4. 인코딩이 너무 느리다

여러 최적화 방법이 있습니다:

1. 프리셋 조정 (가장 쉬움)

# 느림 (현재)
ffmpeg -i input.mp4 -preset slow -crf 23 output.mp4
# 빠름 (권장)
ffmpeg -i input.mp4 -preset veryfast -crf 23 output.mp4
# 인코딩은 빨라지고, 같은 CRF에서 파일은 커짐

2. GPU 인코더 사용 (가장 효과적)

# NVIDIA GPU
ffmpeg -i input.mp4 -c:v h264_nvenc -preset p4 -rc vbr -cq 23 -b:v 0 output.mp4
# NVENC는 -crf 대신 -cq로 품질을 지정합니다
# Intel GPU
ffmpeg -i input.mp4 -c:v h264_qsv -preset fast output.mp4
# AMD GPU
ffmpeg -i input.mp4 -c:v h264_amf -quality balanced output.mp4

3. 해상도 낮추기

ffmpeg -i input_4k.mp4 -vf scale=1920:-2 -crf 23 output_1080p.mp4
# 처리할 픽셀 수가 줄어 인코딩이 빨라짐

4. 병렬 처리 (여러 파일)

# GNU Parallel 사용
parallel -j 4 ffmpeg -i {} -crf 23 {.}_output.mp4 ::: *.mp4
# 4개 파일 동시 처리

5. 프레임레이트 낮추기

ffmpeg -i input.mp4 -r 24 -crf 23 output.mp4
# 인코딩할 프레임 수가 줄어 빨라짐

속도 vs 품질 비교

방법속도품질/크기 영향
preset veryfast크게 빨라짐같은 CRF에서 파일이 커짐
GPU 인코더가장 크게 빨라지는 경우가 많음같은 비트레이트에서 화질이 소프트웨어 인코더보다 다소 낮음
해상도 감소처리할 픽셀 수에 비례해 빨라짐해상도 자체가 낮아짐
프레임레이트 감소처리할 프레임 수만큼 빨라짐움직임이 덜 부드러워짐

실제 배율은 장비와 영상에 따라 달라지므로, 위 “인코딩 속도 직접 비교하기” 절의 스크립트로 확인해 보세요.

Q5. 화질 손실을 최소화하려면?

상황별 최적 전략:

1. 재인코딩 없이 변환 (무손실)

# 컨테이너만 변경
ffmpeg -i input.mkv -c copy output.mp4
# 장점: 초고속, 무손실
# 단점: 해상도/품질 조정 불가

2. 고품질 재인코딩 (거의 무손실)

# CRF 17-18 사용
ffmpeg -i input.mp4 -c:v libx264 -preset slow -crf 17 output.mp4
# 시각적으로 무손실, 파일 크기 큼

3. 편집용 중간 코덱

# ProRes 422 HQ (prores_ks는 macOS가 아니어도 동작)
ffmpeg -i input.mp4 -c:v prores_ks -profile:v 3 output.mov
# DNxHR HQ (해상도·프레임레이트 제약이 있는 DNxHD 대신 DNxHR 프로파일 사용)
ffmpeg -i input.mp4 -c:v dnxhd -profile:v dnxhr_hq -pix_fmt yuv422p output.mov
# 무손실 H.264
ffmpeg -i input.mp4 -c:v libx264 -preset slow -crf 0 output.mp4

4. 다단계 인코딩 최소화

# ❌ 나쁜 예: 3번 인코딩
원본 → 편집 → 효과 → 최종
(품질 손실 누적)
# ✅ 좋은 예: 1번 인코딩
원본 → [편집+효과 동시] → 최종

5. 적절한 코덱 선택

# 보관용: 무손실 또는 고품질
ffmpeg -i input.mp4 -c:v libx264 -crf 17 -preset slow archive.mp4
# 배포용: 균형잡힌 품질
ffmpeg -i input.mp4 -c:v libx264 -crf 20 -preset slow final.mp4
# 웹용: 적정 품질
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium web.mp4

품질 우선순위

  1. 재인코딩 피하기 (-c copy)
  2. 낮은 CRF 값 (17-20)
  3. 느린 프리셋 (slow, slower)
  4. 적절한 해상도 유지
  5. 2-Pass 인코딩 (필요시)

Q6. 웹 스트리밍용 최적 설정은?

# 웹 최적화 (progressive download)
ffmpeg -i input.mp4 \
  -c:v libx264 -preset medium -crf 23 \
  -c:a aac -b:a 128k \
  -movflags +faststart \
  -vf scale=1920:-2 \
  web_optimized.mp4
# 핵심 옵션:
# -movflags +faststart: 메타데이터를 앞으로 (즉시 재생)
# -crf 23: 웹용 적정 품질
# -b:a 128k: 오디오 대역폭 절약

Q7. 여러 파일을 하나로 합치려면?

# 방법 1: concat 프로토콜 (파일을 바이트 단위로 이어 붙이므로 MPEG-TS처럼
# 이어 붙여도 유효한 포맷에서만 동작, MP4에는 쓸 수 없음)
ffmpeg -i "concat:file1.ts|file2.ts|file3.ts" -c copy output.ts
# 방법 2: concat demuxer (권장)
# 1. 파일 목록 생성
echo "file 'file1.mp4'" > list.txt
echo "file 'file2.mp4'" >> list.txt
echo "file 'file3.mp4'" >> list.txt
# 2. 병합
ffmpeg -f concat -safe 0 -i list.txt -c copy output.mp4
# 방법 3: filter_complex (다른 코덱/해상도)
ffmpeg -i file1.mp4 -i file2.mp4 -i file3.mp4 \
  -filter_complex "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[v][a]" \
  -map "[v]" -map "[a]" output.mp4

Q8. 특정 구간만 자르려면?

# 빠른 방법 (재인코딩 없음, 키프레임에서만 정확)
ffmpeg -i input.mp4 -ss 00:01:30 -to 00:02:45 -c copy output.mp4
# 정확한 방법 (재인코딩, 느림)
ffmpeg -i input.mp4 -ss 00:01:30 -to 00:02:45 -c:v libx264 -crf 23 output.mp4
# 시작 시간과 길이로 지정
ffmpeg -i input.mp4 -ss 00:01:30 -t 00:01:15 -c copy output.mp4
# -ss: 시작 시간
# -t: 길이
# -to: 종료 시간

관련 글

FFmpeg와 함께 보면 좋은 비디오·오디오 관련 글입니다: