제목: ffmpeg의 apiexample을 사용한 mpeg encoding 방법
날짜: 2004년 4월 18일
작성: 정성욱 (director@smu.ac.kr)

-Content
---------

1. ffmpeg library 사용법
2. ffmpeg encoding 방법의 종류
 2.1 YUV
 2.2 RGB->YUV
 2.3 YUV library함수 사용하기
 2.4 RGB->YUV library함수 사용하기
3. V4L에서 영상 받기
 3.1 TV Card
 3.2 USB
4. ffmpeg를 사용하여 encoding하기
 4.1 saveToFile
 4.2 포맷에 따는 각각의 MPEG압축 방법
   4.2.1 YUV420P
   4.2.2 RGB24->YUV420P
   4.2.3 YUV library함수 사용하기
   4.2.4 RGB24->YUV420P library함수 사용하기
5. 요약

-서문
------

본 문서는 ffmpeg를 사용하여 video 신호를 mpeg codec로 encoding하는 방법을 소개한다. 여기서 사용하는 source는 libavcodec의 예제 source인 apiexample.c를 가지고 변형하여 작성한 문서임을 밝힌다. Reference문서가 많지 않아서 정확한 의미를 알지 못하는 부분이 있다. 만약 그러한 부분에 대한 정화한 의미를 아는 사람이 있다면, director@smu.ac.kr로 연락주기 바란다. 그럼 이제부터 WebCAM과 TV Card를 사용한 ffmpeg encoding 방법에 대해서 알아보자

1. ffmpeg library 사용법
-------------------------

우리가 ffmpeg library를 사용하기 위해서는 동적 라이브러리 파일을 컴파일시 연결시켜 주어야 한다. 동적 라이브러리 파일은 "*.a"파일로서, 우리가 ffmpeg를 통해 인코디하기 위해 사용하는 파일은 "/libavcodec/libavcodec.a"파일이다. 이렇게 ffmpeg library를 사용하는 파일을 컴파일하는 방법은 다음과 같다.

--------------------------------------------------------------------------------
$ gcc -g -O3 -Wall -o [filename] [filename.c] libavcodec.a -lm -lz -ldl
--------------------------------------------------------------------------------


2. ffmpeg encoding 방법의 종류
-------------------------------

장치가 가지는 신호의 종류는 여러가지이다. 그러나 여기서는 RGB24와 YUV420P두가지의 방식을 사용함을 먼저 밝힌다. RGB24와 YUV420P 두가지의 방식을 사용하여 기본적으로 apiexample에서 제공하는 방식과 함수를 사용하는 방식 2가지로 구분하면 총 4가지의 방식이 나오게 된다. 이제부터 이 4가지의 방식에 대해 소개 하려 한다.

2.1 encoding_YUV.c
-------------------

apiexample.c에서 사용한 기본적인 방식을 사용한 방식이다. AVFrame이라는 구조체를 선헌하고 구조체의 data배열에 실제 영상을 넣어주어 encoding하는 방식이다.

2.2 encoding_YUV_f.c
---------------------

AVPicture 구조체에 avpicture_fill()함수를 사용하여 영상데이터를 library함수로 넣어주는 방식이다.

2.3 encoding_RGB.c
-------------------

encoding_YUV.c와 방식은 동일하다. context의 세팅값은 PIX_FMT_YUV420P방식으로 세팅되어 있다. 단지 영상을 V4L을 통해서 받아 들일때 우리는 RGB24로 받아오게 되는것이다. RGB24로 받아진 영상은 AVPicture라는 구조체에 저장되고 저장된 AVPicture를 img_convert()를 통해서 YUV420P로 변환한다. 그런후에 AVFrame 구조체의 배열인 data에 넣어주고 인코딩하게 되는것이다.

2.4 encoding_RGB_f.c
---------------------

모든 것은 위의 encoding_RGB.c와 동일하나 단지 apiexample.c에서 영상을 받아오는 방법과 틀릴뿐이다. apiexample.c에서는 영상을 받아와 AVFrmae *picture의 data에 넣어 주었다 하지만 여기서는 avpicture_fill()을 사용하여 캡쳐된 영상을 AVPicture 타입의 구조체에 넣어준다. RGB타입인 이 영상을 YUV방식으로 변환하기 위하여 img_convert()함수를 사용하여 RGB24형태의 영상을 YUV420P포맷으로 변환한 후 avcodec_encode_video()를 사용하여 인코드 시키는 것이다.


3. V4L에서 영상받기
--------------------

자세한 내용은 http://pl.smu.ac.kr/researches/projects/linux-dvr/ 의2004-02-02-ELF-Video4linux-Capture.txt 문서를 참조 하기 바란다. 여기서는 TV Card와 USB Camera를 세팅하기 위한 부분만 보도록 하겠다.

--------------------------------------------------------------------------------
int setChannel(struct video_channel chan, int channel_no, int device_type)
{
 switch(device_type)
 {
   /* Case 1 : TV Card setting */
   case 0 :
     chan.channel = channel_no;
     chan.flags = VIDEO_VC_TUNER;
     chan.type = VIDEO_TYPE_TV;
     chan.norm = VIDEO_MODE_NTSC;
     if((rv = ioctl(video, VIDIOCSCHAN, &chan)) < 0)
     {
return -1;
     }
     break;
   case 1 :
     chan.channel = channel_no;
     chan.type = VIDEO_TYPE_CAMERA;
     chan.norm = VIDEO_MODE_NTSC;
     if((rv = ioctl(video, VIDIOCSCHAN, &chan)) < 0)
     {
return -1;
     }
     break;
   default :
     break;
 }
 return 0;
}
--------------------------------------------------------------------------------

3.1 TV Card
------------

위의 소스 코드를 보면 TV Card의 경우 device_type의 값이 0일경우 대응됨을 알 수 있다. 이경우 우리는 총 4가지를 설정해준다. channel값과 flags, type, norm값등이다. 이런 세팅값들은 다른 TV Card들에서도 동일하다.
여기서 channel값은 일반적으로 가정용 비디오에서의 Input의 종류를 의미한다. flag값은 비디오 장치가 tuner를 가지고 있다는 것을 알려준다. tuner가 없을 경우 설정하지 않아도 된다. type값은 비디오 장치의 종류를 의미하고 여기서는 TV Card이기 때문에 VIDEO_TYPE_TVnorm값은 NTSC와 PAL이라는 TV신호의 종류를 의미한다.

3.2 USB
--------

위의 소스 코드를 보면 USB의 경우 device_type의 값이 1일경우 대응됨을 알 수 있다. 이경우 우리는 3가지를 설정해주는데 channel값과 type, norm값을 설정해 주면된다.


4. ffmpeg를 사용하여 encoding하기
----------------------------------

ffmpeg를 사용하여 encoding을 하기 위해서 우리는 크게 5가지의 단계를 거칠 것이다.
첫번째, avcodec_init();
두번째, avcodec_register_all();
세번째, InitCaptureDevice(DEVICE_NAME, VIDEO_INPUT);
네번째, CaptureImage();
다섯번재, SaveToFile(nMaxCount, map, IMG_W, IMG_H);
첫번째 단계는 avcodec_init()함수를 사용하여 avcodec을 초기화 하는 단계이다.
다음으로 우리는 초기화된 avcodec의 모든 codec을 등록해야 하는데 이일을 avcodec_register_all()함수가 해주게 된다.
그러면 일단 avcodec과 관련된 기본적인 설정은 끝난것이다.
다음으로 우리는 우리가 만든 initCaptureDevice()라는 함수를 부르게 된다. 여기에는 DEVICE_NAME와, VIDEO_INPUT이 인수로 들어가게 되는데 둘다 상수 값으로서 DEVICE_NAME는 TV 또는 USB Camera를 의미하고 VIDEO_INPUT은 말 그대로 input의 종류를 의미한다.
그 다음으로 우리는 설정된 디바이스에서 메모리로 사진을 가져오게 된다. 영상은 여러장의 사진이 모아진 것이므로 우리는 먼저 사진을 찍어야하고 이사진들을 여러장 찍어서 동영상을 만드는 작업을 하게 되는 것이다. 따라서, captureImage()라는 함수를 호출하게 된다. 이 함수는 장치 디바이스로 부터 사진을 받아와 v_map이라는 곳에다 저장하게 된다. 그것을 우리는 in_addr이라는 포인터로 접근 할 수 있다.
이렇게 가져온 사진들을 동영상으로 만들기위해 saveToFile(nMaxCount, map, IMG_W, IMG_H);와 같이 saveToFile()함수를 부르게 된다. 이때 같이 넘겨주는 인수는 총 프레임의 수와 v_map영상이 저장된 곳의 주소(여기서는 in_addr) 포인터, 이미지의 가로와 세로 길이를 넘겨준다. saveToFile()함수는 이러한 인수를 받아서 nMaxCount만큼 돌면서 map주소에서 IMG_W*IMG_H크기 만큼의 사진을 얻어와 우리가 설정해준 값에 따라서 파일로 저장해 준다.
그렇다면 이렇게 저장되기 위해서 우리는 어떠한 설정을 해주어야 하는 것이라는 것을 알수 있다. 우리는 encoding을 하기 전에 AVContext라는 구조체의 값을 설정해주어야 한다. 이것은 우리가 encoding할 파일의 정보를 설정하는 것으로서 bitrate, framerate등 영상파일에 대한 정보를 가지고 있다. 자세한 내용은 별첨 문서에서 알아보기로 한다.

4.1 saveToFile
---------------

-----------------------------------------------------------------------------
 /*
  *
  * Function    : saveToFile
  *
  * Description : After encoding a frame, save to file
  *
  * Global      : n_frame_count
  *
  * Arguments   : int n_max_count, char *int_addr, int n_width, int n_height
  *
  */
 void saveToFile( int n_max_count, char *in_addr, int n_width, int n_height )
 {
   unsigned char *y;
   unsigned char *u;
   unsigned char *v;
   int yy;
   int xx;
   int i = 0;

   AVCodec *codec = NULL;
   AVCodecContext *c = NULL;
   int out_size = 0, size = 0, outbuf_size = 0;
   AVFrame *picture = NULL;
   uint8_t *outbuf = 0, *picture_buf = 0;

   if ( n_frame_count == 0 )
   {
     f = fopen ( "test.mpg", "w" );
     if ( !f )
     {
       fprintf( stderr, "could not open test.mpg\n" );
       exit( 1 );
     }
   }

   if( n_frame_count < n_max_count )
   {
     n_frame_count++;
     printf( "Video encoding\n" );

     /* find the mpeg1 video encoder */
     codec = avcodec_find_encoder( CODEC_ID_MPEG1VIDEO );
     if ( !codec )
     {
       fprintf( stderr, "codec not found\n" );
       exit( 1 );
     }
   
     c = avcodec_alloc_context();
     picture = avcodec_alloc_frame();

     /*format*/
     c->get_format = PIX_FMT_YUV420P;
     /* put sample parameters */
     c->bit_rate = 400000;
     /* resolution must be a multiple of two */
     printf("%d, %d\n", n_width, n_height);
     c->width = n_width;
     c->height = n_height;
     /* frames per second */
     c->frame_rate = 30;  
     c->frame_rate_base= 1;
     c->gop_size = 10; /* emit one intra frame every ten frames */
     
     /*maximum number of b frames between non b frames.
       note: the output will be delayed by max_b_frames+! relative to th input
       - encodeing : set by user.
       - decoding: unused*/
     c->max_b_frames=1;

     /* open it */
     if ( avcodec_open( c, codec ) < 0 )
     {
       fprintf( stderr, "could not open codec\n" );
       exit( 1 );
     }
   
     /* the codec gives us the frame size, in samples */
     /* alloc image and output buffer */
     outbuf_size = 100000;
     outbuf = malloc( outbuf_size );
     size = c->width * c->height;
     
     picture_buf = malloc( ( size * 3 ) / 2 ); /* size for YUV 420 */
     picture->data[0] = picture_buf;
     picture->data[1] = picture->data[0] + size;
     picture->data[2] = picture->data[1] + size / 4;
     picture->linesize[0] = c->width;
     picture->linesize[1] = c->width / 2;
     picture->linesize[2] = c->width / 2;
   
     //dp = (unsigned char *)out_addr;
     y = ( unsigned char * )in_addr;
     u = y + n_width * n_height;
     v = u + n_width * n_height / 4;

     fflush( stdout );
     /* prepare a dummy image */
     /* Y */
     for( yy = 0; yy < c->height; yy++ )
     {
       for( xx = 0; xx < c->width; xx++ )
       {
   picture->data[0][yy * picture->linesize[0] + xx] = *y;
   y++;
       }
     }
     /* Cb and Cr */
     for( yy = 0; yy < c->height / 2; yy++ )
     {
       for( xx = 0; xx < c->width / 2; xx++ )
       {
 picture->data[1][yy * picture->linesize[1] + xx] = *u;
     picture->data[2][yy * picture->linesize[2] + xx] = *v;
     u++; v++;
       }
     }

     /* encode the image */
     for( i = 0; i < 2; i++ )
       out_size = avcodec_encode_video( c, outbuf, outbuf_size, picture );
     
     printf( "encoding frame %3d (size=%5d)\n", i, out_size );
     fwrite(outbuf, 1, out_size, f);
   }
 
   if ( n_frame_count == n_max_count )
   {
     n_frame_count++;
     
     /* get the delayed frames */    
     for( ; out_size; i++ )
     {
       fflush( stdout);        
       out_size = avcodec_encode_video( c, outbuf, outbuf_size, NULL );
       printf( "write frame %3d (size=%5d)\n", i, out_size );
       fwrite( outbuf, 1, out_size, f );
     }

     /* add sequence end code to have a real mpeg file */
     outbuf[0] = 0x00;
     outbuf[1] = 0x00;
     outbuf[2] = 0x01;
     outbuf[3] = 0xb7;
     fwrite( outbuf, 1, 4, f );
     fclose( f );
     free( picture_buf );
     free( outbuf );

     avcodec_close( c );
     free( c );
     free( picture );
     printf( "\n" );
   }
 }
-----------------------------------------------------------------------------
위의 소스를 보고 순서 대로 알아보자
여기서는 MPEG1을 사용한 인코딩의 방법만을 알아본다.
우리는 먼저 avcodec_find_encoder() 함수를 사용하여 우리가 사용할 코덱을 찾는다 이 작업이 끝나면 AVContext 형의 c에 avcodec_alloc_context();를 사용하여 메모리 공간을 할당해 준다. 그 다음에 AVFrame 형의 picture에 avcodec_alloc_frame();함수를 사용하여 메모리 공간을 할당하여 준다. 그런후 구조체 c에 값들을 세팅해준다. 여기서 세팅하는 값은 나중에 encoding을 할경우 인자로 같이 넘겨줄 값이다.
그런 다음 우리는 실제 영상이 들어갈 곳을 실제 영상이 있는 곳과 연결 시켜 준다. 이부분은 RGB이인지 YUV인지에 따라서 틀려지는 부분이 될 것이다. 여기서는 YUV420P포멧이기 때문에 Y, U, V 3개로 나눈다. 이것은 picture의 의 멤버 변수인 data[]배열에 연결 시켜주는데, 우리가 v_map에서 얻어온 in_addr의 주소를 data[0]에 넣어주고 data[1]와 data[2]에는 Y값이 차지하는 공간을 계산하고 in_addr주소에 Y가 차지하는 주소 공간을 더한 값을 넣어주게 된다. 이렇게 세팅을 한후 이미지를 인코딩 하는데 이미지가 처음에는 지연에 의해서 인코딩 되지 않기 때문에 2번의 인코딩을 함으로써 우리는 실제 데이터를 얻을 수 있다. 따라서, for문 안에 avcodec_encode_video( c, outbuf, outbuf_size, picture );를 2번 돌려서 우리가 원하는 압축된 사진을 얻을수 있는 것이다. 이렇게 나온 사진을 우리는 바로 파일에 저장한다. 이렇게 끝난후에 우리는 지연된 이미지가 있는지 알아보기위해 처음부터 out_size만큼 지연된 프레임을 확인하고 다시 파일에 저장 하게 된다. 그런후 파일의 마지막에 mpeg헤더를 추가한후에 모든 자원을 해제하고 끝내게 된다.

4.2 포맷에 따는 각각의 MPEG압축 방법
-------------------------------------

포맷과 함수를 사용하는지 마는지에 따라서 우리는 전부 4가지 경우의 사례를 볼 수 있다. 이러한 부분은 전부가 picture의 data배열에 자료를 넣어주는 방법에서 차이를 보이게 된다. 이러한 차이에 대해서 이제부터 알아볼것이다.

4.2.1 YUV420P
--------------

-----------------------------------------------------------------------------
     picture_buf = malloc( ( size * 3 ) / 2 ); /* size for YUV 420 */
     picture->data[0] = picture_buf;
     picture->data[1] = picture->data[0] + size;
     picture->data[2] = picture->data[1] + size / 4;
     picture->linesize[0] = c->width;
     picture->linesize[1] = c->width / 2;
     picture->linesize[2] = c->width / 2;
   
     //dp = (unsigned char *)out_addr;
     y = ( unsigned char * )in_addr;
     u = y + n_width * n_height;
     v = u + n_width * n_height / 4;

     fflush( stdout );
     /* prepare a dummy image */
     /* Y */
     for( yy = 0; yy < c->height; yy++ )
     {
       for( xx = 0; xx < c->width; xx++ )
       {
   picture->data[0][yy * picture->linesize[0] + xx] = *y;
   y++;
       }
     }
     /* Cb and Cr */
     for( yy = 0; yy < c->height / 2; yy++ )
     {
       for( xx = 0; xx < c->width / 2; xx++ )
       {
 picture->data[1][yy * picture->linesize[1] + xx] = *u;
     picture->data[2][yy * picture->linesize[2] + xx] = *v;
     u++; v++;
       }
     }
-----------------------------------------------------------------------------
AVFrame형의 data[0]에는 우리는 원래 YUV420P포맷의 영상을 가지고 있는 주소를 넣어준다. 그런후에 포인터를 Y의 크기 만큼 증가 시킨후에 U의 주소를 data[1]에 넣어주고 V의 주소를 data[2]에 넣어준다. 그런후에 linesize를 정해주는데, Y는 원래 width이고 U,V는 크기가 압축되어 원래 width크기의 반절밖에 안되기 때문에 라인사이즈는 width/2가 된다. 그리고 for문을 돌리면서 각각의  data의 개개별 값에 y, u, v값을 넣어주게 된다.

4.2.2 RGB24->YUV420P
---------------------

-----------------------------------------------------------------------------
   picture_buf = malloc((size * 3) / 2); /* size for YUV 420 */
   picture->data[0] = picture_buf;
   picture->data[1] = picture->data[0] + size;
   picture->data[2] = picture->data[1] + size / 4;
   picture->linesize[0] = c->width;
   picture->linesize[1] = c->width / 2;
   picture->linesize[2] = c->width / 2;

   newPicture.data[0] = picture->data[0];
   newPicture.data[1] = picture->data[1];
   newPicture.data[2] = picture->data[2];
   newPicture.linesize[0] = picture->linesize[0];
   newPicture.linesize[1] = picture->linesize[1];
   newPicture.linesize[2] = picture->linesize[2];

   avpicture_fill(&myPicture, in_addr, PIX_FMT_BGR24, c->width, c->height);
   img_convert(&newPicture, PIX_FMT_YUV420P, &myPicture, PIX_FMT_BGR24, c->width, c->height);
   
   fflush(stdout);
   /* prepare a dummy image */
   /* Y */
   for(y=0;y<c->height;y++)
   {
     for(x=0;x<c->width;x++)
     {
picture->data[0][y * picture->linesize[0] + x] = newPicture.data[0][y * newPicture.linesize[0] + x];
     }
   }
   
   /* Cb and Cr */
   for(y=0;y<c->height/2;y++)
   {
     for(x=0;x<c->width/2;x++)
     {
picture->data[1][y * picture->linesize[1] + x] = newPicture.data[1][y * newPicture.linesize[1] + x];;
picture->data[2][y * picture->linesize[2] + x] = newPicture.data[2][y * newPicture.linesize[2] + x];;
     }
   }
-----------------------------------------------------------------------------
RGB24->YUV420P와 YUV420P와 다른 점은 encoding은 YUV420P로 하지만 처음에 영상을 받은 후에 이미지를 컨버트 하는 점이다. 우리는 avpicture_fill()이란 함수를 사용해 AVPicture형의 구조체에 영상을 받아오게 된다. 받아온 영상은 RGB24포맷이고 이것을 우리는 img_convert()함수를 사용해 YUV420P포맷으로 변화하게 된다. 변화된 영상은 새로운 AVPicture형의 구조체에 할당 되게 되고 이것을 AVFrame의 구조체의 data[0]부터 data[1], data[2]에 넣어주므로서 그냥 YUV420P포맷을 사용했을때와 별 다를바없이 MPEG encoding을 할수 있게 된다.

4.2.3 YUV library함수 사용하기
-------------------------------

-----------------------------------------------------------------------------
   avpicture_fill(picture, in_addr, PIX_FMT_YUV420P, c->width, c->height);
-----------------------------------------------------------------------------
위에서 보았던 것과 같이 AVFrame의 data배열에 직접 넣어주어야 했던 실제 데이터 영상을 우리는 avpicture_fill()이라는 함수를 통해서 자동으로 할 수 있다. 하지만, 여기서는 약간의 문제가 생기게 된다. 무엇이냐면, avpicture_fill()함수의 첫번째인자는 즉 데이터가 들어가게 되는 곳의 형이 AVPicture타입이라는 점이다. AVPicture와 AVFrame의 차이점은 별첨 문서에서 확인하게 될것이다. 이런 것은 우리가 원하는 영상을 얻는데 영향을 미치지는 않는다. 하지만, 컴파일시 warning메세지를 출력하게 된다.
이렇게 간단하게 해주고 우리는 encoding함수를 호출하면 된다.


4.2.4 RGB24->YUV420P library함수 사용하기
------------------------------------------

-----------------------------------------------------------------------------
   avpicture_fill(&temp_picture, buf1, PIX_FMT_BGR24, c->width, c->height);
   avpicture_fill(&temp_picture, in_addr, PIX_FMT_BGR24, c->width, c->height);
   avpicture_fill(&new_picture, buf, PIX_FMT_YUV420P, c->width, c->height);
   img_convert(&new_picture, PIX_FMT_YUV420P, &temp_picture, PIX_FMT_BGR24, c->width, c->height);
   picture = (AVFrame *)&new_picture;
-----------------------------------------------------------------------------
RGB24에서 YUV420P로 변화시키는 과정은 위에서 보았던 함수를 사용하지 않는 것과 차이가 없다. 단지 data배열에 넣는 과정을 함수화 했다는 것이다. 실제 ffmpeg library에 있는 함수를 사용하게 되는것이다. 그리고 형의 충돌을 막기위해 AVFrame형의 picture에 AVPicture형의 new_picture를 넣을때 (AVFrame *)으로 형변환을 해주게 된다.

5. 요약
--------

ffmpeg library에 대한 문서가 정확히 나와있지 않은 상황에서 작성된 문서라 많은 부분이 부족 할 것으로 생각된다. 이 문서를 작성하는 동안에도 새로운 것을 알게 되었고 그부분에 대해서 추가 하지는 못했다. 새로운 방향으로 소스 코드를 고쳐야 하는 문제가 생겼기 때문이다. 그렇지만 기본적으로 영상을 저장하는데는 문제가 없었고 약 1개월동안 Mailing List를 보면서 알게된 점에대해서 부족하지만 적어 보았다. 많은 도움이 되기를 바라고 문제가 있을시에는 director@smu.ac.kr으로 연락 바란다.


끝.

+ Recent posts