서론

멀티미디어 프레임워크(Multimedia Framework)는 다양한 멀티미디어 데이터를 처리하기 위한 도구와 라이브러리를 제공하는 소프트웨어 플랫폼으로, 현재 인터넷 전반에 걸쳐 광범위하게 사용되고 있다.

이 중 가장 대표적인 멀티미디어 프레임워크인 FFmpeg에 대해 알아보자.


FFmpeg

image

FFmpeg은 멀티미디어 기능을 지원하는 도구 및 라이브러리로 구성된 오픈소스 프로젝트이다.

FFmpeg은 주로 비디오/오디오/이미지를 인코딩(encoding), 디코딩(decoding), 먹싱(muxing), 디먹싱(demuxing)하는 용도로 많이 사용된다.

FFmpeg은 빌드 방법과 외부 라이브러리에 따라 LGPL-2.1 이상 혹은 GPL-2.0 이상의 라이선스가 적용된다.

라이선스 정보는 FFmpeg License and Legal Considerations 참고


FFmpeg 도구

FFmpeg은 사용자들이 명령어를 통해 손쉽게 사용할 수 있도록 세 가지 CLI 도구를 제공한다.

  • ffmpeg : 멀티미디어를 처리하는 CLI 도구
  • ffprobe : 멀티미디어 정보를 출력하는 CLI 도구
  • ffplay : 멀티미디어를 재생하는 CLI 도구

도구 사용법은 FFmpeg 공식 문서 참고


FFmpeg 라이브러리

동영상 관련 개발을 진행하다보면 FFmpeg을 명령어가 아닌 라이브러리로 직접 사용해야 하는 경우가 있다.

FFmpeg 라이브러리 종류와 데이터 구조에 대해 알아보자.

FFmpeg 라이브러리 종류

FFmpeg은 동영상 처리 과정에 필요한 다양한 라이브러리들을 제공한다.

  • libavcodec : 인코딩, 디코딩 기능을 제공하는 라이브러리
  • libavformat : 먹싱, 디먹싱 기능을 제공하는 라이브러리
  • libavutil : 다양한 유틸리티(난수 생성기, 수학 루틴 등) 기능을 제공하는 라이브러리
  • libavdevice : 장치를 통한 I/O(캡처, 렌더링 등) 기능을 제공하는 라이브러리
  • libavfilter : 미디어 필터 기능을 제공하는 라이브러리
  • libswscale : 이미지 처리(스케일링, 픽셀 포맷 변환 등) 기능을 제공하는 라이브러리
  • libswresample : 오디오 처리(리샘플링 등) 기능을 제공하는 라이브러리

아래 그림은 트랜스먹싱(transmuxing), 트랜스코딩(transcoding) 과정을 수행할 때 사용하는 라이브러리들이다.

image

FFmpeg 라이브러리 데이터 구조

FFmpeg 라이브러리에서는 컨테이너(container), 스트림(stream), 코덱(codec)을 관리하기 위해 이를 추상화한 컨텍스트 구조체가 존재한다.

image

  • AVFormatContext : 컨테이너 정보를 저장한 구조체
    • 파일로부터 읽은 컨테이너 내용을 저장하거나 새로 생성한 컨테이너를 파일에 쓰기 위한 용도로 사용된다.
    • AVFormatContext 내부에는 적어도 하나 이상의 AVStream이 존재한다.
  • AVStream : 스트림(코덱에 의해 압축된 일련의 데이터) 정보를 저장한 구조체
    • 시간과 관련된 정보(e.g. fps, timebase)를 포함하고 있다.
    • AVStream 내부에는 적어도 하나 이상의 AVCodecParameters(사용된 코덱 정보를 저장한 구조체)가 존재한다.
  • AVCodecContext : 인코딩, 디코딩 작업(과정)에 필요한 정보를 저장한 구조체
    • AVCodec(사용할 코덱에 대한 정보를 저장한 구조체)를 통해 초기화된다.
    • 비디오 코덱은 해상도, 픽셀 포맷과 같은 정보를 저장한다.
    • 오디오 코덱은 샘플레이트, 채널 개수와 같은 정보를 저장한다.

또한, 패킷(packet), 프레임(frame) 데이터를 저장하고 관리하기 위한 구조체도 존재한다.

  • AVPacket : 압축(인코딩)된 비디오/오디오 데이터를 저장하는 구조체
  • AVFrame : 압축 해제(디코딩)된 비디오/오디오 데이터를 저장하는 구조체

image

패킷(AVPacket)과 프레임(AVFrame)은 코덱(AVCodecContext)를 통해 상호 변환(인코딩, 디코딩)될 수 있다.


FFmpeg 라이브러리 실습

그러면 이제 FFmpeg 라이브러리를 사용하는 코드를 직접 작성해보자.

FFmpeg 라이브러리예제 코드를 바탕으로 작성했다.

1. Scanning

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
#include <iostream>

int main(int argc, const char** argv)
{
  // FFmpeg 라이브러리의 로그 레벨 지정
  av_log_set_level(AV_LOG_DEBUG);

  if (argc < 2)
  {
    std::cout << "Couldn't find video file\n";
    return 0;
  }

  // 컨테이너 정보를 포함하는 AVFormatContext 구조체에 메모리 할당
  AVFormatContext* av_format_ctx = avformat_alloc_context();

  // 만약 AVFormatContext 구조체에 메모리를 할당하지 않았다면 avformat_open_input() 메서드 내부에서 할당
  // 파일로부터 읽은 컨테이너 정보를 AVFormatContext 구조체에 저장
  if (avformat_open_input(&av_format_ctx, argv[1], nullptr, nullptr) < 0)
  {
    std::cout << "Couldn't open video file\n";
    return 0;
  }

  // 스트림 관련 정보를 읽고 AVFormatContext 구조체에 저장
  if (avformat_find_stream_info(av_format_ctx, nullptr) < 0)
  {
    std::cout << "Failed to retrieve input stream information\n";
    return 0;
  }

  for (int index = 0; index < av_format_ctx->nb_streams; ++index)
  {
    // AVCodecParameters 구조체는 스트림에서 사용된 코덱 정보를 포함
    AVCodecParameters* av_codec_params = av_format_ctx->streams[index]->codecpar;
    if (av_codec_params->codec_type == AVMEDIA_TYPE_VIDEO)
    {
      std::cout << "-------- video info --------\n";
      std::cout << "codec_id : " << av_codec_params->codec_id << "\n";
      std::cout << "bitrate : " << av_codec_params->bit_rate << "\n";
      std::cout << "width : " << av_codec_params->width << ", height : " << av_codec_params->height << "\n";
    }
    else if (av_codec_params->codec_type == AVMEDIA_TYPE_AUDIO)
    {
      std::cout << "-------- audio info --------\n";
      std::cout << "codec_id : " << av_codec_params->codec_id << "\n";
      std::cout << "bitrate : " << av_codec_params->bit_rate << "\n";
      std::cout << "sample_rate : " << av_codec_params->sample_rate << "\n";
      std::cout << "number of channels : " << av_codec_params->channels << "\n";
    }
  }

  if (av_format_ctx)
  {
    // AVFormatContext 구조체에 할당한 메모리 해제
    avformat_close_input(&av_format_ctx);
  }

  return 0;
}

2. Demuxing

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
#include <iostream>

struct FileContext
{
  AVFormatContext* av_format_ctx;
  int v_index;
  int a_index;
};

FileContext input_ctx;

int open_input(const char* filename);
void release();

int main(int argc, const char** argv)
{
  av_log_set_level(AV_LOG_DEBUG);

  if (argc < 2)
  {
    std::cout << "Couldn't find video file\n";
    return 0;
  }

  if (open_input(argv[1]) < 0)
  {
    release();
    return 0;
  }

  AVPacket av_packet;
  while (true)
  {
    // AVFormatContext 구조체로부터 패킷을 순서대로 읽어 AVPacket 구조체에 저장
    int ret = av_read_frame(input_ctx.av_format_ctx, &av_packet);
    if (ret == AVERROR_EOF)
    {
      // 더 이상 읽어올 패킷이 없으면 종료
      std::cout << "End of frame\n";
      break;
    }

    if (av_packet.stream_index == input_ctx.v_index)
    {
      std::cout << "Video packet\n";
    }
    else if (av_packet.stream_index == input_ctx.a_index)
    {
      std::cout << "Audio packet\n";
    }

    // av_packet_unref(과거에는 av_free_packet) 함수는 패킷을 다 쓴 후 필드를 리셋하는 함수 (내부 청소용)
    // av_packet_free 함수는 동적 할당한 패킷의 메모리를 해제하는 함수 (할당 해제용)
    av_packet_unref(&av_packet);
  }

  release();

  return 0;
}

int open_input(const char* filename)
{
  input_ctx.av_format_ctx = nullptr;
  input_ctx.v_index = input_ctx.a_index = -1;

  if (avformat_open_input(&input_ctx.av_format_ctx, filename, nullptr, nullptr) < 0)
  {
    std::cout << "Couldn't open video file\n";
    return -1;
  }

  if (avformat_find_stream_info(input_ctx.av_format_ctx, nullptr) < 0)
  {
    std::cout << "Failed to retrieve input stream information\n";
    return -1;
  }

  for (int index = 0; index < input_ctx.av_format_ctx->nb_streams; ++index)
  {
    AVCodecParameters* av_codec_params = input_ctx.av_format_ctx->streams[index]->codecpar;
    if (av_codec_params->codec_type == AVMEDIA_TYPE_VIDEO && input_ctx.v_index < 0)
    {
      input_ctx.v_index = index;
    }
    else if (av_codec_params->codec_type == AVMEDIA_TYPE_AUDIO && input_ctx.a_index < 0)
    {
      input_ctx.a_index = index;
    }
  }

  if (input_ctx.v_index < 0 && input_ctx.a_index < 0)
  {
    std::cout << "No video or audio stream found\n";
    return -1;
  }

  return 0;
}

void release()
{
  if (input_ctx.av_format_ctx)
  {
    avformat_close_input(&input_ctx.av_format_ctx);
  }
}

3. remuxing

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
#include <iostream>

struct FileContext
{
  AVFormatContext* av_format_ctx;
  int v_index;
  int a_index;
};

FileContext input_file_ctx, output_file_ctx;

int open_input(const char* filename);
int create_output(const char* filename);
void release();

int main(int argc, const char** argv)
{
  av_log_set_level(AV_LOG_DEBUG);

  if (argc < 3)
  {
    std::cout << "Not enough arguments entered.\n";
    return 0;
  }

  if (open_input(argv[1]) < 0)
  {
    release();
    return 0;
  }

  if (create_output(argv[2]) < 0)
  {
    release();
    return 0;
  }

  // 파일에 대한 정보 출력
  av_dump_format(output_file_ctx.av_format_ctx, 0, output_file_ctx.av_format_ctx->url, 1);

  AVPacket av_packet;
  // 입력 스트림에서 패킷을 하나씩 읽어 출력 스트림으로 복사
  while (true)
  {
    int ret = av_read_frame(input_file_ctx.av_format_ctx, &av_packet);
    if (ret == AVERROR_EOF)
    {
      std::cout << "End of frame\n";
      break;
    }

    if (av_packet.stream_index != input_file_ctx.v_index && av_packet.stream_index != input_file_ctx.a_index)
    {
      av_packet_unref(&av_packet);
      continue;
    }

    AVStream* in_stream = input_file_ctx.av_format_ctx->streams[av_packet.stream_index];
    AVStream* out_stream = output_file_ctx.av_format_ctx->streams[av_packet.stream_index];

    // 패킷의 pts, dts, duration을 입력 스트림의 시간 기준에서 출력 스트림의 시간 기준으로 재조정
    av_packet.pts = av_rescale_q_rnd(av_packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF);
    av_packet.dts = av_rescale_q_rnd(av_packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF);
    av_packet.duration = av_rescale_q(av_packet.duration, in_stream->time_base, out_stream->time_base);

    // pos는 스트림의 byte 위치를 의미 (알 수 없는 경우 -1로 표시)
    av_packet.pos = -1;

    // 재조정된 패킷 정보를 AVFormatContext 구조체에 입력
    if (av_interleaved_write_frame(output_file_ctx.av_format_ctx, &av_packet) < 0)
    {
      std::cout << "Error occurred when writing packet into file\n";
      break;
    }

    av_packet_unref(&av_packet);
  }

  // AVPacket 구조체를 쓰는 시점에 정리하지 못한 정보들을 출력 미디어 파일에 씀
  // moov 헤더처럼 모든 스트림 정보가 있어야 추가할 수 있는 정보들이 존재
  av_write_trailer(output_file_ctx.av_format_ctx);
  release();

  return 0;
}

int open_input(const char* filename)
{
  input_file_ctx.av_format_ctx = nullptr;
  input_file_ctx.v_index = input_file_ctx.a_index = -1;

  if (avformat_open_input(&input_file_ctx.av_format_ctx, filename, nullptr, nullptr) < 0)
  {
    std::cout << "Couldn't open video file\n";
    return -1;
  }

  if (avformat_find_stream_info(input_file_ctx.av_format_ctx, nullptr) < 0)
  {
    std::cout << "Failed to retrieve input stream information\n";
    return -1;
  }

  for (int index = 0; index < input_file_ctx.av_format_ctx->nb_streams; ++index)
  {
    AVCodecParameters* av_codec_params = input_file_ctx.av_format_ctx->streams[index]->codecpar;
    if (av_codec_params->codec_type == AVMEDIA_TYPE_VIDEO && input_file_ctx.v_index < 0)
    {
      input_file_ctx.v_index = index;
    }
    else if (av_codec_params->codec_type == AVMEDIA_TYPE_AUDIO && input_file_ctx.a_index < 0)
    {
      input_file_ctx.a_index = index;
    }
  }

  if (input_file_ctx.v_index < 0 && input_file_ctx.a_index < 0)
  {
    std::cout << "No video or audio stream found\n";
    return -1;
  }

  return 0;
}

int create_output(const char* filename)
{
  output_file_ctx.av_format_ctx = nullptr;
  output_file_ctx.v_index = output_file_ctx.a_index = -1;

  if (avformat_alloc_output_context2(&output_file_ctx.av_format_ctx, nullptr, nullptr, filename) < 0)
  {
    std::cout << "Couldn't create output file context\n";
    return -1;
  }

  for (int index = 0; index < input_file_ctx.av_format_ctx->nb_streams; ++index)
  {
    if (index != input_file_ctx.v_index && index != input_file_ctx.a_index)
    {
      continue;
    }

    AVStream* in_stream = input_file_ctx.av_format_ctx->streams[index];
    AVCodecParameters* in_codec_params = in_stream->codecpar;

    // 새로운 스트림 생성
    AVStream* out_stream = avformat_new_stream(output_file_ctx.av_format_ctx, nullptr);
    if (!out_stream)
    {
      std::cout << "Failed to allocate output stream\n";
      return -1;
    }

    // 새로운 스트림에 AVCodecParameters 구조체 정보 복사
    if (avcodec_parameters_copy(out_stream->codecpar, in_codec_params) < 0)
    {
      std::cout << "Error occurred while copying codec parameters\n";
      return -1;
    }

    if (index == input_file_ctx.v_index)
    {
      output_file_ctx.v_index = index;
    }
    else
    {
      output_file_ctx.a_index = index;
    }
  }

  // avio_open() 함수는 fopen() 함수처럼 아무것도 쓰이지 않은 빈 파일을 생성할 때 사용
  if (!(output_file_ctx.av_format_ctx->oformat->flags & AVFMT_NOFILE))
  {
    if (avio_open(&output_file_ctx.av_format_ctx->pb, filename, AVIO_FLAG_WRITE) < 0)
    {
      std::cout << "Failed to open output file\n";
      return -1;
    }
  }

  // avformat_write_header() 함수는 컨테이너의 규격에 맞는 헤더를 생성하는 함수
  // AVFormatContext 구조체의 컨테이너 정보와 AVStream 구조체의 스트림 정보를 기반으로 헤더를 씀
  if (avformat_write_header(output_file_ctx.av_format_ctx, nullptr) < 0)
  {
    std::cout << "Failed to write header into output file\n";
    return -1;
  }

  return 0;
}

void release()
{
  if (input_file_ctx.av_format_ctx)
  {
    // avformat_open_input() 함수로 메모리를 할당했으면 avformat_close_input() 함수로 해제해야 메모리 릭이 발생하지 않음
    avformat_close_input(&input_file_ctx.av_format_ctx);
  }

  if (!(output_file_ctx.av_format_ctx->oformat->flags & AVFMT_NOFILE))
  {
    avio_closep(&output_file_ctx.av_format_ctx->pb);
  }

  if (output_file_ctx.av_format_ctx)
  {
    // AVFormatContext 구조체 내부에 할당한 메모리 해제
    avformat_free_context(output_file_ctx.av_format_ctx);
  }
}

4. Decoding

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/common.h>
}
#include <iostream>

struct FileContext
{
  AVFormatContext* av_format_ctx;
  AVCodecContext* video_codec_ctx;
  AVCodecContext* audio_codec_ctx;
  int v_index;
  int a_index;
};

FileContext input_file_ctx;

int open_input(const char* filename);
int open_decoder(AVCodecParameters* av_codec_params, AVCodecContext** av_codec_ctx);
int decode_packet(AVCodecContext** av_codec_ctx, AVPacket* av_packet, AVFrame** av_frame);
void release();

int main(int argc, const char** argv)
{
  av_log_set_level(AV_LOG_DEBUG);

  if (argc < 2)
  {
    std::cout << "Not enough arguments entered\n";
    return -1;
  }

  if (open_input(argv[1]) < 0)
  {
    release();
    return -1;
  }

  // 디코딩된 프레임을 저장할 수 있도록 AVFrame 구조체에 메모리 할당
  AVFrame* decoded_frame = av_frame_alloc();
  if (!decoded_frame)
  {
    release();
    return -1;
  }

  AVPacket av_packet;

  while (true)
  {
    int ret = av_read_frame(input_file_ctx.av_format_ctx, &av_packet);
    if (ret == AVERROR_EOF)
    {
      std::cout << "End of frame\n";
      break;
    }

    if (av_packet.stream_index != input_file_ctx.v_index && av_packet.stream_index != input_file_ctx.a_index)
    {
      av_packet_unref(&av_packet);
      continue;
    }

    AVStream* av_stream = input_file_ctx.av_format_ctx->streams[av_packet.stream_index];
    AVCodecContext** av_codec_ctx;
    if (av_packet.stream_index == input_file_ctx.v_index)
    {
      av_codec_ctx = &(input_file_ctx.video_codec_ctx);
    }
    else
    {
      av_codec_ctx = &(input_file_ctx.audio_codec_ctx);
    }

    // 타임스탬프 재조정
    av_packet_rescale_ts(&av_packet, av_stream->time_base, (*av_codec_ctx)->time_base);

    ret = decode_packet(av_codec_ctx, &av_packet, &decoded_frame);
    if (ret >= 0)
    {
      std::cout << "-----------------------\n";
      if ((*av_codec_ctx)->codec_type == AVMEDIA_TYPE_VIDEO)
      {
        std::cout << "Video : frame->width, height : " << decoded_frame->width << "x" << decoded_frame->height << "\n";
        std::cout << "Video : frame->sample_aspect_ratio : " << decoded_frame->sample_aspect_ratio.num << "/"
                  << decoded_frame->sample_aspect_ratio.den << "\n";
      }
      else
      {
        std::cout << "Audio : frame->nb_samples : " << decoded_frame->nb_samples << "\n";
        std::cout << "Audio : frame->channels : " << decoded_frame->channels << "\n";
      }

      av_frame_unref(decoded_frame);
    }

    av_packet_unref(&av_packet);
  }

  // AVFrame 구조체에 할당한 메모리 해제
  av_frame_free(&decoded_frame);
  release();

  return 0;
}

int open_input(const char* filename)
{
  input_file_ctx.av_format_ctx = nullptr;
  input_file_ctx.video_codec_ctx = nullptr;
  input_file_ctx.audio_codec_ctx = nullptr;
  input_file_ctx.v_index = input_file_ctx.a_index = -1;

  if (avformat_open_input(&input_file_ctx.av_format_ctx, filename, nullptr, nullptr) < 0)
  {
    std::cout << "Couldn't open input file " << filename << "\n";
    return -1;
  }

  if (avformat_find_stream_info(input_file_ctx.av_format_ctx, nullptr) < 0)
  {
    std::cout << "Failed to retrieve input stream information\n";
    return -1;
  }

  for (int index = 0; index < input_file_ctx.av_format_ctx->nb_streams; ++index)
  {
    AVCodecParameters* av_codec_params = input_file_ctx.av_format_ctx->streams[index]->codecpar;
    if (av_codec_params->codec_type == AVMEDIA_TYPE_VIDEO && input_file_ctx.v_index < 0)
    {
      if (open_decoder(av_codec_params, &input_file_ctx.video_codec_ctx) < 0)
      {
        break;
      }
      input_file_ctx.v_index = index;
    }
    else if (av_codec_params->codec_type == AVMEDIA_TYPE_AUDIO && input_file_ctx.a_index < 0)
    {
      if (open_decoder(av_codec_params, &input_file_ctx.audio_codec_ctx) < 0)
      {
        break;
      }
      input_file_ctx.a_index = index;
    }
  }

  if (input_file_ctx.v_index < 0 && input_file_ctx.a_index < 0)
  {
    std::cout << "No video or audio stream found\n";
    return -1;
  }

  return 0;
}

int open_decoder(AVCodecParameters* av_codec_params, AVCodecContext** av_codec_ctx)
{
  // 코덱 ID를 통해 FFmpeg 라이브러리가 자동으로 코덱 탐색
  AVCodec* av_decoder = avcodec_find_decoder(av_codec_params->codec_id);
  if (!av_decoder)
  {
    std::cout << "Couldn't find AVCodec\n";
    return -1;
  }

  *av_codec_ctx = avcodec_alloc_context3(av_decoder);
  if (!*av_codec_ctx)
  {
    std::cout << "Couldn't create AVCodecContext\n";
    return -1;
  }

  if (avcodec_parameters_to_context(*av_codec_ctx, av_codec_params) < 0)
  {
    std::cout << "Couldn't initialize AVCodecContext\n";
    return -1;
  }

  if (avcodec_open2(*av_codec_ctx, av_decoder, nullptr) < 0)
  {
    std::cout << "Couldn't open codec\n";
    return -1;
  }

  return 0;
}

int decode_packet(AVCodecContext** av_codec_ctx, AVPacket* av_packet, AVFrame** av_frame)
{
  int ret = avcodec_send_packet(*av_codec_ctx, av_packet);
  if (ret < 0)
  {
    std::cout << "Couldn't send AVPacket\n";
    return ret;
  }

  ret = avcodec_receive_frame(*av_codec_ctx, *av_frame);
  if (ret >= 0)
  {
    (*av_frame)->pts = (*av_frame)->best_effort_timestamp;
  }

  return ret;
}

void release()
{
  if (input_file_ctx.av_format_ctx)
  {
    avformat_close_input(&input_file_ctx.av_format_ctx);
  }

  if (input_file_ctx.video_codec_ctx)
  {
    avcodec_close(input_file_ctx.video_codec_ctx);
    avcodec_free_context(&input_file_ctx.video_codec_ctx);
  }

  if (input_file_ctx.audio_codec_ctx)
  {
    avcodec_close(input_file_ctx.audio_codec_ctx);
    avcodec_free_context(&input_file_ctx.audio_codec_ctx);
  }
}


참고할 만한 자료들