💡 한 줄 요약
- 이 글에서는
avformat_open_input을 C API 그대로 사용할 때 생기기 쉬운 double free와 메모리 누수 문제를, RAII로 소유권을 관리해 해결하는 방법을 설명한다. - 이어서
avformat_find_stream_info의 비용이 컨테이너마다 왜 다른지도 함께 정리한다.
🚨 1. 문제 상황
FFmpeg의 AVFormatContext는 컨테이너를 열고 스트림 정보를 담는 핵심 구조체다.
문제는 이 구조체의 수명 관리 방식이 생각보다 직관적이지 않다는 점이다.
AVFormatContext *raw = nullptr;
int ret = avformat_open_input(&raw, path, nullptr, nullptr);
if (ret < 0) {
// raw는 이미 내부에서 해제되어 nullptr가 된다.
// 여기서 avformat_close_input(&raw)를 다시 호출할 필요는 없다.
return 1;
}
avformat_open_input이 실패하면 FFmpeg가 내부에서 컨텍스트를 정리하고 *ps를 nullptr로 되돌린다.
이 사실을 모르고 실패 경로에서도 정리 코드를 따로 넣거나, 반대로 성공 경로에서 여러 return이 흩어져 있어 해제를 빠뜨리면 문제가 생긴다.
즉, C API를 직접 다룰 때 흔히 마주치는 실수는 크게 두 가지다.
- 실패 경로 처리 방식에 대한 오해
- 성공 이후 수동 해제 코드가 여러 분기에서 누락되는 문제
⚠️ 자주 겪는 문제
디버깅하다 보면 “왜 가끔만 크래시가 나지?” 싶은 경우가 있다.
이런 문제를 따라가 보면, 실패 경로에서 이미 정리된 포인터를 다시 건드리거나, 반대로 특정 조기 반환 경로에서 해제를 빠뜨린 경우가 적지 않다.
특히 여러 파일을 순차 처리하는 배치 코드에서는 이런 버그가 늦게 드러나기 쉽다.
이 글에서는 AVFormatContext의 소유권을 RAII로 넘겨 이런 문제를 구조적으로 막는 방법을 먼저 살펴보고, 이어서 avformat_find_stream_info가 컨테이너에 따라 왜 비용 차이가 나는지도 정리한다.
🛠️ 2. 개발 환경
| 분류 | 기술 스택 / 도구 | 버전 | 비고 |
| OS / 아키텍처 | Ubuntu | x86_64 | |
| FFmpeg | libavformat 등 | 6.1.1-3ubuntu5 | Ubuntu 패키지 빌드 |
| 컴파일러 | g++ | 13.3.0 | |
| 언어 표준 | C++20 이상 | 코루틴 사용 전제 |
✅ 3. 해결 방법
3.1 먼저 avformat_open_input의 소유권 규칙을 분명히 이해한다
avformat_open_input의 시그니처는 다음과 같다.
int avformat_open_input(AVFormatContext **ps, const char *url,
const AVInputFormat *fmt, AVDictionary **options);
이 함수의 동작 규칙은 다음 두 가지로 정리할 수 있다.
*ps가nullptr이면 내부에서avformat_alloc_context()를 호출해 새 컨텍스트를 만든다.- 함수가 실패하면, 방금 만든 컨텍스트는 내부에서 이미 해제되고
*ps는nullptr로 정리된다.
즉 호출자가 기억해야 할 핵심은 하나다.
해제 책임은 성공했을 때만 생긴다.
실패했을 때는 호출자가 따로 정리할 것이 없다.
3.2 성공한 직후 곧바로 RAII 객체로 소유권을 넘긴다
이 규칙을 코드에서 확실하게 지키는 가장 좋은 방법은, avformat_open_input이 성공한 직후 raw 포인터를 바로 RAII 객체에 넘기는 것이다.
AVFormatContext *raw = nullptr;
int ret = avformat_open_input(&raw, path, nullptr, nullptr); // 실패 시 내부에서 정리됨
if (ret < 0) {
std::fprintf(stderr, "open failed '%s': %s\n", path, ff::errstr(ret).c_str());
return 1;
}
ff::FormatCtx fmt{raw}; // 여기서부터 RAII가 소유
ff::FormatCtx는 raw 포인터를 받아 소유권을 넘겨받고, 소멸자에서 avformat_close_input()을 호출하는 얇은 래퍼라고 보면 된다. 예를 들면 다음과 같은 형태다.
class FormatCtx {
public:
explicit FormatCtx(AVFormatContext *ctx) noexcept : ctx_(ctx) {}
~FormatCtx() { if (ctx_) avformat_close_input(&ctx_); }
FormatCtx(const FormatCtx &) = delete;
FormatCtx &operator=(const FormatCtx &) = delete;
FormatCtx(FormatCtx &&other) noexcept : ctx_(other.ctx_) { other.ctx_ = nullptr; }
AVFormatContext *get() const noexcept { return ctx_; }
AVFormatContext *operator->() const noexcept { return ctx_; }
private:
AVFormatContext *ctx_ = nullptr;
};
이렇게 해두면 ff::FormatCtx fmt{raw}; 이후에는 함수 안에 return이 여러 군데 있든, 예외가 발생하든, 마지막에 소멸자가 avformat_close_input()을 호출해준다.
정리하면 장점은 분명하다.
- 실패 경로는 RAII 객체가 만들어지기 전에 끝나므로 불필요한 정리 코드를 고민할 필요가 없다.
- 성공 경로는 RAII가 해제를 맡으므로 누수 가능성이 크게 줄어든다.
- 복사를 막고 이동만 허용하면, 같은 컨텍스트를 두 번 해제하는 실수도 예방할 수 있다.
💭 C++20 코루틴을 쓴다면 이 패턴을 비동기 흐름으로 확장하고 싶을 수 있다.
다만 avformat_open_input 자체는 동기식 blocking 호출이므로, 코루틴으로 감싸려면 별도 스레드나 io_uring 같은 하위 계층이 필요하다.
이 부분은 이번 글의 범위를 벗어나므로 여기서는 다루지 않는다.
3.3 avformat_find_stream_info와 컨테이너별 비용 차이
컨텍스트를 안전하게 연 뒤에는 보통 avformat_find_stream_info를 호출하게 된다.
// 헤더만으로 부족한 스트림 정보를 일부 패킷 분석으로 보완
ret = avformat_find_stream_info(fmt.get(), nullptr);
if (ret < 0) {
std::fprintf(stderr, "find_stream_info failed: %s\n", ff::errstr(ret).c_str());
return 1;
}
avformat_open_input은 기본적으로 컨테이너를 열고 헤더를 읽는 단계에 가깝다.
그런데 컨테이너마다 헤더에 담고 있는 정보의 양이 다르다. 이 차이 때문에 avformat_find_stream_info의 비용도 달라진다.
- MKV(Matroska)
트랙별 코덱 정보가 헤더에 비교적 충실하게 들어 있는 편이다.
그래서avformat_find_stream_info가 추가로 많은 패킷을 읽지 않아도 되는 경우가 많고, 대체로 비용이 낮다. - MPEG-TS, RTSP
스트리밍이나 방송 환경을 염두에 둔 포맷이라 헤더 정보가 상대적으로 제한적이다.
해상도, 프레임레이트 같은 코덱 파라미터가 헤더만으로는 충분하지 않은 경우가 많다.
이때avformat_find_stream_info는 실제 패킷을 더 읽고, 경우에 따라 임시 디코딩까지 하면서 필요한 정보를 추론한다.
그래서 비용이 더 크고, 입력에 따라 지연 시간이 눈에 띄게 늘어날 수 있다.
이 비용은 AVFormatContext의 probesize, max_analyze_duration 같은 옵션으로 어느 정도 조절할 수 있다.
다만 이 부분은 설명할 내용이 많아, 별도 글로 다루는 편이 적절하다.
🎯 4. 적용 범위와 자주 하는 실수
적용 범위
- 이 패턴은 FFmpeg 6.x 기준으로 설명했지만,
avformat_open_input과avformat_close_input의 기본 소유권 규칙은 오래전부터 유지되어 왔다. 따라서 구버전에도 같은 방식으로 적용할 수 있다. - 다만 멀티스레드 환경에서 하나의
AVFormatContext를 여러 스레드가 동시에 사용하는 문제까지 해결해주지는 않는다.AVFormatContext자체는 기본적으로 스레드 세이프하지 않으므로, 그런 경우에는 별도의 동기화가 필요하다.
❌ 자주 하는 실수
avformat_open_input의 반환값을 확인하지 않고 곧바로raw나fmt.get()에 접근하는 경우- 성공 후에도 raw 포인터를 계속 직접 들고 다니면서, 예외나 조기 반환 경로에서 해제를 빠뜨리는 경우
avformat_find_stream_info가 실패했는데도codecpar를 신뢰하고 다음 단계로 넘어가는 경우
🏁 5. 마무리
avformat_open_input은 규칙 자체는 단순하지만, 실제 코드에서는 놓치기 쉽다.
핵심은 성공한 시점부터 해제 책임이 생긴다는 점을 정확히 이해하고, 그 즉시 RAII로 소유권을 넘기는 것이다.
이렇게 해두면 이후의 실패 경로나 예외 상황에서도 해제 코드를 따로 신경 쓸 필요가 없다.
여기에 더해 avformat_find_stream_info의 비용이 컨테이너 특성에 따라 달라진다는 점까지 이해하고 있으면, 다음 단계인 AVCodecParameters나 디코더 초기화로 넘어갈 때도 훨씬 맥락을 잡기 쉬워진다.
참고 자료