💡 한 줄 요약
- 이 글에서는
AVCodecParameters가 미디어 타입에 따라 읽어야 할 필드가 달라진다는 점과,AVStream.time_base와AVFormatContext.duration이 서로 다른 시간 단위를 쓰는 이유를probe.cpp의print_stream,print_format코드를 바탕으로 설명한다.
🚨 1. 문제 상황과 대상 독자
AVCodecParameters를 다루다 보면 보통 두 지점에서 많이 헷갈린다.
- 비디오 스트림과 오디오 스트림이 서로 다른 필드를 쓰는데, 타입을 확인하지 않고 접근했다가 의미 없는 값을 읽는 경우
AVStream.duration과AVFormatContext.duration을 같은 단위로 생각해 시간 계산이 틀어지는 경우
⚠️ 자주 부딪히는 문제
FFmpeg 5.1 이전 예제를 참고하다 보면 par->channels, par->channel_layout을 그대로 읽는 코드가 아직도 많이 보인다.
하지만 최신 FFmpeg에서는 이 필드들이 deprecated 상태라, 그대로 가져다 쓰면 컴파일 경고가 나거나 기대한 값과 다르게 동작할 수 있다.
이 글은 다음과 같은 분들을 염두에 두고 썼다.
- 1편에서
AVFormatContext,AVStream초기화 흐름까지 따라온 분 - FFmpeg로 미디어 메타데이터를 직접 읽고 있고, 최근
channels,channel_layout이 deprecated되었다는 사실을 접한 분
🛠️ 2. 사전 지식과 개발 환경
2.1 사전 지식
- 1편 「FFmpeg
AVFormatContext, RAII로 감싸야 하는 이유」에서 다룬avformat_open_input,avformat_find_stream_info흐름 AVFormatContext.streams[i]가AVStream이고, 그 안의codecpar가AVCodecParameters라는 구조
2.2 개발 환경
| 분류 | 기술 스택 / 도구 | 버전 | 비고 |
| OS / 아키텍처 | Ubuntu | x86_64 | |
| FFmpeg | libavformat / libavutil 등 | 6.1.1-3ubuntu5 | Ubuntu 패키지 빌드 |
| 컴파일러 | g++ | 13.3.0 | |
| 언어 표준 | C++20 이상 |
🔍 3. 핵심 개념과 원인
3.1 AVCodecParameters는 타입에 따라 읽는 필드가 달라진다
AVCodecParameters는 하나의 구조체 안에 여러 미디어 타입의 필드가 함께 들어 있다.
그래서 어떤 필드가 실제로 의미를 가지는지는 codec_type에 따라 달라진다. 사용 방식만 놓고 보면 판별 유니온(discriminated union)에 가깝다.
probe.cpp의 분기 코드는 이 점을 잘 보여준다.
if (par->codec_type == AVMEDIA_TYPE_VIDEO) {
std::printf(" resolution : %dx%d\n", par->width, par->height);
std::printf(" pix_fmt : %s\n",
av_get_pix_fmt_name((AVPixelFormat)par->format));
if (st->avg_frame_rate.num)
std::printf(" frame_rate : %.3f fps\n", av_q2d(st->avg_frame_rate));
} else if (par->codec_type == AVMEDIA_TYPE_AUDIO) {
std::printf(" sample_rate: %d Hz\n", par->sample_rate);
std::printf(" sample_fmt : %s\n",
av_get_sample_fmt_name((AVSampleFormat)par->format));
char lb[64] = {0};
av_channel_layout_describe(&par->ch_layout, lb, sizeof(lb));
std::printf(" ch_layout : %s (%d ch)\n", lb, par->ch_layout.nb_channels);
}
예를 들어 format 필드만 봐도 그렇다.
- 비디오에서는
AVPixelFormat으로 해석해야 하고 - 오디오에서는
AVSampleFormat으로 해석해야 한다
즉 codec_type을 먼저 확인하지 않은 채 format을 읽으면, 값은 있어도 의미를 잘못 해석하게 된다.
3.2 왜 channels, channel_layout 대신 ch_layout을 써야 할까
FFmpeg 5.1 이전에는 오디오 채널 정보를 다음 두 필드로 표현했다.
int channels: 채널 수uint64_t channel_layout: 채널 배치를 나타내는 비트마스크
이 방식은 단순한 경우에는 쓸 만했지만, 점점 한계가 드러났다.
- 비트마스크만으로는 5.1.4, 7.1.4처럼 높이 채널이 포함된 확장 레이아웃이나, 표준에 없는 임의 채널 순서를 표현하기 어렵다.
channels와channel_layout이 서로 다른 정보를 담고 있어, 둘이 어긋나는 경우를 항상 따로 확인해야 한다.
이를 보완하기 위해 FFmpeg 5.1부터 AVChannelLayout 구조체와 ch_layout 필드가 도입됐고, 기존 channels, channel_layout은 deprecated되었다.
AVChannelLayout은 다음과 같은 정보를 함께 담는다.
- 채널 배치 방식(
order) - 채널 수(
nb_channels) - 비트마스크 또는 커스텀 채널 배열
즉 예전보다 훨씬 일반적인 방식으로 채널 구성을 표현할 수 있게 된 셈이다.
그래서 probe.cpp도 par->channels를 직접 읽지 않고, av_channel_layout_describe(&par->ch_layout, ...)로 사람이 읽기 쉬운 문자열로 바꿔 출력한다.
이 함수는 내부 표현이 네이티브 비트마스크이든, 커스텀 배열이든 상관없이 일관된 문자열을 돌려준다.
💭 여기서 문자열화는 어디까지나 출력용이다.
AVChannelLayout을 실제로 설정할 때는 av_channel_layout_default(), AV_CHANNEL_LAYOUT_5POINT1 같은 표준 매크로, av_channel_layout_from_string() 같은 API를 상황에 맞게 사용하면 된다.
즉 구조체 내부가 문자열로 저장되는 것은 아니다.
N.M 표기에서 N은 수평 방향 스피커 수, M은 서브우퍼(LFE) 수를 뜻한다. 여기에 세 번째 숫자가 붙으면(예: 5.1.4, 7.1.4) 천장 방향 또는 머리 위쪽에 배치된 높이 스피커 수를 의미한다. Dolby Atmos, DTS:X 같은 몰입형 오디오 포맷에서 자주 쓰인다.
3.3 왜 time_base는 스트림마다 다르고, 컨테이너 레벨 시간은 고정 단위를 쓸까
probe.cpp에는 시간을 계산하는 코드가 두 가지 나온다.
// 스트림 레벨
if (st->duration != AV_NOPTS_VALUE)
std::printf(" duration : %.3f s\n",
st->duration * av_q2d(st->time_base));
// 컨테이너 레벨
if (fc->duration != AV_NOPTS_VALUE)
std::printf(" duration : %.3f s\n", fc->duration / double(AV_TIME_BASE));
계산식이 다른 이유는 두 값이 애초에 서로 다른 시간 단위를 쓰기 때문이다.
AVStream.time_base
각 스트림에 맞는 시간 단위다.
예를 들어 어떤 비디오 스트림은1/90000, 어떤 오디오 스트림은1/48000처럼 설정될 수 있다.
스트림의 타임스탬프를 얼마나 세밀하게 표현해야 하는지에 따라 값이 달라진다.AVFormatContext.duration
컨테이너 전체 길이를 나타내는 값이다.
이 값은 항상AV_TIME_BASE를 기준으로 표현되며, 단위는 마이크로초(1/1000000)다.
왜 이렇게 나뉘어 있을까?
- 스트림 레벨에서는 각 스트림에 맞는 정밀도가 중요하고
- 컨테이너 레벨에서는 비디오, 오디오, 자막처럼 서로 다른 스트림을 하나의 기준으로 비교할 수 있어야 하기 때문이다
즉 스트림 레벨은 원본 스트림의 정밀도, 컨테이너 레벨은 서로 다른 스트림 사이의 공통 기준을 우선한 설계라고 보면 된다.
그래서 둘을 섞어 쓰면 문제가 생긴다. 예를 들어
- 스트림
duration을AV_TIME_BASE로 나누거나 - 컨테이너
duration에 스트림time_base를 곱하면
대부분 잘못된 값이 나온다.
💭 컨테이너 레벨 시간값은 실전에서도 자주 쓰인다.
예를 들어 플레이어에서 전체 재생 시간을 표시할 때, 또는 서로 다른 스트림 길이를 같은 기준으로 비교할 때 유용하다.
탐색 API에서도 이런 공통 단위가 필요하다.
✅ 4. 해결 방법과 검증
4.1 해결 방법
정리하면 다음 원칙만 지켜도 실수를 크게 줄일 수 있다.
AVCodecParameters는 반드시codec_type으로 먼저 분기한 뒤, 해당 타입에 맞는 필드만 읽는다.- 채널 정보는
ch_layout만 사용하고, 필요하면av_channel_layout_describe()로 문자열로 바꿔 출력한다. - 시간을 초 단위로 바꿀 때는 값의 출처를 먼저 확인한다.
AVStream에서 온 값이면 해당 스트림의time_baseAVFormatContext에서 온 값이면AV_TIME_BASE
둘은 절대 섞어 쓰지 않는다.
4.2 결과 검증
검증은 ffprobe -v quiet -show_streams -show_format <file> 출력과 probe.cpp 출력을 나란히 비교하는 방식으로 할 수 있다.
해상도, 샘플레이트, 채널 레이아웃, duration 값이 일치하는지 확인하면 된다.
실제로 CAM1_Edit_noB.mkv 파일로 비교한 결과는 다음과 같다.
| 항목 | ffprobe | probe.cpp |
| resolution | 1920x1080 | 1920x1080 |
| pix_fmt | yuv420p | yuv420p |
| avg_frame_rate | 30/1 | 30.000 fps |
| sample_rate | 48000 Hz | 48000 Hz |
| ch_layout | stereo (channels=2) | stereo (2 ch) |
| 컨테이너 duration | 95.766000 s | 95.766 s |
확인한 값은 모두 일치했다.
💭 실제로 보면 흥미로운 점
이 파일은 스트림 레벨 duration(duration_ts)이 두 스트림 모두 N/A로 나온다.
즉 MKV라고 해서 항상 AVStream.duration이 채워지는 것은 아니다.
대신 각 스트림의 TAG:DURATION 메타데이터에는 비디오 00:01:35.766, 오디오 00:01:35.722처럼 값이 들어 있고, 둘 사이에는 44ms 차이가 난다.
오디오 스트림의 start_time이 0.021(21ms)인 점까지 함께 보면, 오디오가 약간 늦게 시작하고 그만큼 끝나는 시점도 달라진 것으로 해석할 수 있다.
반면 컨테이너 레벨 duration은 이런 미세한 차이와 별개로 파일 전체를 대표하는 하나의 값으로 유지된다.
💡 5. 조금 더 알아둘 점
5.1 적용 범위
AVChannelLayout,ch_layoutAPI는 FFmpeg 5.1부터 도입되었다.
그 이전 버전을 대상으로 한다면 이 글의ch_layout관련 내용은 그대로 적용되지 않고, 기존channels,channel_layout조합을 써야 한다.
5.2 자주 하는 실수
format필드를 미디어 타입 확인 없이 무조건AVPixelFormat또는AVSampleFormat으로 캐스팅하는 경우- 스트림
duration을AV_TIME_BASE로 나누거나, 컨테이너duration에 스트림time_base를 곱하는 식으로 단위를 섞는 경우
5.3 FAQ
- Q.
avg_frame_rate가 0으로 나온다.
A. 가변 프레임레이트(VFR) 스트림이거나, 헤더 정보만으로는 프레임레이트를 추정하기 어려운 경우 그럴 수 있다. 이런 때는r_frame_rate를 참고하거나, 실제 패킷 타임스탬프 간격을 분석해야 한다.
🏁 6. 마무리
AVCodecParameters는 한 구조체에 여러 타입의 정보가 함께 들어 있는 만큼, 반드시 타입을 먼저 확인하고 그에 맞는 필드만 읽어야 한다.
오디오 채널 정보는 FFmpeg 5.1부터 ch_layout 중심으로 정리되었고, 시간값은 스트림 레벨과 컨테이너 레벨이 서로 다른 목적을 위해 다른 단위를 쓴다.
이 세 가지만 분명히 잡고 있어도, 스트림 메타데이터를 읽을 때 생기는 실수는 많이 줄어든다.
다음 글에서는 disposition 플래그와 metadata, 그리고 av_find_best_stream으로 원하는 트랙을 고르는 과정을 다룰 예정이다.
참고 자료
예제 코드
decode-render/tools/probe.cpp at main · moony211/decode-render
디코딩과 렌더링을 연구한다. Contribute to moony211/decode-render development by creating an account on GitHub.
github.com