buntalk.com
FFmpeg and SDL Tutorial - Making Screencaps 본문
반응형
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;
}
반응형
'FFmpeg' 카테고리의 다른 글
FFmpeg and SDL Tutorial - Synching Video (0) | 2023.03.19 |
---|---|
FFmpeg and SDL Tutorial - Spawning Threads (0) | 2023.03.19 |
FFmpeg and SDL Tutorial - Playing Sound (0) | 2023.03.19 |
FFmpeg and SDL Tutorial - Outputting to the Screen (0) | 2023.03.19 |
muxing, demuxing (0) | 2023.03.19 |