블로그 목록
시리즈: 실시간 통화, 어떻게 녹화하는가5/6
  1. 1.실시간 통화, 왜 녹화해야 하는가
  2. 2.내 서버에서 직접 녹화한다면
  3. 3.녹화 모드 해부
  4. 4.M3U8과 TS 파일의 모든 것
  5. 5.FFmpeg, 미디어의 스위스 아미 나이프 ← 현재 글
  6. 6.FFmpeg 실전 파이프라인
Backend시리즈14분 읽기

FFmpeg, 미디어의 스위스 아미 나이프 — 컨테이너, 코덱, 스트림

Container와 Codec의 차이, Stream 개념, -c copy vs Transcoding, FFmpeg 내부 5단계 파이프라인, SRS 리패키징 서버 실전 예시(RTMP→디먹서→HLS/WebRTC 분기 먹서)까지 해부합니다.

FFmpegCodecContainerMuxingTranscoding

FFmpeg 없이 미디어를 다루겠다는 건, Git 없이 코드를 관리하겠다는 것과 같다. 물론 가능하긴 하다. 하지만 그 고통을 굳이 감수할 이유가 없다.

녹화 파일이 생성된 순간부터 사용자에게 실제로 전달되기까지, 그 사이에는 수많은 후처리 과정이 존재한다. TS 파일을 MP4로 변환하고, 개별 트랙을 합치고, 구간을 자르고, 썸네일을 뽑아내고. 이 모든 작업의 중심에 FFmpeg이 있다.

이 글에서는 FFmpeg을 제대로 이해하기 위한 세 가지 핵심 개념 — Container, Codec, Stream — 과 함께, 실무에서 반드시 알아야 할 명령어 패턴을 정리한다.


핵심 개념 1: Container vs Codec

가장 많이 혼동하는 개념부터 시작한다.

Container (컨테이너) = 택배 상자
  파일 포맷 자체를 의미. 내부 데이터를 어떻게 담을지 정의.
  MP4, TS, MKV, WebM, FLV, AVI ...

Codec (코덱) = 상자 안의 물건을 포장하는 방식
  실제 미디어 데이터를 어떻게 압축/복원할지 정의.
  비디오: H.264, H.265(HEVC), VP8, VP9, AV1
  오디오: AAC, Opus, MP3, FLAC

핵심: 같은 코덱을 다른 컨테이너에 담을 수 있다

  H.264 + AAC → MP4  (일반적인 영상 파일)
  H.264 + AAC → TS   (HLS 스트리밍 세그먼트)
  H.264 + AAC → MKV  (아카이브용 만능 컨테이너)
  VP9 + Opus  → WebM (웹 브라우저 최적화)

택배 상자(MP4)와 포장 방식(H.264)은 별개다. 같은 물건을 다른 상자에 옮겨 담을 수 있고, 같은 상자에 다른 방식으로 포장된 물건을 넣을 수도 있다.

컨테이너별 특성:

컨테이너특징주요 용도
MP4가장 범용, 브라우저 호환 최고VOD, 다운로드
TS자기완결적 패킷, 장애 내성HLS 스트리밍, 실시간 녹화
MKV거의 모든 코덱 지원, 다중 트랙아카이브, 자막 포함 영상
WebMVP8/VP9 + Opus 전용웹 최적화
FLV레거시 Flash 포맷RTMP 스트리밍 (레거시)

TS가 "자기완결적 패킷"이라는 표현에 주목할 필요가 있다. HLS 세그먼트로 TS를 쓰는 이유가 바로 이것이다. 패킷 단위로 독립적으로 파싱 가능하기 때문에, 네트워크 오류나 파일 손상이 발생해도 이후 패킷부터 정상 재생이 가능하다.


핵심 개념 2: Stream (스트림)

컨테이너 내부에는 하나 이상의 스트림(트랙)이 존재한다.

하나의 MP4 파일 내부:
┌──────────────────────────────────┐
│  MP4 Container                   │
│                                  │
│  ┌──────────────────────────┐    │
│  │ Stream 0: Video (H.264)  │    │
│  ├──────────────────────────┤    │
│  │ Stream 1: Audio (AAC)    │    │
│  ├──────────────────────────┤    │
│  │ Stream 2: Subtitle (SRT) │    │
│  └──────────────────────────┘    │
└──────────────────────────────────┘

각 스트림은 독립적으로 처리할 수 있다. 비디오 스트림만 추출하거나, 오디오 스트림만 교체하거나, 자막 스트림을 추가하는 것이 모두 가능하다.

ffprobe로 파일의 스트림 구성을 확인할 수 있다:

ffprobe -v quiet -show_streams -of json input.mp4

출력 결과에서 codec_type, codec_name, width, height, sample_rate 등을 확인할 수 있다. 후처리 파이프라인을 작성하기 전에 항상 먼저 실행해보는 습관을 들이면 좋다. "왜 오디오가 없지?" 같은 문제를 미리 발견할 수 있다.


핵심 개념 3: Muxing / Demuxing

스트림을 컨테이너에 넣고 빼는 작업에도 이름이 있다.

Demuxing (분해):
┌──────────┐              ┌─ Video stream (H.264)
│  MP4     │ ─── Demux ─→ ├─ Audio stream (AAC)
│  파일    │              └─ Subtitle stream
└──────────┘

Muxing (조립):
Video stream  ─┐
Audio stream  ─┼─── Mux ───→ ┌──────────┐
Subtitle      ─┘             │  MKV     │
                              │  파일    │
                              └──────────┘

Remuxing (재조립):
┌──────┐         ┌─ Video stream ─┐         ┌──────┐
│  TS  │ Demux ─→│               │─→ Mux ─→ │  MP4 │
└──────┘         └─ Audio stream ─┘         └──────┘
스트림은 그대로, 컨테이너만 교체
  • Demuxing: 컨테이너에서 스트림을 꺼내는 작업
  • Muxing: 스트림들을 컨테이너에 담는 작업
  • Remuxing: 스트림 변경 없이 컨테이너만 바꾸는 것 (TS → MP4)

Remuxing이 중요한 이유는 비디오/오디오 데이터를 디코딩/인코딩하지 않기 때문이다. 원본 데이터를 그대로 다른 상자에 옮겨 담는 것이라 속도가 극도로 빠르고 품질 손실이 전혀 없다.

실전 예시 — 리패키징 서버 (SRS 등)

디먹서/먹서 개념이 실제로 어떻게 조합되는지 가장 흔한 사례가 리패키징 서버입니다. OBS 같은 송출 도구가 RTMP로 푸시하면, 서버 한 곳에서 받아서 여러 프로토콜로 동시에 재배포하는 구조.

대표 오픈소스: SRS (Simple Realtime Server).

OBS 송출 RTMP push tcp :1935 RTMP 리패키징 서버 (예: SRS) RTMP 수신 모듈 포트 1935 스트림 관리자 채널별 라우팅 디먹서 (H.264 / AAC 추출) 컨테이너만 벗김 — 코덱 데이터 그대로 (재인코딩 아님) HLS 먹서 MPEG-TS 세그먼트 + .m3u8 WebRTC 먹서 RTP 패킷 포장 HLS 시청자 모바일 · 브라우저 범용 WebRTC 시청자 저지연 필요시

플로우:

  1. RTMP 수신 — OBS가 푸시한 RTMP 스트림을 TCP 1935에서 수신
  2. 디먹서 — RTMP 컨테이너에서 H.264(비디오) + AAC(오디오) 코덱 데이터만 추출. 코덱 페이로드는 그대로 유지 (재인코딩 아님)
  3. 분기 먹서
    • HLS 먹서: 추출한 H.264/AAC를 MPEG-TS 세그먼트로 포장 → .m3u8 플레이리스트 갱신
    • WebRTC 먹서: 같은 H.264/AAC를 RTP 패킷으로 포장 → 저지연 피어 전송
  4. 재배포 — 한 입력이 두 가지 프로토콜로 동시 제공

왜 이 구조가 효율적인가

측면설명
재인코딩 없음디먹서→먹서만 수행. CPU 부하 최소. 화질 손실 없음
프로토콜 다양성한 입력으로 HLS(대중 호환) + WebRTC(저지연) 동시
확장성디먹서 출력을 캐시해서 N개 먹서에 공급 가능

주의: 코덱 호환성

디먹서는 컨테이너만 벗기므로 코덱 자체가 목적지와 호환돼야 합니다. 예를 들어 송출단이 H.264 Main Profile + B-frame을 쓰면, HLS 쪽은 OK지만 WebRTC 쪽에서 디코딩 실패할 수 있습니다 (B-frame + RTP 단일 타임스탬프 모델의 구조적 충돌).

이 때문에 실전에서 RTMP→WebRTC 변환 경로는 송출단 설정을 Baseline / Main+bframes=0으로 유도하거나, 엣지에서 **트랜스코딩 (재인코딩)**으로 전환해야 합니다. 사례 분석: RTMP 라이브 송출의 B-frame이 만든 PTS rollback.


FFmpeg 명령어 구조

FFmpeg 명령어는 처음 보면 복잡해 보이지만, 구조를 이해하면 패턴이 보인다.

ffmpeg [global options] [input options] -i input [output options] output

예시 해부:
ffmpeg -y -ss 00:01:00 -i input.mp4 -t 30 -c:v libx264 -crf 23 -c:a aac output.mp4
  │     │      │          │          │       │            │        │       │
  │     │      │          │          │       │            │        │       └─ 출력 파일
  │     │      │          │          │       │            │        └─ 오디오 코덱: AAC
  │     │      │          │          │       │            └─ 품질: CRF 23 (낮을수록 고품질)
  │     │      │          │          │       └─ 비디오 코덱: H.264
  │     │      │          │          └─ 출력 길이: 30초
  │     │      │          └─ 입력 파일
  │     │      └─ 시작 지점: 1분 (입력 옵션으로 seek)
  │     └─ 기존 파일 덮어쓰기 (yes)
  └─ FFmpeg 실행

주요 옵션 정리:

옵션의미예시
-i입력 파일 지정-i input.mp4
-c:v비디오 코덱 지정-c:v libx264
-c:a오디오 코덱 지정-c:a aac
-c copy모든 스트림 복사 (remux)-c copy
-ss시작 시점-ss 00:01:00
-t출력 길이-t 30
-to종료 시점-to 00:02:00
-vf비디오 필터-vf scale=1280:720
-an오디오 스트림 제거-an
-vn비디오 스트림 제거-vn
-y출력 파일 덮어쓰기-y

-c copy vs Transcoding

FFmpeg을 쓸 때 가장 중요한 판단 중 하나다.

Remuxing (-c copy):
┌──────┐                           ┌──────┐
│  TS  │ ─→ Demux ─→ 스트림 그대로 ─→ Mux ─→ │  MP4 │
└──────┘                           └──────┘
소요 시간: 수 초     품질 손실: 0%     CPU: 거의 없음

Transcoding (-c:v libx264):
┌──────┐                                          ┌──────┐
│  TS  │ ─→ Demux ─→ Decode ─→ Filter ─→ Encode ─→ Mux ─→ │  MP4 │
└──────┘                                          └──────┘
소요 시간: 수 분~시간   품질 손실: 있음 (CRF로 제어)   CPU: 집약적
-c copy (Remuxing)Transcoding
속도매우 빠름 (I/O bound)느림 (CPU bound)
품질무손실 (원본 그대로)CRF/비트레이트로 제어
CPU거의 안 씀집약적
용도컨테이너 변환, 스트림 추출해상도 변경, 레이아웃 합성, 코덱 변환
필터 사용불가가능

결론: 코덱을 바꾸거나 필터를 적용할 필요가 없다면 항상 -c copy를 써라. 녹화 후처리의 대부분은 -c copy로 충분하다.


-map: 스트림 선택

입력이 여러 개일 때, 또는 특정 스트림만 선택해야 할 때 -map을 사용한다.

# 입력이 2개일 때: 영상은 첫 번째 파일, 오디오는 두 번째 파일에서
ffmpeg -i video.mp4 -i audio.m4a -map 0:v -map 1:a output.mp4
#                                       │        │
#                          입력 0의 비디오  입력 1의 오디오

# 문법: -map 입력번호:스트림타입
#   0:v = 첫 번째 입력의 비디오 스트림
#   1:a = 두 번째 입력의 오디오 스트림
#   0:s = 첫 번째 입력의 자막 스트림
#   0:0 = 첫 번째 입력의 첫 번째 스트림 (인덱스 직접 지정)

Individual 모드로 녹화한 파일을 합칠 때 이 패턴을 자주 쓰게 된다. 비디오 트랙과 오디오 트랙이 별도 파일로 저장되어 있을 경우, -map으로 각각 지정해서 하나의 파일로 mux한다.


FFmpeg 내부 5단계 파이프라인

FFmpeg이 내부적으로 어떻게 동작하는지 이해하면, 에러 메시지를 해석하고 옵션을 조합하는 데 도움이 된다.

┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
│ Demuxer  │→ │ Decoder  │→ │  Filter  │→ │ Encoder  │→ │  Muxer   │
│          │  │          │  │          │  │          │  │          │
│컨테이너  │  │코덱→Raw  │  │변환/합성 │  │Raw→코덱  │  │컨테이너  │
│열기/파싱 │  │데이터    │  │처리      │  │압축      │  │쓰기      │
└──────────┘  └──────────┘  └──────────┘  └──────────┘  └──────────┘

-c copy 사용 시 (Decoder/Filter/Encoder 단계 스킵):
┌──────────┐                                              ┌──────────┐
│ Demuxer  │──────────── 인코딩된 패킷 그대로 ──────────→  │  Muxer   │
└──────────┘                                              └──────────┘

트랜스코딩이 느린 이유가 여기 있다. Decode → Filter → Encode 세 단계를 거치는 동안 Raw 데이터(압축 해제된 원시 영상/음성)를 처리해야 하기 때문이다. 4K 영상 1시간을 트랜스코딩하면 수십 분이 걸리는 이유다.


기본 명령어 10가지

# 1. 파일 스트림 정보 확인 (항상 먼저 실행)
ffprobe -v quiet -show_streams -of json input.mp4

# 2. 컨테이너 변환 (TS → MP4, 무손실 remux)
ffmpeg -i input.ts -c copy output.mp4

# 3. M3U8 → MP4 (HLS 플레이리스트를 단일 파일로)
ffmpeg -i recording.m3u8 -c copy output.mp4

# 4. 오디오만 추출 (비디오 제거)
ffmpeg -i input.mp4 -vn -c:a copy audio.m4a

# 5. 비디오만 추출 (오디오 제거)
ffmpeg -i input.mp4 -an -c:v copy video.mp4

# 6. 구간 잘라내기 (1분~2분 구간, 무손실)
ffmpeg -ss 00:01:00 -to 00:02:00 -i input.mp4 -c copy clip.mp4

# 7. 해상도 변경 (720p로 다운스케일)
ffmpeg -i input.mp4 -vf scale=1280:720 -c:a copy resized.mp4

# 8. 여러 파일 이어붙이기 (concat)
# filelist.txt: 각 줄에 "file '파일명'" 형식으로 작성
ffmpeg -f concat -safe 0 -i filelist.txt -c copy merged.mp4

# 9. GIF 생성 (10초 지점부터 5초 구간)
ffmpeg -ss 00:00:10 -t 5 -i input.mp4 -vf "fps=10,scale=480:-1" output.gif

# 10. 썸네일 추출 (30초 지점 프레임 1장)
ffmpeg -ss 00:00:30 -i input.mp4 -frames:v 1 thumbnail.jpg

명령어 8번에서 filelist.txt 형식은 다음과 같다:

file 'segment_001.ts'
file 'segment_002.ts'
file 'segment_003.ts'

HLS 녹화 후 여러 TS 세그먼트를 하나의 MP4로 합칠 때 이 패턴을 쓴다.


"언제 -c copy, 언제 트랜스코딩?" 판단 플로우차트

              내가 하려는 작업은?
                      │
        ┌─────────────┼─────────────┐
        │             │             │
   컨테이너만      스트림만        필터/변환
   바꾸기          추출           적용
        │             │             │
     -c copy       -c copy      트랜스코딩 필요
     (무손실)      (무손실)           │
                              ┌──────┼──────┐
                              │      │      │
                         해상도   레이아웃  코덱
                         변경     합성     변환
                              │      │      │
                         -vf scale  -filter_complex  -c:v libx264
                                    (복수 입력)      -c:v libx265

실무 경험상 녹화 후처리의 약 80%는 -c copy로 해결된다. TS → MP4 변환, M3U8 → MP4 변환, 스트림 추출, 파일 연결. 트랜스코딩이 필요한 경우는 해상도 변경이나 Individual 모드 트랙을 특정 레이아웃으로 합성할 때 정도다.


핵심 요약

  • Container는 포맷(상자), Codec은 압축 방식(포장법): 같은 코덱이 여러 컨테이너에 담길 수 있다
  • Stream은 컨테이너 내부의 개별 트랙: 하나의 파일에 비디오/오디오/자막 스트림이 공존한다
  • Remuxing (-c copy)은 무손실, 초고속: 컨테이너만 바꿀 때는 반드시 -c copy를 사용하라
  • Transcoding은 필터가 필요할 때만: 디코딩 → 처리 → 인코딩을 거치므로 CPU 집약적이다
  • -map으로 스트림을 정밀 선택: 멀티 입력이나 특정 트랙 추출 시 필수 옵션이다
  • ffprobe를 습관처럼 먼저 실행하라: 파일 구조를 모르고 명령어를 작성하면 예상치 못한 결과가 나온다

시리즈 네비게이션

실시간 통화, 어떻게 녹화하는가 시리즈

  1. 실시간 통화, 왜 녹화해야 하는가 — Cloud Recording Architecture
  2. 내 서버에서 직접 녹화한다면 — On-Premise vs Cloud
  3. 녹화 모드 해부 — Individual, Mix, Web의 내부 파이프라인
  4. M3U8과 TS 파일의 모든 것 — HLS Deep Dive
  5. FFmpeg, 미디어의 스위스 아미 나이프 ← 현재 글
  6. FFmpeg 실전 파이프라인 — 녹화에서 VOD까지

© 2026 Frank Kim. All rights reserved.