[C/C++] MACRO함수 사용시 일어날 수 있는 문제들 └ C/C++

stackoverflow 눈팅하다가 괜찮아 보여서 링크~

http://stackoverflow.com/questions/9104568/macro-vs-function-in-c



[WINAPI] Terminate Process Nicely(?) └ C/C++

<일단, 이 글은 사용자가 직접 새로운 프로세스를 생성/실행 시켜 해당 프로세스의 pid 등을 알고 있다는 전제하에 작성>

유닉스/리눅스 계열에서는 kill() 함수에 의해서 프로그램을 강제 종료 시킬 수 있다.

윈도우즈는 어떨까?

만일 프로그램으로 하여금 일반적으로 윈도우창의 X버튼을 눌렀을 때와 같이 종료를 시키고자 한다면,
해당 프로그램에게 WM_CLOSE 메시지를 날리는 방법이 가장 안전할 것이다.

하면 어떻게 WM_CLOSE메시지를 날릴 수 있을까?

EnumWindows() 함수를 이용해서 현재 실행중인 윈도우즈를 조회하는 방법이 있다.

아래는 간단한 샘플 코드이다.

BOOL CALLBACK EnumWindowProc( HWND hWnd, LPARAM lParam )
{	
	if( GetParent( hWnd ) == NULL ) {		
		DWORD pid = 0;		
		GetWindowThreadProcessId( hWnd, &pid );		
		if( pid == (DWORD)lParam )	{			
			PostMessage( hWnd, WM_CLOSE, 0, 0 );			
			  //PostMessage( hWnd, WM_SYSCOMMAND, SC_CLOSE, 0 );		
		}	
	}	
       return TRUE;
}

void KillProcess( PROCESS_INFORMATION* pInfo ){	
	EnumWindows( EnumWindowProc, pInfo->dwProcessId );
	if( WaitForSingleObject( pInfo->hProcess, 2000 ) != WAIT_OBJECT_0 )	{
		///> 2sec timeout
TerminateProcess ( pInfo->hProcess, 0 );
	}
}

윈도우들을 조회하면서 동일한 PID를 갖는 모든 hWnd에 WM_CLOSE 메시지를 보내는 것이다.

일단, 왠만한 상황에서는 이 코드가 정상적으로 동작하는 것을 확인했다.

EnumWindows() 밑에 부분은 2초동안 프로세스 종료를 기다려보고, 

그래도 종료가 안되었다면 응답없음으로 판단하고 강제 종료 시키는 부분이다.

단, TerminateProcess의 경우 DLL에 의해 관리되는 전역 데이터 상태가 손상될 수 있고, 

DLL들에게 detaching notify가 안날라가는 등, 추후 문제의 소지가 될 수 있다.


*** 추가적으로 위 코드가  TerminateProcess를 타는 몇가지 경우가 있었는데...

가장 치명적이었던 경우는 바로  VLC로 영상을 재생 중에 종료를 시도할 때였다

재생 중이 아니라면 정상 종료되었으나 재생 중에는 위 방법을 사용했다가, VLC가 Hang이 걸리는 문제가 발생했다.

보통 하나의 프로세스 내에 여러 개의 hWnd가 존재할 수 있는데 그 중 하나가 WM_CLOSE 메시지를 받고

그대로 Blocking 상태가 되어버린 것으로 추측하고 있다.

하드 코딩으로 여러개의 hWnd 중 주 윈도우 핸들 ( 실질적으로 윈도우 메시지를 처리하는 중심 윈도우 ? ) 에게만

WM_CLOSE 메시지를 보내줄 때는 정상적으로 종료가 됨을 확인했다.

문제는 그 주 Window 핸들을 특정지을만한 방법이 우리에게는 없다는 것이다.

몇가지 편법으로 IsWindowVisible() 이나, GetWindow( hWnd, GW_CHILD ) 함수를 사용해서

화면에 보이고 있거나, Child Window가 존재하는 녀석이 주 윈도우라고 판단하고 WM_CLOSE 메시지를 보내도록 해보기도 했다.

일단 VLC의 경우에 한해서는 성공적이었으나, 이게 모든 case를 커버할 것이라고는 생각하지 않는다.


=====================================================================================================


그 다음으로 찾은 방법은 WindowsXP Pro 이상에서만 가능한 방법인데

taskkill 유틸리티를 이용하는 방법이다.

taskkill 유틸리티는 유닉스/리눅스 계열의 kill 커멘드와 유사하다고 보면된다.

다만, 직접적으로 함수로 있는게 아닌 cmd 유틸리티이므로

CreateProcess() 나 ShellExecute() 계열의 함수를 이용해서 실행해 주면 된다.

커멘드는 : [ taskkill /im 프로그램이름 ] 과 같이 사용하면 된다.

가령, vlc를 종료한다고 하면, "taskkill /im vlc.exe" 가 된다.

한가지 단점은, 화면에 떠있는 모든 vlc 로 종료 시그널이 날라가기 때문에

특정 vlc만 죽이고 싶을 때는 좋은 방법이 아니다.




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 등도 사용하게 되는데요. 해당 개념을 처음 들으시는 분들은 미리 공부를 하고 들으셔야 이후 강좌를 들으시기가 좀 더 수월하실 거라고 생각합니다.




FFmpeg Player with Visual Studio - 04 FFmpeg을 이용하여 미디어 파일 열기 └ FFMPEG

4. FFmpeg을 이용하여 미디어 파일 열기


이번 강좌에서는 FFmpeg을 이용해서 어떻게 미디어 파일 또는 미디어 스트림을 열고, 미디어의 정보를 가져올 수 있는지 알아보도록 하겠습니다.


> Library 초기화 하기


거의 모든 오픈소스 Library들은 사용하기에 앞서 초기화가 필요합니다. FFmpeg의 초기화 과정은 전혀 어렵지 않습니다. 프로그램 시작 부에 아래 함수를 한번만 호출해 주시면 됩니다.


av_register_all();


av_register_all() 함수는 말 그대로 ffmpeg에서 지원되는 모든 Demuxer, Muxer, Protocol 및 Codec 등을 사용할 수 있도록 등록하는 행위입니다. ( 여기서 말한 지원되는 모든 컴포넌트들은 Library 버전에 따라서도 다를 수 있지만, Library를 컴파일 하는 과정에서 옵션으로 enable/disable 시킨 여부에 따라서 다를 수 있습니다.)


만일 네트워크를 통한 입력 source ( rtsp, rtp, udp, hls 스트림 등 ) 를 처리 할 일이 있다면, 아래의 추가 초기화 함수를 호출해 주시면 됩니다.


avformat_network_init();


avformat_network_init() 함수는 network 사용을 위한 추가 초기화 작업을 수행합니다. 가령 windows의 경우 winsock을 사용하기 위해서 초기화 함수인 WSAStartup() 함수를 호출해줘야 하는데, 그런 과정을 저 함수에서 처리한다고 보시면 됩니다. 이 함수를 호출했다면, 종료 시 해제 함수 avformat_network_deinit(); 을 같이 호출해 주셔야 합니다.


> 미디어 파일을 열어 보자!!


FFmpeg을 이용해서 미디어 파일을 열기 위해서는 최소한 파일의 URL 또는 파일 이름이 필요합니다. 당연한 내용이지요? 우리는 아래 함수를 이용하여, 미디어 파일을 열 수 있습니다.


avformat_open_input (AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);


첫번째 패러미터는 AVFormatContext 구조체 입니다. 사용자가 avformat_alloc_context() 함수를 이용하여 직접 생성한 후 넘겨줘도 되고, NULL을 가리키는 포인트 변수를 넘겨서 내부에서 생성하도록 하셔도 됩니다.

참고로 AVFormatContext 구조체는 FFmpeg Library중 avformat Library의 가장 핵심적인 데이터 타입으로, 주로 I/O 및 Muxing/Demuxing 과정에 사용됩니다. 마치 c library의 FILE 포인터와 같이 미디어 I/O 및 Muxing/Demuxing에 필요한 정보들을 포함하고 있습니다.

avformat_open_input() 함수는 첫번째 인자로 넘긴 AVFormatContext 구조체에 I/O 및 Muxing/Demuxing에 필요한 정보를 채워주는 역할을 합니다.


두번째 패러미터는 열고자 하는 파일이름입니다. 단순히 변수 이름이 filename 이라고 해서 파일만 가능한 것이 아니라, rtsp, udp 등 스트리밍 URL 모두 가능합니다. 굳이 다시 이름 붙이자면 Input Source 정도가 적당할 것 같네요.


세번째 패러미터는 강제로 Input Format을 지정할 경우에 사용됩니다. 그냥 NULL 값으로 지정하시면 입력 소스에 따라서 자동으로 Input Format을 검색합니다.


마지막 패러미터는 demuxer의 추가 옵션을 지정할 때 사용합니다. 이 패러미터 역시 잘 사용하지 않으므로 그냥 NULL 값을 넣어주시면 됩니다. 몇몇 특수한 경우, 가령 raw-video 를 demuxing 할 일이 발생한다거나 할 때, ( 파일에는 아무 정보도 없기 때문에 Parsing을 위해서 raw-video의  video 해상도 크기 및 Color 포맷을 알려줘야 하겠죠? ) 해당 정보를 이 옵션 패러미터로 넘겨주는 겁니다.


> 미디어 정보를 가져오자


avformat_open_input() 함수에서 미디어 파일을 열 때, 파일 헤더 정보가 존재하면 미리 같이 읽습니다. Input Format에 따라서는 파일 헤더 정보에 모든 내용이 기록된 포맷도 있겠지만 정보가 충분하지 않거나, 헤더가 없는 Format도 존재합니다. 이 경우 부족한 정보를 얻기 위해서는 어떻게 해야할까요? 그렇습니다. 충분한 정보를 얻기 위해서 미리 Data를 조금 읽어들여서 Parsing 또는 Decode 해보는 방법이 있습니다. FFmpeg Library는 이를 위해서 다음과 같은 함수를 제공합니다.


avformat_find_stream_info( AVFormatContext *ic, AVDictionary ** options );


첫번째 패러미터는 앞서 avformat_open_input()에 사용한 AVFormatContext 객체를 사용하시면 됩니다.


두번째 패러미터는 Codec 옵션을 지정할 때 사용합니다. 때에 따라서는 정보를 얻기 위해서 미리 Data를 Decode 해봐야 할 때가 있는데, 그 때 임시로 생성할 Codec에 넘겨줄 Codec 옵션을 지정합니다. 역시 잘 사용하지 않으므로 그냥 NULL 값을 넣어주시면 됩니다.


참고로 avformat_find_stream_info() 는 blocking 함수 입니다. Input Source로 Network Protocol을 사용한다면 정보가 담겨있는 패킷이 발견될 때까지 계속해서 Read를 시도하기 때문에 시간이 지연되거나, 최악의 경우 패킷이 들어오지 않는다면, block되어 버리는 일이 발생할 수도 있으니 주의합시다.


> 미디어 파일을 닫자


마지막으로 열었던 미디어 파일을 닫아보겠습니다. 다음 함수를 호출해서 파일을 닫을 수 있습니다.


avformat_close_input (AVFormatContext **s);


위 함수는 단순히 열려있는 파일을 닫는 기능 뿐만아니라  avformat_open_input() 또는 avformat_alloc_context() 함수를 통해 할당된 AVFormatContext 객체 s 를 해제까지 해줍니다. 즉, 따로 free 계열의 함수를 호출할 필요 없이, 이 함수 하나로 파일 닫기 및 자원 해제가 가능합니다.


> 정리


지금까지의 과정을 실제 코드로 정리를 해보도록 하겠습니다.

다음 코드는 AVFormatContext를 할당하고, 특정 파일을 열며 (포맷 등은 자동으로 검색), 스트림 정보를 뽑아내서 AVFormatContext에 정보를 저장하는 과정을 보여주는 코드입니다.



const char *szFilePath = "test.mp4";

///> Initialize libavformat and register all the muxers, demuxers and protocols.
av_register_all();

///> Do global initialization of network components.
avformat_network_init();

int ret;
AVFormatContext *pFmtCtx =
NULL;

///> Open an input stream and read the header.
ret = avformat_open_input( &pFmtCtx, szFilePath,
NULL, NULL );
if( ret != 0 ) {
av_log(
NULL, AV_LOG_ERROR, "File [%s] Open Fail (ret: %d)\n", ret );
exit( -1 );
}
av_log(
NULL, AV_LOG_INFO, "File [%s] Open Success\n", szFilePath );

///> Read  of a media file to get stream information
ret = avformat_find_stream_info( pFmtCtx,
NULL );
if( ret < 0 ) {
av_log(
NULL, AV_LOG_ERROR, "Fail to get Stream Information\n" );
exit( -1 );
}
av_log(
NULL, AV_LOG_INFO, "Get Stream Information Success\n" );

///> Close an opened input AVFormatContext.
avformat_close_input( &pFmtCtx );

///> Undo the initialization done by avformat_network_init.
avformat_network_deinit();


첨부한 소스코드에는 정보를 출력하는 부분까지 추가되어 있습니다.

(visual studio 2010용 프로젝트 파일입니다)

FFmpegFirst.7z.001
FFmpegFirst.7z.002

프로젝트를 여신 후에 szFilePath 값을 바꿔가면서 테스트 해보세요.

입력은 반드시 파일일 필요가 없으며, HLS, UDP 등의 네트워크 스트림도 가능합니다.


이번 강좌는 여기서 마치도록 하겠습니다.




FFmpeg Player with Visual Studio - 03 미디어 파일 └ FFMPEG

3. 미디어 파일


이번 장에서는 본격적인 실습에 앞서, 미디어 파일에 대한 간략한 설명과 함께 플레이어가 어떤 과정을 거쳐서 미디어 파일을 재생하게 되는지에 대해서 알아보려고 합니다. 비록 자세한 설명은 아니지만 향후 강좌 이해에 조금이나마 도움이 될까해서 이번 장을 마련했습니다.


> 미디어 파일


이후 제 강좌에서 미디어 파일이란 “플레이어에서 재생하기 위한 오디오/비디오 데이터를 가지고 있는 파일” 이라고 정의하겠습니다. 물론 이게 미디어 파일에 대한 올바른 정의라고 보기는 힘들지만 최소한 이번 장에서 만큼은 위의 의미로 사용할 것입니다.


오늘 날 우리는 많은 미디어 파일을 접하고 있습니다. 누구나 컴퓨터를 통해서 동영상을 재생해 본 경험이 있을 것입니다. 그럼 이런 미디어 파일은 어떻게 만들어질까요?

먼저 미디어 파일이 어떻게 만들어지는지를 한번 알아봅시다.


완전하지는 않지만 대략적으로 미디어 파일은 아래의 과정을 걸쳐서 만들어지게 됩니다.




과정을 좀 더 자세하게 들여다 봅시다. 크게 2가지 과정이 필요합니다.


A. 가공되지 않은 원본 영상 및 소리 ( Video/Audio Source 입니다 ) 를 압축 및 가공하는 Encoding (인코딩) 과정이 있습니다. 결과물을 Video / Audio Elements 라고 하겠습니다. 데이터를 가공해 주는 녀석을 우리는 Encoder (인코더) 라고 부르며, 여기서 말하는 Encoder (인코더) 가 (이후 언급할 Decoder(디코더)와 합쳐서 ) 우리가 흔히 말하는 Codec (코덱) 입니다. 모두 한번쯤은 들어보셨을 겁니다. H.264 (AVC) 라던가 최근 UHD 컨텐츠에 주로 사용되고있는 H.265(HEVC) 등이 그것입니다.


그렇다면 왜 인코딩 과정이 필요할까요?

여러가지 이유가 있겠지만 가장 큰 이유는 역시 가공되지 않은 원본 영상 및 소리의 크기가 너무 크기 때문입니다. 이 부분은 매우 크리티컬한 이슈가 됩니다. 데이터가 크기가 너무 클 경우 이는 곧 원할한 재생을 보장하지 못하기 때문입니다.

가령, 최근 보편화되고 있는 FHD 영상을 예로 들어 보겠습니다.

FHD 해상도 영상 (Image)의  경우 한장당 압축하지 않은 이미지의 크기가 약 6MB 정도 합니다. 보통 영상이 1초에 30 프레임으로 재생된다고 했을 때, 1초에 디스크에서 읽어야 할 데이터의 크기는 영상만 놓고 봤을 때, 약 180 MB 가 됩니다. 최신 PC에서는 그럭저럭 허용 범위입니다만, 그 외 구형 PC나 모바일 환경 및 HDD보다 상대적으로 I/O가 매우 느린 ODD를 통한 재생의 경우에는 원할한 재생을 보장하기 힘들 수 밖에 없습니다.


B. Source를 압축, 가공 한 후에는 결과물인 Video / Audio Elements 들을 포장하는 Muxing ( Muxing ( 먹싱 ) 은 Multiplexing ( 멀티플렉싱 ) 의 약자입니다 ) 과정이 있습니다. 일반적으로 한 종류의 Video와 Audio 를 포장하지만 경우에 따라서는 다수의 Video / Audio Elements를 포장하는 경우도 있습니다. 이 결과물이 우리가 원하는 미디어 파일이 됩니다.


포장을 하기 위해서는 Elements들을 담을 무언가가 필요하겠죠? 간단하게 상자를 상상해 봅시다. 상자의 (크기나 모양에 따른) 종류에 따라서 우리는 서로 다른 방식으로 포장을 하게 됩니다. 이런 상자들을 우리는 Container (컨테이너) 포맷이라고 합니다. 우리는 이러한 컨테이너 포맷을 자주 접하고 있습니다. 바로 파일의 확장자를 통해서죠. 미디어 파일 형식에는 다양한 종류가 있죠? AVI, MPG, MOV, MKV, MP4, 3GP 등 이 모두가 컨테이너 포맷들입니다.




참고로 종종 파일 확장자(컨테이너 포맷)와 코덱을 동일시 하시는 분들이 계시는데 다르게 보시는게 맞습니다. 물론 mp4 포맷에는 H.264 코덱을 사용하라고 강력히 권하고 있고, 그로 인해 대부분의 mp4 파일의 영상은 H.264로 인코딩 되어 있습니다.  때문에 보통 mp4 파일은 H.264라고 봐도 무방합니다만, 컨테이너 포맷과 코덱의 개념은 분명히 분리하고 가야 한다고 봅니다. 예로 mp4 포맷에 MPEG4 코덱이 사용될 수도 있고, MPG 포맷에 H.264 코덱이 사용될 수 있습니다. ( 물론 미디어 파일 중 오디오 전용 미디어 파일만 놓고 봤을 때는 파일확장자가 곧 코덱 이름인 것도 있습니다. 가령, mp3 파일이나 flac 파일 등은 코덱이름 역시 파일 확장자와 같습니다.  )


> 플레이어가 하는 일


그렇다면, 플레이어가 하는 일은 무엇일까요?

그렇습니다. 위에서 미디어 파일을 만들기 위한 과정을 역으로 진행하면 바로 플레이어가 하는 일이 됩니다.




A. 가장 먼저 Demuxing 과정입니다. 컨테이너 포맷 이라는 상자를 열고 안에 내용물을 들여다 봅니다.


컨테이너 포맷 안에는 설명서 또는 명세서 역할을 하는 특별한 데이터가 들어있습니다. 해당 데이터를 분석함으로써 우리는 이 상자 (컨테이너 포맷) 안에 몇 종류의 Video/Audio Elements들이 들어있고, 어떤 코덱을 사용하여 인코딩 되어있는지 등의 정보를 알 수 있습니다. 이 단계에서 우리는 Video/Audio Elements들을 디코딩하기 위한 디코더를 준비할 수 있습니다.


디코더가 준비되었다면, 상자 (컨테이너 포맷) 에서 Video/Audio Elements들을 하나씩 꺼내기 시작힙니다.


B.  상자 (컨테이너 포맷) 에서 꺼낸 Video/Audio Elements들은 준비된 디코더에 의해서 디코딩이 진행됩니다. 디코딩 된 Elements 들은 각각 원본 영상 및 소리로 변환됩니다.


C. 사용자가 원한다면, 이 단계에서 원본 영상 및 소리를 가지고 후처리 작업을 진행할 수도 있습니다. ( 영상처리 작업이나 소리 보정 또는 왜곡 등 다양한 작업을 진행해 볼 수 있겠죠? )


D. 마지막으로 플레이어는 영상과 소리를 시간에 맞게 잘 동기화해서 화면 또는 스피커로 출력해주면 됩니다. 일반적으로 사람은 영상보다는 소리에 민감하기 때문에 소리를 계속해서 재생하면서 소리 시간에 맞추어 영상을 출력해 주는 동기화 방법을 가장 많이 사용합니다.


> 플레이어에서 FFmpeg의 역할


그렇다면 FFmpeg 라이브러리는 어디까지 지원해 줄까요? 앞서 제일 첫 시간에 FFmpeg에 미디어 처리에 관련된 거의 모든 과정을 지원한다고 했었습니다. 따라서 위에서 언급한 과정 중 마지막 과정인 출력 (E) 을 제외한 모든 과정 (A ~ D)을 FFmpeg 라이브러리를 이용해서 처리한다고 보시면 되겠습니다.



다음 시간에는 FFmpeg 라이브러리를 이용해서 Demuxing 을 어떻게 시작하는지에 대해서 알아보도록 하겠습니다. 이제부터 본격적인 FFmpeg 코딩이 들어갑니다 ^^



1 2 3 4 5 6 7 8 9 10 다음