FFmpeg Player with Visual Studio - 05 FFmpeg을 이용한 Video Decoding └ FFMPEG

요즘 회사일이 좀 바빠서 업로드가 많이 늦었습니다 ㅜㅜ 
벌써 5번째 강좌군요 그럼 시작하겠습니다

05. FFmpeg을 이용한 Video Decoding


저번 강좌에서 FFmpeg 라이브러리를 이용하여 미디어 파일을 열어보고 해당 미디어 파일의 정보를 가져오는 부분까지 알아봤습니다


이번 시간에는 가져온 미디어 정보를 바탕으로 본격적인 디코딩을 위해서 코덱을 생성하고, 실제 스트림 패킷을 읽어서 디코딩을 해보는 과정까지 진행해 볼까 합니다


> Streams


지루하겠지만, 일단 이론을 좀만 더 보시죠. 스트림( Stream ) 이란 녀석에 대해서 좀 이야기를 해볼 까 합니다. 일반적으로 일관된 데이터의 흐름을 스트림 이라고 합니다. 우리가 다루고 있는 미디어 파일에서 본다면, 비디오나 오디오 데이터가 이에 해당된다고 볼 수 있겠죠?


일반적으로 우리가 접하는 미디어 파일은 비디오 스트림과 오디오 스트림이 각각 하나씩 들어있습니다.


하지만 파일에 따라서는 비디오 / 오디오 이외에 특별한 목적을 가지고 넣는 private data 스트림이 들어가는 경우도 있습니다. 또한 DVD 영화와 같이 자막 스트림이 같이 포함되는 경우도 있습니다.


또한, 제작자가 원한다면 미디어 파일에 다중 비디오 / 오디오 스트림을 넣는 경우도 볼 수 있죠. 가령, DVD 영화의 경우 영어 / 우리말 / 감독의 주석 등 여러가지 오디오가 한번에 들어 있는 경우를 보셨을 겁니다.


반대로 CCTV와 같이 영상만 필요한 경우는 오디오 스트림이 존재하지 않는 경우도 있을거라고 생각해 볼 수 있겠습니다.


일반적으로 미디어 파일을 재생하면, 한번에 하나의 비디오와 오디오만 렌더링하게 되므로, 만일 다중 비디오 및 다중 오디오 스트림이 존재하는 미디어 파일이 있다면 Decoding을 할 대상을 특정지어야 됩니다.


FFMpeg Library에서는 이 Stream들의 목록을 어떻게 관리할까요?


앞서 avformat_open_input() 함수와 avformat_find_stream_info() 함수를 통해 AVFormatContext 구조체에 미디어 파일의 정보를 채웠던 것을 기억하실 겁니다. AVFormatContext 구조체를 살표보면 streams 라는 변수가 있습니다. AVStream 구조체 포인터의 배열이라고 생각하시면 됩니다. 총 몇개의 Stream이 존재하는지는 nb_streams 변수를 참고 하시면 됩니다.


일반적으로 FFmpeg Library에서 처리하는 Stream의 Type은 6가지 이며, 각 타입은 상수로 다음과 같이 정의되어 있습니다.


enum AVMediaType {
   AVMEDIA_TYPE_UNKNOWN = -1,  ///< Usually treated as AVMEDIA_TYPE_DATA
   AVMEDIA_TYPE_VIDEO,
   AVMEDIA_TYPE_AUDIO,
   AVMEDIA_TYPE_DATA,       ///< Opaque data information usually continuous
   AVMEDIA_TYPE_SUBTITLE,
   AVMEDIA_TYPE_ATTACHMENT,    ///< Opaque data information usually sparse
   AVMEDIA_TYPE_NB
};


마지막 AVMEDIA_TYPE_NB는 타입 정의가 아니라 미디어 타입이 총 5개 있음을 의미합니다. enum 규칙에 따라서 AVMEDIA_TYPE_NB에 5가 들어가지요.


위의 타입 중 실질적으로 우리가 반드시 처리해야하는 경우는 AVMEDIA_TYPE_VIDEO와 AVMEDIA_TYPE_AUDIO이며, DVD와 같이 내장된 자막을 처리하고자 한다면 AVMEDIA_TYPE_SUBTITLE까지 신경쓰시면 됩니다. ( 여기서 말한 자막 Stream은 smi나 sub 파일과 같이 별도로 존재하는 자막 파일이 아닌 미디어 파일에 내장되어 있는 자막을 의미합니다. 만일 별도파일의 자막을 처리하고 싶으시다면, 해당 내용을 따로 구현하셔야 합니다. )


아래 코드는 미디어 파일의 스트림 정보를 조회하며, 해당 스트림의 타입을 출력하는 코드입니다.


AVFormatContext *pFmtCtx; ///> 파일 열기 과정 생략
int i;
for( i = 0 ; i < pFmtCtx->nb_streams ; i++ ) {
printf("Stream %d is ", i);
switch( pFmtCtx->streams[i]->codec->codec_type ) {
case AVMEDIA_TYPE_VIDEO:
printf("Video\n");
break;
case AVMEDIA_TYPE_AUDIO:
printf("Audio\n");
break;
case AVMEDIA_TYPE_SUBTITLE:
printf("Subtitle\n");
break;
case AVMEDIA_TYPE_DATA:
case AVMEDIA_TYPE_ATTACHMENT:
printf("Data\n");
break;
default:
printf("Unknown\n");
break;
}
}


여기서 stream 배열의 index 값이 되는 저 i 값이 중요합니다. 저 i 값은 AVStream 구조체 내에 index라는 변수명으로 또한 따로 저장되어 있습니다. 저 값이 중요한 이유는 Decoding을 위해서 미디어 파일의 스트림 데이터를 읽어들일 때, 읽어들인 데이터가 어떤 스트림용 데이터인지 구분지을 때, 저 index 값을 사용하기 때문입니다. 이는 실제 데이터를 읽어들이는 부분에서 다루도록 하겠습니다.


> AVCodecContext


앞서 우리는 Demuxing을 위해서 AVFormatContext 구조체를 사용했습니다. 앞선 강좌에서 AVFormatContext는 I/O, Muxing, Demuxing에 필요한 정보를 저장한 구조체라고 했다는 내용을 기억하시는지요?

그렇다면 자연스럽게, Encoding, Decoding에도 이와 비슷한 역할을 하는 대표 구조체가 존재할 수 있다고 추측해 볼 수 있습니다. 그 구조체가 바로 AVCodecContext 입니다


기본적으로 AVCodecContext 구조체에는 Encoding/Decoding에 필요한 정보들이 거의 모두 포함되어 있다고 보시면 됩니다. 가령, 비디오의 경우 비디오 이미지의 해상도와 frame rate 등의 정보가 있고, 오디오의 경우 sample rate, channel layout 등의 정보가 포함되어 있습니다. 여기까지는 Encoding / Decoding 공통 정보이고, 여기에 추가적으로 Decoder에 대한 정보를 붙이게 되면, AVCodecContext 구조체는 Decoding을 위한 용도로, 반대로 Encoder에 대한 정보를 붙이게 되면 Encoding을 위한 용도로 사용됩니다.


Encoding을 하기 위해서는 AVCodecContext를 직접 생성 해 줘야 하는 경우가 일반적이지만, Decoding의 경우에는 앞서 avformat_open_input() 함수와 avformat_find_stream_info() 함수를 거치면서 AVFormatContext 구조체 내의 AVStream 구조체 내에 codec 이라는 변수명으로 자동으로 생성 되어 있으며, 사용자는 여기에 추가적으로 Decoder에 대한 정보만 붙여주면 됩니다.


> Decoder 준비하기


이제 직접 코드를 통해서 어떻게 Decoder를 준비하는지 보도록 하겠습니다.

이하 코드들은 저번 시간에 살펴본 avformat_find_stream_info() 함수까지 정상적으로 실행되었다는 가정하에 작성된 것입니다.


1. 대상 비디오 / 오디오 스트림 찾기


먼저 Decoding의 대상을 가져와 봅시다. Decoding의 대상이라고 한다면, 당연히 미디어 파일 안에 포함되어 있는 오디오 및 비디오 스트림이 될 것입니다. 일단 “찾는다” 라는 표현을 사용했습니다만, 여기서는 Decoding 대상을 특정짓는다 라고 하는게 맞는 표현일지도 모르겠네요.


아래 코드는 위의 스트림 타입 출력 코드를 살짝 변형 시켜서 만든 첫번 째 비디오와 오디오 스트림을 찾는 소스코드 입니다.


AVFormatContext *pFmtCtx; ///> 파일 열기 과정 생략
int i;
int nVSI = -1; ///> Video Stream Index
int nASI = -1; ///> Audio Stream Index
for( i = 0 ; i < pFmtCtx->nb_streams ; i++ ) {
if( pFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO ) {
nVSI = nVSI < 0 ? i : nVSI;
}
else if( pFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO ) {
nASI = nASI < 0 ? i : nASI;
}
}


위와 같이 코드를 작성하면 (제 생각에는…) 90%의 미디어 파일에 대해서 이상 없이 사용하실 수 있습니다. 나머지 10%는 어떤 경우일까요? 일반적으로는 접하기 어려운 파일인데, 방송용으로 만들어진 스트림의 경우 하나의 미디어 파일 ( 이라기보다는 스트리밍이 더 옳은 표현이겠네요… 테스트 목적 이외에는 보통 파일로 저장해 놓지 않으니까요 ) 안에는 여러개의 채널이 존재할 수 있습니다. 이 경우는 똑같은 다중 스트리밍 이지만 DVD와는 달리 하나의 그룹이 아닌 다중 그룹(채널 별로 그룹지어지겠죠?)이기 때문에 위와 같은 코드를 사용할 경우 문제가 발생할 수 있습니다. 가령, 영상은 1번채널 영상인데, 음성은 2번채널 음성이 나오면 이상하겠죠? 물론 이 경우는 매우 특수한 경우이기 때문에 보통은 위의 코드만으로도 충분합니다.


만약 저런 특수한 경우까지 고려해서 연관된 비디오 / 오디오 스트림을 찾을려면 어떻게 해야할까요? FFmpeg Library에서는 위의 사항을 고려해서 적용된 Index를 가져오는 API 함수를 따로 제공하고 있습니다. FFmpeg에서 제공하는 플레이어 ffplay에서는 해당 API를 이용해서 스트림의 Index를 가져오도록 하고 있습니다. 아래 코드를 직접 보면서 설명하겠습니다.


AVFormatContext *pFmtCtx; ///> 파일 열기 과정 생략
int nVSI = -1; ///> Video Stream Index
int nASI = -1; ///> Audio Stream Index


///> 비디오 스트림 찾기
nVSI = av_find_best_stream(pFmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
///> 오디오 스트림 찾기 (비디오 스트림 있다면 연관된)
nASI = av_find_best_stream(pFmtCtx, AVMEDIA_TYPE_AUDIO, -1, nVSI, NULL, 0);


사용된 함수는 av_find_best_stream() 함수 입니다.


  • 첫번째 인자인 ic는 미디어 파일 열기에 사용했던 AVFormatContext 구조체 입니다.

  • 두번째 인자는 찾고자 하는 스트림 타입입니다. 보통 비디오, 오디오 또는 자막이 되겠습니다.

  • 세번째 인자는 특별히 지정하고자 하는 스트림 Index 번호입니다. 범위는 0 ~ pFmtCtx->nb_streams - 1 이며, -1 값을 지정할 경우 자동 선택입니다. 일반적으로 -1 값을 가장 많이 사용합니다.

  • 네번째 인자는 찾고자 하는 스트림의 연관된 스트림 Index 번호입니다. -1을 넣을 경우, 가장 처음으로 만나는 Target 스트림 Index를 얻어옵니다. 위의 코드를 보시면, 비디오 스트림은 가장 처음 검색되는 Index를 가져오고, 오디오 스트림을 검색할 때는 해당 비디오 스트림의 Index 값을 넘겨서 연관된 오디오 스트림을 찾도록 하고 있습니다.

  • 다섯번째 인자는 Decoder까지 함께 검색할지 여부입니다. AVCodec 구조체의 주소를 넘기도록 되어있습니다. 만약 NULL 값이 아니면 넘겨진 AVCodec 구조체에 Decoder 정보를 채워줍니다. 보통은 NULL 값을 넣고 사용자가 직접 FFmpeg Library에서 해당 Decoder를 검색하거나 생성합니다.

  • 마지막 인자는 나중에 확장을 위해서 만들어 놓은 값으로 현재는 따로 flag가 정의되어 있는 것이 없으므로, 그냥 0 값을 집어넣으면 됩니다.


이런 방법도 있다고만 알아두시면 될 것 같습니다. 실제로는 처음 제시했던 코드를 더 많이 사용하며, ffplay에서도 처음 미디어 오픈 시에만 위의 함수를 사용하고, 다중 오디오 재생을 위해서 다음 오디오 스트림을 열 때는 결국 첫번째 방법을 사용해서 하더군요.


av_find_best_stream() 함수를 참고하여 자신만의 find_stream 함수를 만들어 보는 것도 나쁘지 않은 방법입니다. av_find_best_stream() 함수는 libavformat/utils.c 에 구현되어 있습니다.


2. Decoder 검색 및 초기화


Decoding 대상인 비디오 / 오디오 스트림을 찾았다면, 해당 스트림이 어떤 Codec으로 Encoding 되어있는지 확인한 후에 FFmpeg Library에서 해당 Codec 용 Decoder 정보를 검색한 후, 해당 Decoder 정보를 이용하여 AVCodecContext 구조체를 Decoder로서 초기화 해보도록 하겠습니다.


아래 코드는 일단 비디오 스트림을 대상으로 Decoder를 초기화하는 코드입니다만, 오디오 스트림에 대해서도 똑같이 적용할 수 있습니다.


AVFormatContext *ic; ///> 스트림 열기 생략
int nVSI; ///> 비디오 Index 찾기 생략

///> Find Video Decoder
AVCodec *pVideoCodec = avcodec_find_decoder(

   pFmtCtx->streams[nVSI]->codec->codec_id

);
if( pVideoCodec == NULL ) {
av_log( NULL, AV_LOG_ERROR, "No Decoder was Found\n" );
exit( -1 );
}

///> Initialize Codec Context as Decoder
if( avcodec_open2( pFmtCtx->streams[nVSI]->codec, pVideoCodec, NULL ) < 0 ) {
av_log( NULL, AV_LOG_ERROR, "Fail to Initialize Decoder\n" );
exit( -1 );
}


먼저 해당 스트림이 어떤 Codec을 사용했는지에 대한 정보는 AVCodecContext 구조체 내의 codec_id 값을 확인하면 됩니다. 저번 시간에 올렸던 소스코드예제를 보신 분이라면, 저 정보를 이용해서 Codec 정보를 출력했다는 것을 기억하실 겁니다.


avcodec_find_decoder() 함수에 codec_id 값을 넘겨줌으로서 FFmpeg Library에 해당 Codec용 Decoder정보가 있는지 찾아볼 수 있습니다. 일반적으로 FFmpeg Library 자체 S/W Decoder가 먼저 검색되어지며, 만약 하나의 Codec에 대해서 복수개의 Decoder가 존재하며, 다른 Decoder를 사용하고 싶다거나 할 경우에는 avcodec_find_decoder_by_name() 함수를 통해서 이름으로 Decoder를 검색할 수도 있습니다. 하지만 보통은 FFmpeg Library를 빌드 할 때 옵션을 통해서 코덱당 하나의 Decoder만 나오도록 조절 합니다. 만약, 해당 코덱용 Decoder가 없는 경우에는 NULL 값을 리턴합니다.


만일 Decoder 정보가 존재한다면, AVCodecContext에 해당 정보를 넘겨줘서 Decoder로서 초기화 하는 일이 남았습니다. 이 때 사용하는 함수가 avcodec_open2() 입니다.


  • 첫번째 인자는 AVCodecContext 구조체 입니다.

  • 두번째 인자는 방금 찾은 AVCodec 구조체 입니다.

  • 마지막 인자는 Decoder 초기화에 필요한 추가 옵션입니다. 가령, 비디오의 경우 bitrate정보 등을 넣어 줄 수도 있고, decoding 시 thread 사용 여부 등을 결정해 줄 수도 있습니다. 일반적으로 NULL 값을 사용합니다.


여기까지 성공했다면, 이제 Decoding을 위한 기본 준비는 모두 마친 상태입니다.


> 실제 파일을 읽으면서 디코딩을 해보자!!


이제 마지막 단계입니다. 실제로 비디오 / 오디오 데이터를 읽어들이면서 디코딩을 해보도록 하겠습니다. 사실 상 이 단계까지 마치면 Player 제작 과정 중 FFmpeg Library를 이용해서 할 수 있는 일은 거의 다 했다고 보시면 됩니다. ( 물론 실제 Player를 만들 때는 설계가 많이 바뀌게 되지만, 호출하는 함수는 동일합니다 )


먼저 데이터를 읽는 과정입니다. 아래 코드를 보시죠.


AVFormatContext *pFmtCtx; ///> 초기화 생략
AVPacket packet;

while( av_read_frame( pFmtCtx, &packet ) >= 0 ) {
/**
* Decoding Process Here
*/

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


새로운 구조체가 등장했습니다. AVPacket 입니다. AVPacket 구조체는 FFmpeg Library에서 인코딩된 비디오 / 오디오 데이터를 저장하기 위한 구조체 입니다. Decoder에 이 구조체의 데이터가 전달되서 Decoding 과정을 수행합니다.


파일로부터 인코딩된 비디오 / 오디오 데이터를 읽기 위해서는 av_read_frame() 함수를 이용합니다. 파일 뿐만 아니라 network stream 일 경우에도 이 함수를 이용해서 패킷 데이터를 읽어올 수 있습니다. 첫번째 인자로는 미디어 파일을 여는데 사용한 AVFormatContext 구조체가 들어갑니다. 그리고 두번째 인자로는 읽어들인 데이터를 저장하기 위한 AVPacket 구조체가 들어갑니다.


여기서 한가지 주의해야 할 점이 있는데, AVPacket 구조체 자체는 위 소스코드에서 보시면 일반 변수로 선언되어 있습니다만, AVPacket 구조체 내에 data 라는 변수자체가 포인터 변수입니다. av_read_frame() 함수가 정상적으로 데이터를 읽어오게 되면, 이 data라는 포인터 변수에 동적할당을 통해 데이터를 기록합니다. 따라서 동적할당 된 데이터를 다 사용한 후 (디코딩 후가 되겠죠?) 에는 Release해줘야 할 필요가 있습니다. while() 문 마지막에 보시면 av_free_packet() 이라는 함수를 통해서 말이죠. ( **여기서 av_free_packet() 함수는 동적 할당 된 AVPacket 구조체 데이터를 Release하는게 아닌 AVPacket 구조체 내에 av_read_frame()함수를 통해 동적할당 된 데이터를 Release하는 것입니다. 주의합시다. )


이번에는 디코딩을 해보도록 합시다. 이번에도 코드를 먼저 보고 설명을 드리겠습니다.


AVFormatContext *pFmtCtx; ///> 초기화 생략
int nVSI, nASI; ///> Stream Index 찾기 과정 생략

AVCodecContext *pVCtx = pFmtCtx->streams[nVSI]->codec;
AVCodecContext *pACtx = pFmtCtx->streams[nASI]->codec;

AVPacket packet;
AVFrame *pVFrame = avcodec_alloc_frame();
AVFrame *pAFrame = avcodec_alloc_frame();

int bGotPicture = 0;
int bGotSound = 0;

while( av_read_frame( pFmtCtx, &packet ) >= 0 ) {
if( packet.stream_index == nVSI ) {
// Decode Video
avcodec_decode_video2( pVCtx, pVFrame, &bGotPicture, &packet );
if( bGotPicture ) {
// Ready to Render Image
}
}
else if( packet.stream_index == nASI ) {
// Decode Audio
avcodec_decode_audio4( pACtx, pAFrame, &bGotSound, &packet );
if( bGotSound ) {
// Ready to Render Sound
}
}

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

av_free( pVFrame );
av_free( pAFrame );


소스코드가 좀 길어보입니다. 이번에도 새로 보는 변수들이 막 등장하는군요. ^^;;


av_read_frame() 을 통해서 인코딩 된 데이터를 불러왔을 경우, 이 데이터가 비디오인지 오디오인지 사용자가 알아야 해당 Decoder를 통해 Decoding을 할 수 있겠죠? 이번 강좌 제일 처음 Stream 편 마지막에 index값이 중요하다고 했던거 아직 기억하고 계신가요? 그렇습니다. 코드에 보시면 AVPacket 구조체 내에 stream_index라는 변수가 나오는데 바로 이 값이 위에서 stream을 검색할 때 찾았던 바로 그 Index 값이 됩니다. 이 index 값을 이용해서 우리의 목표 스트림인 비디오 / 오디오 데이터를 구분짓는 것입니다.


stream_index로 비디오 또는 오디오 데이터를 찾았다면 이제 디코딩을 해야 합니다. 비디오 / 오디오 각각 avcodec_decode_video2() 함수와 avcodec_decode_audio4() 함수를 사용합니다. ( ** avcodec_open2() 나 avcodec_decode_audio4() 와 같이 함수 뒤에 숫자가 붙은 경우가 있는데, 이는 함수들이 버전 업을 하면서 붙은 숫자들입니다. 일부 과거 호환성을 위해서 코드상으로는 아직 남아있는 경우도 있는데 deprecated 된 함수이므로 사용하지 맙시다. )


decode 함수의 첫번째 인자는 당연히 Decoder 입니다. Decoder용으로 초기화한 AVCodecContext 구조체를 넘겨주시면 됩니다.


위에서 av_read_frame() 설명할 때, AVPacket 구조체가 파일로부터 인코딩 된 비디오 / 오디오 데이터를 읽어 저장하기 위한 구조체라고 했었습니다. 그렇다면 반대로 디코딩 된 결과 데이터를 저장하는 구조체도 있지 않을까요? 그렇습니다. 바로 그 역할을 하는 것이 AVFrame 구조체 입니다. decode 함수의 두번째 인자가 되겠습니다.


세번째 인자는 decoding된 결과가 이제 Rendering되어도 되는지 알려주는 flag 값이라고 보시면 됩니다. 이 flag 값이 0이면 Decode된 비디오 / 오디오 데이터가 없다는 의미입니다. (Decoding에 실패했다는 의미는 아닙니다. 가령, 비디오 패킷 데이터가 다음 비디오 이미지를 디코딩하기 위한 선행 정보라면 그 때는 디코딩은 성공했지만 비디오 데이터는 없겠죠? 디코딩의 실패는 함수 자체의 리턴 값으로 판단하시면 됩니다.) flag 값이 non-zero 값이면 Decode된 비디오 / 오디오 데이터가 AVFrame 구조체에 저장되었다는 의미이므로, AVFrame 구조체의 내용을 화면 / 스피커에 Rendering 하기만 하면 됩니다.


마지막 인자는 인코딩된 비디오 / 오디오 데이터를 가지고 있는 AVPacket 구조체입니다. av_read_frame() 함수를 통해 읽은 데이터를 여기에 넘기시면 됩니다.


실제로 Decoding된 데이터를 출력해야하는 문제가 남아있지만, 그 부분은 FFmpeg Library를 벗어나는 주제이므로 일단 이번 장에서는 생략하고, 이후 강좌에서 다루도록 하겠습니다. 미리 언급드리자면 윈도우에서 영상은 OpenGL을 통해서 출력할 것이며, 음성은 DirectSound를 통해서 출력할 예정입니다. 다만, 이를 처음부터 설명드리기에는 본 강의 방향성에 맞지 않기 때문에 가능한 실습 레벨에서는 위 기능들을 라이브러리 화 해서 제공할 예정이며, 혹시 직접 구현에 관심있으신분들이 있으실 수도 있으므로 따로 강좌 페이지를 제공할 예정입니다. (언제가 될지는 장담을 못드리지만 ^^;;;)


아래 첨부된 소스코드는 5장까지 배웠던 모든 내용을 정리해 본 것입니다. 다시 한번 정리해 보시는 것도 괜찮을 거라고 생각합니다. 그리고 비디오 출력이 정상적으로 이루어졌는지 확인도 안해보면 이상할 것 같아서 첫 비디오 프레임을 ascii 도트로 찍도록 해봤습니다. (가능하면 해상도가 (매우) 낮으면서 첫 장면이 gray scale로 봤을 때 무리없이 볼 수 있는 비디오로 테스트 해보시기 바랍니다.)

(** main 시작 부분에 szFilePath에 테스트 할 비디오 경로를 넣어주시면 됩니다 )


FFmpegTutorial.7z.001


FFmpegTutorial.7z.002


> 5장을 마치며


이번 장에서 우리는 Decoder를 초기화하고 파일을 읽으면서 실제 데이터를 Decoding 해보는 과정까지 살펴보았습니다. 사실상 FFmpeg Library를 이용한 Player의 Core 부분은 거의 5장까지 배운 범위 내에서 이루어집니다. 물론 이것으로 FFmpeg을 이용한 Player 개발이 완료되는 것이 아닙니다. 간단한 예제를 통해서 일단 핵심이 되는 FFmpeg 함수들을 살펴본 것이며, 다음 장 부터는 Player 개발에 있어서 실질적인 문제들을 다루어 가볼까 합니다. 이제부터는 본격적으로 Player 개발에 있어서 가장 힘든 부분들이 됩니다. “비디오 / 오디오를 어떻게 Rendering 할 것인가?”, “비디오 / 오디오의 동기화는 어떻게 처리하는게 좋은가?”,  “Player 구조는 어떻게 처리할 것인가?” 등 벌써부터 머리가 아파오시죠 ^^;;;

덧붙여 본격적인 Player 개발에 들어가게 되면 thread나 mutex, condition 등도 사용하게 되는데요. 해당 개념을 처음 들으시는 분들은 미리 공부를 하고 들으셔야 이후 강좌를 들으시기가 좀 더 수월하실 거라고 생각합니다.




덧글

  • 슈밧 2014/05/14 15:00 # 삭제 답글

    정말 매번 참고하면서 공부하는데 5월 13일 날짜로 새로운 강의가 올라왔네요..
    감사합니다.
    header파일 보면서 일일이 분석하고 있는데 애매하던 부분도 확 잡아주셨네요.

    다시한번 감사합니다!!!!!
  • 개뒷치기 2014/06/02 22:08 # 삭제 답글

    안녕하세요 ffmpeg 프로젝트 진행중인 학생입니다.
    mp4 나 avi 같은 컨테이너가 포함된 파일은 디코딩해서 캡쳐하는데 성공하였으나
    raw h264와 같이 컨테이너가 없는 h264를 디코딩 하는 방법은 없는지 궁금하여 댓글을 남깁니다.
    감사합니다.
  • 체인지겟타 2014/06/03 10:52 #

    컨테이너가 없이 Raw 데이터를 디코딩하시겠다면,
    직접 AVCodecContext 를 생성하셔서 정보를 입력하시면 됩니다.
    avcodec_alloc_context3() 함수를 이용해서 생성할 수 있으며, 이후 Deocder를 붙이는 부분은
    av_find_decoder( AV_CODEC_ID_H264 )를 통해서 가져올 수 있습니다.

    컨테이너가 있다면, AVCodecContext의 추가 config 정보를 av_format_input() 함수가 처리해 주지만,
    컨테이너가 없다면, 수동으로 config 정보를 채워줘야 합니다.
    H264 패킷의 SPS 유닛 등을 Parsing하여 AVCodecContext에 필요한 값을 추가로 채워줘야 하는데요
    ( SPS 와 PPS를 Parsing하면 코덱 정보 및 해상도 등 정보가 나옵니다 )

    이 부분은 저도 이렇게 하면 되지 않을까? 라고 생각만 한 단계고
    (안드로이드용 MediaCodec 객체는 저렇게 직접 넣어서 디코더를 초기화했었습니다)
    실제로 아직 구현을 해보지 않아서 직접적으로 소스코드를 제공해 드리기는 어렵구요

    시간은 걸리겠지만 실제로 ffmpeg 상 avformat_open_input()나 avformat_find_stream_info() 함수에서 어떻게 처리하는지 직접 쫓아가 보시는 방법을 권해드릴 수 밖에 없겠네요

    avfomrat_open_input() 함수와 avformat_find_stream_info() 함수는 libavformat/utils.c 에 구현되어 있습니다
    (몇 단계 함수를 타고 들어가셔야 할거에요 ^^;;;)

    나중에 이쪽에 관련되서 추가 강좌를 작성할 수 있다면 해보도록 하겠습니다
  • 개뒷치기 2014/06/03 20:50 # 삭제 답글

    답변감사합니다!. h264 덩어리 파일은 디코딩하는데 성공했는데
    문제는 랜선타고 날아오는 sps pps i 파싱하는거군요.. 끅
  • 청청하늘 2014/06/09 17:43 # 답글

    안녕하세요. 쏘스트웨어기술자(영상처리분야)입니다.

    ffmpeg에 대해서 이번까지 많은걸 배웠습니다.
    선생님이 친절히 배워준 ffmpeg기술을 이용해 윈도우기반의 플레이어도 만들었습니다.
    현재 애로되는 문제에 답변주시면 정말 감사하겠습니다.
    저는 웹캠이나 혹은 이미 존재하는 동영상파일을 어느 한 컴퓨터에 서버를 구축해놓고 그것을 날라와 저장 혹은 요청한 다른 클라이언트 컴퓨터에 실시간 전송하려고 합니다. 결국 중간 서버를 하나 구축해놓고 임의의 동영상을 불러다가 저장 및 다른 클라이언트들에 실시간 전송하려고 합니다.
    그래서 ffserver를 시도해봤는데 윈도우에서는 정말 막막하네요.
    개발환경은 VS 2010입니다.
    그러니 ffserver를 Visual Studio 2010에서 이용할수는 없겠는지요?
    할수 없다면 윈도우에서 할수 있는 다른 서버는 없겠는지요?


    다시 한번 감사드립니다.!!!
  • 체인지겟타 2014/06/11 16:49 #

    질문하신 내용을 제가 잘 이해하고 있는지는 모르겠습니다만...

    결국 Source단의 영상 ( 웹캠에 의한 실시간 스트리밍 혹은 NAS를 통한 파일 접근 등 ) 을
    중간 스트리밍 서버를 통해서 클라이언트 쪽에 서비스 하시는 것이 최종 목적이신 듯한데요

    어떤 형식의 서비스를 원하느냐에 따라서 서버 구성은 달라질 듯 하며,
    중요한 점은 스트리밍 서비스를 하기위해서는 프로토콜에 따라서 거의 필수적으로 transcoding을 진행해야 하는데, 이 부분이 고려되어 있는지도 궁금하네요

    단순히, 개발이 아닌 이미 나와있는 프로그램을 이용하고 싶으시다면

    VLC 같은 프로그램을 이용하시면, 기본적인 스트리밍 ( MP2TS on UDP, MP2TS on RTP, RTSP 등 )이 가능한데, 상용으로 이용 시 라이센스 확인 및 퍼포먼스 측정은 필요할 듯 하네요

    위에서 언급하신 ffserver도 가능할 듯하네요 ( 웹캠 + ffserver 로 검색하시면 자료는 구하시기 쉬울거에요 )

    실제로 스트리밍 서버는 상용에서도 구축 목적이나 서비스 대상 등에 따라서 워낙 다양한 방법들이 존재해서 여기서 다 열거하기는 힘들 지만, 확실한 것은 iOS의 에어 비디오와 같이 실시간으로 Transcoding + Streaming 해주는 방식은 서버에 부하가 많이 걸리기 때문에 일반적으로는 영상 입수 시 사전 Transcoding 작업을 마쳐놓고 Streaming 서버는 단순히 전송만 책임지는 방식을 가장 많이 사용합니다.

    직접 구현하시겠다고 하신다면, 미디어 서버의 경우 참조하실만한 오픈소스로는 RTSP 서버인 live555 나 ffmpeg 또는 ffserver의 소스코드를 참조하시는 편이 좋겠네요. VLC의 경우도 오픈소스로 알고 있는데, 맞다면 VLC쪽 소스코드를 참조해 보시는 것도 나쁘지 않을 것 같습니다.
  • 슈퍼 2014/06/15 23:48 # 답글

    안녕하세요. 많이 배우고 감사드립니다.

    ffmpeg에 대해 자뭇 자신심이 생겼어요

    저는 실시간 영상을 받아 rtsp프로토콜에 따라서 실시간 스트리밍을 하려고 합니다.
    ffserver로 스트리밍서버를 구축하려고 하는데 윈도우용으로 컴파일이 쉽게 안되더군요
    저는 스트리밍서버를 이미 나와 있는 프로그램을 이용하는것이 아니라
    완전한 개발을 위한 목적이므로 선생님이 올려준 원천소스와 같은 VS2010에서 이용가능한
    ffserver를 얻고 싶습니다.

    웹캠+ffserver로 검색을 했지만 리눅스에서만 컴파일이 되는군요.

    부탁드립니다.
  • 체인지겟타 2014/06/17 14:30 #

    제가 모든 내용을 답변해 드릴 수 있지는 않습니다만... 일단 ffserver를 이용하시겠다면, 이미 윈도우즈 용으로 빌드된 버전을 찾을 수 있습니다. VS에서 직접 빌드해서 사용하고 싶으시다면, 일단 아래 내용을 참고해서 컴파일을 시도해 보시는게 좋을 것 같네요.
    http://ffmpeg.org/platform.html#Microsoft-Visual-C_002b_002b-or-Intel-C_002b_002b-Compiler-for-Windows

    일단 제가 아직 직접 ffmpeg 라이브러리를 빌드해보진 않아서 (추후에 직접 해보고 강좌를 연재할 계획이긴 합니다만...) 아직 뭐라고 확답을 드리기는 힘들 것 같습니다

    그 이외에 직접 RTSP 서버를 개발하시겠다면, ffserver 소스를 참조하시거나, live555 소스코드를 참조하시라고 밖에 답변을 드릴 수 없을 것 같습니다.

    RTSP의 경우, HTTP 통신과 유사하기 때문에 특별히 구현하기 어려운 부분은 없을 것 같구요.
    다만 세션별로 RTCP 나 RTP 전송 관리 쪽이 좀 어려울 수 있는데 이에 대해서는 실제 ffserver나 live555등이 어떻게 처리하는지 아이디어를 얻으셔서 직접 구현해 보시는게 빠를 것 같습니다.
  • 슈퍼 2014/06/17 17:36 # 답글

    답변 감사드려요

    한번 힘내서 해보겠습니다.
  • 꼬맹이ㅋㅋ 2014/07/24 01:50 # 삭제 답글

    FFmpeg을 이용해서 player를 만들어 보고자 하는 학생입니다.
    강좌를 처음부터 보면서 많은 도움을 받았는데요.
    공부를 하다가 궁금한 점이 생겨서 질문해봅니다.
    대부분의 FFmpeg을 이용한 플레이어를 보면, SDL을 사용해서 출력을 합니다.
    그래서 Open 소스인 FFplay를 실행해 보아도, 새로운 윈도우 창으로 재생이 됩니다.
    저는 MFC를 이용해서 player를 만들고 있는데, 출력을 새로운 창이 아닌,
    현재 프로그램창에 패널 같은것을 두어 출력하고 싶은데,
    어떻게 하는것이 좋을까요 ?

    SDL을 사용하고도 새로운 창이 아닌, 프로그램창에서 출력이 가능할까요 ?
  • 체인지겟타 2014/07/25 11:00 #

    일단 인터넷에 공개되어있는 SDL을 이용한 player들은 모두 원 소스가 ffplay라고 보시면 됩니다
    ffplay가 SDL을 이용해서 소스를 제공하고 있기 때문이구요

    만약 ffplay에서 SDL을 제거하고 싶으시다면 비디오 Renderer와 오디오 Renderer를 직접 구현하셔야 합니다
    비디오의 경우 OpenGL이나 DirectX 등을 이용하시면 될 것 같구요,
    오디오의 경우 DirectSound 등을 이용하는 방법이 있겠네요

    만일 SDL을 그대로 사용하신다고 하신다면,
    SDL 윈도우 생성 시 NO_FRAME 속성으로 생성하시고 MFC 특정창의 Child Window로 붙여보는 방법도 생각해 볼 수 있겠네요

    SDL 창을 다른 윈도우의 Child로 붙이는 방법은 아래와 같이 조금만 검색해 보시면 나올 것 같습니다
    http://www.doctort.org/adam/nerd-notes/sdl-child-window.html
  • 다다이즘 2014/08/20 09:30 # 삭제 답글

    안녕하세요 체인지겟타님 블로그 보며 FFMPEG에 대해 많이 알게된 학생입니다.
    저는 현재 네트워크 소켓프로그래밍 + FFMPEG으로 영상 전송 프로젝트를 하나 진행중인데요.
    궁금한점이 있어 이렇게 글 남깁니다.

    avformat_network_init(); 쓰고 서버와 클라이언트 연결한 다음 avformat_open_input 함수로 서버쪽의 영상을
    읽어올수 있을까요?

    avformat_open_input 함수 두번째 인자로 파일경로 뿐만아니라 rtsp, udp 등 스트리밍 URL 모두 가능하다고 하셨는데 혹시 제가 만약 udp를 사용한다면 어떤 형식으로 두번째 인자를 써야할까요?

    혹시 이 문제에 해결책을 아신다면 알려주시면 감사하겠습니다! 그럼 오늘도 좋은하루 되세요!
  • JUN 2014/09/24 11:04 # 삭제 답글

    감사합니다 덕분에 잘 배우고 갑니다. 요즘 개발자들은 자신의것으로 유지하기 급급한데 이렇게 지식 전달을 주셔서 감사합니다~
  • DT 2014/10/24 11:05 # 삭제

    제가 하고 싶은 말을 그대로 해주셨네요. 게다가 왠만한 교재보다 이해하기 쉽네요. 진심으로 독자들을 배려해주신 마음이 느껴집니다. 감사합니다.
  • jinro 2014/12/04 14:48 # 삭제 답글

    감사합니다.
    체인지게타님 글이 정말 쉽게 설명이 잘되었네요..
    6장연재가 기다려집니다.
  • PG Park 2014/12/08 13:08 # 삭제 답글

    FFMPEG를 사용해서 프로젝트를 할 일이 생겼는데 덕분에 기본부분을 많이 배우고 갑니다.^^
  • SoulJung 2015/03/04 18:02 # 삭제 답글

    감사합니다 많은 도움이 되었습니다.
  • 루피사마 2015/03/09 18:15 # 삭제 답글

    avcodec_find_encoder함수로 부르면 널이라고 나왔는데 혹시 해결방법 아세요? 구글링 찾아봐도 어렵네요.좀도와주세요
    avcodec_find_decoder 함수는 불러올수 있는데 avcodec_find_encoder함수은 안되네요.
    인터넷 찾아보니까 h264 코덱을 설치해야하는데 저는 설치되있는데 이거 맞는지 잘 모르겠네요 ㅜㅜ;
  • lee 2015/04/28 02:20 # 삭제 답글

    혹시 비디오를 디코딩할 때 디코딩을 부분적으로 수행할 수는 없을까요?
    총 프레임이 1000프레임의 비디오라고 했을 때 100프레임부터 200프레임까지만 디코딩을 한다던지...
  • jakejang 2015/06/15 15:28 # 삭제 답글

    좋은 강좌 잘보고 갑니다.
    정말 많은 도움이 되었습니다.
  • sgcho 2015/10/22 15:44 # 삭제 답글

    영상관련 대학원 전공자(sgcho79@gmail.com)임에도 불구하고 ffmpeg의 복잡도 때문에 어려움이 있었는데,
    운영자 분의 위 정보가 많은 도움이 되었네요.

    혹여나 하는 마음에 아래의 정보 또한 남깁니다.
    관심있으신 분들은 아래의 링크를 참조해 보세요.
    http://sourceforge.net/u/leixiaohua1020/wiki/simplest_ffmpeg_tutorial/

  • 야매코더 2016/02/16 17:53 # 삭제

    아주 좋은 코드 이군요. 대부분 C 라 조금 거슬리는 코드들이 많은데, 이것은 C++ 이군요. 잘 보겠습니다.
  • inu 2015/12/30 17:17 # 삭제 답글

    우와 너무 감동적이에요 ㅜㅜ output.txt로 나온 화면이 정말 예술이네요 .
    ffmpeg라는 키워드 하나만 알고 완전 깜깜한 상태였는데
    멋진 설명 정말 감사합니다.^^
    복받으실 거에요 ^^
  • 야매코더 2016/02/16 17:50 # 삭제 답글

    잘 읽었습니다. 많은 도움 되었습니다. 강좌를 이용해서 제 블로그에 포스트 하나 올렸습니다. 감사합니다.
  • 김창도 2016/05/13 10:09 # 삭제 답글

    Player 개발에 있어서 가장 힘든 부분들이 됩니다. “비디오 / 오디오를 어떻게 Rendering 할 것인가?”, “비디오 / 오디오의 동기화는 어떻게 처리하는게 좋은가?”, “Player 구조는 어떻게 처리할 것인가?” 등 의 내용이 이후에 매우 보고싶습니다. 책 출간같은것은 안하시나요?
  • 지나가는이 2016/07/27 14:54 # 삭제 답글

    글 잘 읽었습니다.
    FFmpeg 으로 뭘 해보려는데 많은 도움이 되었습니다.
    감사합니다~
  • 전사 2016/10/21 19:52 # 삭제 답글

    안녕하세요? 강좌 정말 감사합니다.
    첫번째 예제의 <미디어 파일의 스트림 정보를 조회하며, 해당 스트림의 타입을 출력하는 코드>
    이부분에서 에러가 나는데 이거 원인을 못찾겠어요.
    구글링해도 알 수가 없습니다. ㅜㅜ

    이 스위치 구문에서 에러가 나고
    switch (pFmtCtx->streams[i]->codec->codec_type) {

    아래는 에러 메시지 입니다.
    error C4996: 'AVStream::codec': was declared deprecated
    includelibavformatavformat.h(880) : see declaration of 'AVStream::codec'

    이게 뭐가 문제인건가요?? 제발 답변 부탁드려요.ㅜ
  • ㅇㅇㅇ 2017/01/31 21:52 # 삭제

    #pragma warning(disable:4996)
    사용하시면 오류 안뜹니다
  • 구르지롱 2017/05/04 17:28 # 삭제 답글

    올려주신 완성 코드 100, 101 Line
    avcodec_alloc_frame(); -> av_frame_alloc();
    으로 수정해야 합니다
  • 무재칠시 2017/11/17 12:42 # 답글

    따라해보기 정말 좋게 잘 작성해 주셔서 감사드립니다!~
  • 박스줍는보더 2017/12/10 15:07 # 삭제 답글

    정리 정말 잘 하셨네요. 궁금한 부분들이 쏙쏙 이해되네요. 감사합니다.
  • Jinsoo Par 2018/11/02 14:49 # 삭제 답글

    안녕하세요.
    while (av_read_frame(pFmtCtx, &pkt) >= 0) 에서 while 문을 제외하여 수정하고 싶은데, 가능할까요?
댓글 입력 영역