반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

buntalk.com

FFmpeg and SDL Tutorial - Making Screencaps 본문

FFmpeg

FFmpeg and SDL Tutorial - Making Screencaps

분석톡톡 2023. 3. 19. 18:05
반응형

2019/04/07

 

Tutorial 01: Making Screencaps

 
 
Overview
 
영화파일은 기본적인 몇가지 컴포넌트를 가지고 있다. 먼저, 파일 자체는 컨테이너라고 불리며 컨테이너의 형식으로 파일이 갖는 정보를 알 수 있다. 컨테이너에는 AVI와 퀵타임같은게 있다. 다음은 스트림 모음이다. 예를들어, 오디오 스트림과 비디오 스트림. ("스트림"은 시간에 따라 만들어진 데이터 엘리먼트의 묶음"을 나타내는 용어이다. 스트림내 데이터 엘리먼트는 프레임. 각 스트림은 다른 코덱으로 인코드될 수 있다. 코덱은 어떻게 실제 데이터가 코드되고 디코드되는지를 정의하는 것으로 그래서 이름이 코덱이다. 코덱의 예시에는 DivX와 MP3가 있다. 패킷은 스트림에서 읽혀진다. 패킷은 데이터의 조각으로서 데이터의 비트를 포함할 수 있어 로프레임으로 디코드될 수 있으며 최종적으로 어플리케이션에서 처리할 수 있게 된다. 이런 목적을 위해 각 패킷은 완전한 프레임을 포함하고 오디오의 경우 다중 프레임을 포함한다.
 
Container -> Stream -> Frame
and Packet
 
아주 기본 수준에서 비디오와 오디오 스트림을 다루는 것은 아주 쉽다:
10 video.avi에서 비디오 스트림 열기
20 비디오 스트림에서 패킷을 읽어 프레임을 만든다
30 만약 프레임이 완전하지 않으면 20으로 간다
40 프레임으로 어떤것을 한다
50 20으로 간다
 
FFmpeg으로 멀티미디어를 다루는 것은 이 프로그램만큼 단순하다. 비록 몇몇 프로그램은 아주 복잡한 "어떤것을 하는" 것도 있다. 이 튜토리얼에서는 파일을 열고, 비디오 스트림을 읽고 "어떤 것을 한다" 는 프레임을 PPM파일에 쓰기이다.
 
파일 열기
 
먼저, 첫 단계로 파일을 어떻게 여는지 살펴보자. FFmpeg으로 라이브러리를 초기화한다.
 
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <ffmpeg/swscale.h>
... int main(int argc, char *argv[])
{
  av_register_all();
 
이것은 가용한 모든 파일포맷과 코덱을 등록한다. 그래서 포맷/코덱에 연관되는 파일이 열릴때 자동적으로 사용된다. 일단 av_register_all() 을 호출해야 하므로 이곳 main()에 둔다. 원한다면 특정 파일포맷과 코덱에 대해서만 등록할 수 도 있지만 보통 그렇게 해야할 이유는 없을 것이다.
이제 실제 파일을 연다
AVFormatContext *pFormatCtx = NULL;
// 비디오 파일 열기
if (avformat_open_input(&pFormatCtx, argv[1], NULL, 0, NULL) != 0)
{
  return -1; // 파일을 열수 없었음
}​
 
첫번째 매개변수로 파일 이름을 얻는다. 이 함수는 파일 헤더를 읽어 AVFormatContext 구조체에 파일 포맷에 대한 정보를 저장한다. 마지막 세 매개변수는 파일 포맷, 버퍼 사이즈, 그리고 포맷 옵션에 대한 것으로 이를 NULL이나 0으로 지정하면 libavformat 은 자동검출한다.
 
이 함수는 오직 헤더만 확인하므로 다음으로 파일내의 스트림 정보를 확인한다.
 

 

 
// 스트림 정보 얻기
if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
{
  return -1; // 스트림 정보를 찾을 수 없었음
}
이 함수는 pFormatCtx->streams 를 적절한 정보로 채운다. 무엇이 안에 있는지 알 수 있게 하는 편리한 디버깅 함수가 있다.
 
// 표준에러로 파일에 정보를 덤프한다
av_dump_format(pFormatCtx, 0, argv[1], 0);​

 

 
이제 pFormatCtx->streams 는 포인터의 배열이며, pFormatCtx->nb_streams 사이즈이다. 이제 비디오 스트림을 찾을 때까지 확인할 수 있다.
 
int i;
AVCodecContext *pCodecCtxOrig = NULL;
AVCodecContext *pCodecCtx = NULL;

// 첫 비디오 스트림을 찾는다
videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++)
  if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
  {
    videoStream = i;
    break;
  }
if (videoStream == -1)
  return -1; // 비디오 스트림을 찾을 수 없었음

// 비디오 스트림에 대한 코덱컨텍스트를 가리키는 포인터를 얻음
pCodecCtx = pFormatCtx->streams[videoStream]->codec;​

 

 
코덱에 대한 스트림 정보는 "코덱 컨텍스트" 라는 것이다. 이 것은 스트림이 사용하는 코덱에 대한 모든 정보를 포함하고 우리는 이를 가리키는 포인터를 가진다. 하지만 여전히 실제 코덱을 찾고 열어야 한다.
 
AVCodec *pCodec = NULL;

// 비디오 스트림에 대한 디코더를 찾는다
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (pCodec == NULL)
{
  fprintf(stderr, "Unsupported codec!\n");
  return -1; // 코덱이 발견되지 않음
}
// 컨텍스트 복사
pCodecCtx = avcodec_alloc_context3(pCodec);
if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0)
{
  fprintf(stderr, "Couldn't copy codec context");
  return -1; // 코덱 컨텍스트를 복사하는 동안 에러
}
// 코덱 열기
if (avcodec_open2(pCodecCtx, pCodec) < 0)
  return -1; // 코덱을 열지 못함
 
비디오 스트림에 대한 AVCodecContext 는 직접적으로 사용해서는 안된다. 그러므로 avcodec_copy_context() 를 사용해 새로운 위치에 컨텍스트를 복사했다.
 
데이터 저장하기
 
이제 프레임을 실제로 저장할 장소가 필요하다.
 
 
AVFrame *pFrame = NULL;
// 비디오 프레임 할당
pFrame = av_frame_alloc();
 
24비트 RGB를 저장하는 PPM파일을 출력할 예정이므로 프레임을 RGB로 변환해야 한다. FFmpeg 은 이런 컨버전을 수행해 준다. 대부분의 프로젝트에서 초기 프레임을 특정 포맷으로 컨버트하기를 원할 것이다. 이제 컨버트된 프레임을 위한 프레임을 할당하자
 
 
// AVFrame구조체를 할당
pFrameRGB = av_frame_alloc();
if (pFrameRGB == NULL)
  return -1;
프레임을 할당했더라도 컨버트할 때 로데이터를 저장할 장소가 필요하다. avpicture_get_size를 사용해 필요한 사이즈를 얻고 수동으로 공간을 할당한다.
uint8_t *buffer = NULL;
int numBytes;
// 요구되는 버퍼사이즈를 측정하고 버퍼를 할당한다.
numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height);
buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));​
 
av_malloc은 FFmpeg의 malloc으로 메모리 주소 정리와 같은 것을 수행할 수 있도록 하는 래퍼이다. 이 것이 메모리 누수나 이중 free 와  같은 다른 malloc문제를 해소하지는 않는다.
 
avpicture_fill 을 사용해 프레임과 새롭게 할당한 버퍼를 연관시킨다. AVPicture 캐스트에 대해 AVPicture 구조체는 AVFrame 구조체의 부분집합으로서 AVFrame 구조체의 시작부분은 AVPicture 구조체와 동일하다.
 
// 버퍼의 적절한 부분을 pFrameRGB내의 이미지 플레인으로 할당
// pFrameRGB는 AVFrame 이지만 AVFrame 은 AVPicture의 슈퍼셋이다.
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height);
 
이제 우리는 스트림으로부터 읽어들일 준비가 되었다.
 
데이터 읽기
 
우리가 할 것은 전체 비디오 스트림을 패킷을 통해 읽고 프레임에 디코딩하고 일단 프레임이 완료되면 컨버트하고 저장한다.
 
struct SwsContext *sws_ctx = NULL;
int frameFinished;
AVPacket packet;
// 소프트웨어 스케일링을 위해 SWS 컨텍스트 초기화
sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);

i = 0;
while (av_read_frame(pFormatCtx, &packet) >= 0)
{
  // 이 패킷이 비디오 스트림에서 읽은 것인가?
  if (packet.stream_index == videoStream)
  {
    // 비디오 프레임 디코드
    avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
    // 비디오 프레임을 얻었는가?
    if (frameFinished)
    {
      // 이미지를 RGB로 변환
      sws_scale(sws_ctx, (uint8_t const *const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
      // 디스크에 프레임 저장하기
      if (++i <= 5)
        SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
    }
  }
  // av_read_frame 에 의해 할당된 패킷을 해제
  av_free_packet(&packet);
}
 
패킷에 대해 알아둘 것
 
기술적으로 패킷은 일부의 프레임이나 데이터의 다른 비트를 저장할 수 있지만, FFmpeg의 파서는 완전한 프레임 또는 다중 프레임을 포함하도록 보증한다
 
처리는 단순하다. av_read_frame() 은 패킷에 읽어들이고 AVPacket 구조체에 저장한다. 오직 패킷 구조체만 할당했음을 기억하자. FFmpeg 은 내부적으로 할당을 수행하는데 이 것은 packet.data 로 가리켜진다. 이 것은 나중에 av_free_packet() 으로 해제한다. avcodec_decode_video() 는 패킷을 프레임으로 컨버트한다. 그러나 패킷을 디코드한 후 한 프레임에 대한 모든 정보가 채워지지 않았을 수 있다. 그래서 avcodec_decode_video() 가 frameFinished 를 다음 프레임을 가질 때 할당해준다. 최종적으로 sws_scale()을 사용해 pCodecCtx->pix_fmt 를 RGB로 변환한다. AVFrame 포인터를 AVPicture 포인터로 캐스트할 수 있음을 기억하자. 마지막으로, 프레임, 너비와 높이를 SaveFrame 함수에 전달한다.
 
이제 남은 것은 SaveFrame 를 작성해 PPM포맷으로 RGB정보를 만들어 내는 것이다. PPM형식을 자체적으로 어떤 것인지 작성할 것이다. 
 
 
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame)
{
  FILE *pFile;
  char szFilename[32];
  int y;

  // 파일 열기
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile = fopen(szFilename, "wb");
  if (pFile == NULL)
    return;
  // 헤더 작성
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);
  // 픽셀 데이터 작성
  for (y = 0; y < height; ++y)
    fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
  // 파일 닫기
  fclose(pFile);
}
 
표준 파일 열기등을 수행하고 RGB데이터를 적었다. 한번에 한줄씩 파일에 썼다. PPM파일은 긴 문자열로 RGB정보를 담는 단순한 파일이다. HTML색상을 안다면 #ff0000#ff0000 이면 붉은 색인것을 알 것이다. (바이너리고 구분자는 없지만 무슨의미인지 이해할 것이다.) 헤더는 이미지가 너비 높이를 나타내고 RGB값의 최대값은 어떤 값인지 나타낸다.
 
이제, main()함수로 돌아간다. 비디오 스트림을 읽었으니 전체를 해제해야 한다.
 
// RGB이미지 해제
av_free(buffer);
av_free(pFrameRGB);

// YUV프레임 해제
av_free(pFrame);

// 코덱 닫기
avcodec_close(pCodecCtx);
avcodec_close(pCodecCtxOrig);

// 비디오 파일 닫기
avformat_close_input(&pFormatCtx);

return 0;
 
avcodec_alloc_frame 과 av_malloc 으로 할당한 메모리에 대해 av_free를 사용한다.
이 것이 전체코드이다. 이제 리눅스와 같은 플랫폼에서 실행해보자.
 
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lm
 
대부분의 이미지 프로그램은 PPM파일을 열 수 있다. 영화파일로 테스트해보자
 
// tutorial01.c
// Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101
// on GCC 4.7.2 in Debian February 2015

// A small sample program that shows how to use libavformat and libavcodec to
// read video from a file.
//
// Use
//
// gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lswscale -lz
//
// to build (assuming libavformat and libavcodec are correctly installed
// your system).
//
// Run using
//
// tutorial01 myvideofile.mpg
//
// to write the first five frames from "myvideofile.mpg" to disk in PPM
// format.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <stdio.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55, 28, 1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame)
{
  FILE *pFile;
  char szFilename[32];
  int y;

  // Open file
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile = fopen(szFilename, "wb");
  if (pFile == NULL)
    return;

  // Write header
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);

  // Write pixel data
  for (y = 0; y < height; y++)
    fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);

  // Close file
  fclose(pFile);
}

int main(int argc, char *argv[])
{
  // Initalizing these to NULL prevents segfaults!
  AVFormatContext *pFormatCtx = NULL;
  int i, videoStream;
  AVCodecContext *pCodecCtxOrig = NULL;
  AVCodecContext *pCodecCtx = NULL;
  AVCodec *pCodec = NULL;
  AVFrame *pFrame = NULL;
  AVFrame *pFrameRGB = NULL;
  AVPacket packet;
  int frameFinished;
  int numBytes;
  uint8_t *buffer = NULL;
  struct SwsContext *sws_ctx = NULL;

  if (argc < 2)
  {
    printf("Please provide a movie file\n");
    return -1;
  }
  // Register all formats and codecs
  av_register_all();

  // Open video file
  if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0)
    return -1; // Couldn't open file

  // Retrieve stream information
  if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
    return -1; // Couldn't find stream information

  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);

  // Find the first video stream
  videoStream = -1;
  for (i = 0; i < pFormatCtx->nb_streams; i++)
    if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
    {
      videoStream = i;
      break;
    }
  if (videoStream == -1)
    return -1; // Didn't find a video stream

  // Get a pointer to the codec context for the video stream
  pCodecCtxOrig = pFormatCtx->streams[videoStream]->codec;
  // Find the decoder for the video stream
  pCodec = avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if (pCodec == NULL)
  {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }
  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0)
  {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
    return -1; // Could not open codec

  // Allocate video frame
  pFrame = av_frame_alloc();

  // Allocate an AVFrame structure
  pFrameRGB = av_frame_alloc();
  if (pFrameRGB == NULL)
    return -1;

  // Determine required buffer size and allocate buffer
  numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
                                pCodecCtx->height);
  buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));

  // Assign appropriate parts of buffer to image planes in pFrameRGB
  // Note that pFrameRGB is an AVFrame, but AVFrame is a superset
  // of AVPicture
  avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
                 pCodecCtx->width, pCodecCtx->height);

  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
                           pCodecCtx->height,
                           pCodecCtx->pix_fmt,
                           pCodecCtx->width,
                           pCodecCtx->height,
                           PIX_FMT_RGB24,
                           SWS_BILINEAR,
                           NULL,
                           NULL,
                           NULL);

  // Read frames and save first five frames to disk
  i = 0;
  while (av_read_frame(pFormatCtx, &packet) >= 0)
  {
    // Is this a packet from the video stream?
    if (packet.stream_index == videoStream)
    {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

      // Did we get a video frame?
      if (frameFinished)
      {
        // Convert the image from its native format to RGB
        sws_scale(sws_ctx, (uint8_t const *const *)pFrame->data,
                  pFrame->linesize, 0, pCodecCtx->height,
                  pFrameRGB->data, pFrameRGB->linesize);

        // Save the frame to disk
        if (++i <= 5)
          SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height,
                    i);
      }
    }

    // Free the packet that was allocated by av_read_frame
    av_free_packet(&packet);
  }

  // Free the RGB image
  av_free(buffer);
  av_frame_free(&pFrameRGB);

  // Free the YUV frame
  av_frame_free(&pFrame);

  // Close the codecs
  avcodec_close(pCodecCtx);
  avcodec_close(pCodecCtxOrig);

  // Close the video file
  avformat_close_input(&pFormatCtx);

  return 0;
}
반응형