libavcodec을 사용한 H.264 인코딩/디코딩 - C code

by Blogger 하얀쿠아
2012. 10. 3. 05:58 수행 프로젝트 이력/스마트폰을 이용한 감시시스템 [2011.09~ 2012.09]


* 스마트폰을 이용한 가정용 화상감시 장비 프로젝트 진행 과정에서 얻게된 지식을 공유하는 포스팅 입니다.


본 프로젝트의 전체적인 시스템 구성도는 아래와 같다.



여기에서 코덱이 필요한 이유는, 감시 영상을 네트워크를 통해 전달하기 때문이다.

raw data를 그대로 전달 할 경우, 1프레임 영상의 크기가 1메가를 상회하게 된다. 

(지원 해상도는 QCIF, QVGA 만 허용함. 이는 각각 352 x 288과 320 x 240 이다.)


네트워크 대역폭 제한으로 인해 받아 보는 쪽에서 영상의 프레임이 낮을 것이다.

또한 3G/4G 를 사용한다면 데이터 요금도 상당할 것이다.

 이를 h.264 코덱으로 인코딩 하여 네트워크로 보내게 된다면, 이런 문제를 해결 가능 할 것이라 판단하여 코덱을 사용하였다. 물론 인코딩 하는 카메라 스마트폰의 CPU사용량이 상당히 증가할 것이지만, 뷰어 측의 원할한 영상 수신을 위해서 이정도는 감수하기로 하였다.


감시카메라 Application을 구동하는 스마트폰에서는 입력되는 카메라 영상을 H.264 코덱으로 인코딩 하여 중계서버로 전송한다.


사용자뷰어 Application을 구동하는 스마트폰에서는 중계서버로부터 전송받는 인코딩된 데이터를 디코딩하여 사용자에게 보여준다.


위 과정에서 이루어지는 인코딩/디코딩은 ffmpeg 를 android 용으로 포팅하여 사용했다.

1. 안드로이드에서 ffmpeg의 라이브러리 함수를 호출할 수 있도록 C로 인터페이스 함수를 만들었다.

2. 이 인터페이스 함수들만 모아 .so파일로 만들어 dynamic link 하여 사용하였다.


아래는 인터페이스 함수 제작 과정에 참고한 예제 코드이다.


출처 : http://natnoob.blogspot.kr/search?updated-min=2011-01-01T00:00:00%2B07:00&updated-max=2012-01-01T00:00:00%2B07:00&max-results=6


이 예제는 api-example을 기반으로 구현했다.

이것은 C코드를 이용하여 raw data 영상(QCIF)을 H264로 인코딩 하는 방법과 인코딩 된 영상을 yuv로 디코딩 하는 방법을 보여준다.



 - 아래는 main 함수 부분이다.

int main(int argc, char **argv)

{


/* must be called before using avcodec lib */

avcodec_init();


/* register all the codecs */

avcodec_register_all();


h264_encode_decode("Foreman.qcif","Decoded.yuv");


return 0;

}



- 시작 부분은 언제나 코덱의 등록과 초기화이다.

그 후 이 예제에서는 h264_encode_decode( ) 함수를 호출할 것인데, 이 함수는 Foreman.qcif 라는 파일로 입력되는 영상을 h.264로 인코딩 한 뒤, 다시 yuv 파일로 디코딩 하여 Decoded.yuv 로 저장 할 것이다.


총 5단계로 나눠서 보이겠다.

1. 인코딩/디코딩에 사용할 변수 선언

AVCodec *codecEncode, *codecDecode;

AVCodecContext *ctxEncode= NULL, *ctxDecode = NULL; 


FILE *fin, *fout;

AVFrame *pictureEncoded, *pictureDecoded;


uint8_t *encoderOut, *picEncodeBuf;

int encoderOutSize, decoderOutSize;

int pic_size;


AVPacket avpkt;

int got_picture, len;


const int clip_width = 176;

const int clip_height = 144;


int frame = 0;

uint8_t *decodedOut;



2. 코덱 초기화/디코더를 위한 picture 구조체

codecDecode = avcodec_find_decoder(CODEC_ID_H264);

if (!codecDecode) {

fprintf(stderr, "codec not found\n");

exit(1);

}


ctxDecode= avcodec_alloc_context();

avcodec_get_context_defaults(ctxDecode);

ctxDecode->flags2 |= CODEC_FLAG2_FAST;

ctxDecode->pix_fmt = PIX_FMT_YUV420P;

ctxDecode->width = clip_width;

ctxDecode->height = clip_height; 

ctxDecode->dsp_mask = (FF_MM_MMX | FF_MM_MMXEXT | FF_MM_SSE);


if (avcodec_open(ctxDecode, codecDecode) < 0) {

fprintf(stderr, "could not open codec\n");

exit(1);

}


pictureDecoded= avcodec_alloc_frame();

avcodec_get_frame_defaults(pictureDecoded);

pic_size = avpicture_get_size(PIX_FMT_YUV420P, clip_width, clip_height);


decodedOut = (uint8_t *)malloc(pic_size);

fout = fopen(fileout, "wb");

if (!fout) {

fprintf(stderr, "could not open %s\n", fileout);

exit(1);

}



3. 코덱 초기화/인코더를 위한 picture 구조체

codecEncode = avcodec_find_encoder(CODEC_ID_H264);

if (!codecEncode) {

printf("codec not found\n");

exit(1);

}


ctxEncode= avcodec_alloc_context();

ctxEncode->coder_type = 0; // coder = 1

ctxEncode->flags|=CODEC_FLAG_LOOP_FILTER; // flags=+loop

ctxEncode->me_cmp|= 1; // cmp=+chroma, where CHROMA = 1

ctxEncode->partitions|=X264_PART_I8X8+X264_PART_I4X4+X264_PART_P8X8+X264_PART_B8X8; // partitions=+parti8x8+parti4x4+partp8x8+partb8x8

ctxEncode->me_method=ME_HEX; // me_method=hex

ctxEncode->me_subpel_quality = 0; // subq=7

ctxEncode->me_range = 16; // me_range=16

ctxEncode->gop_size = 30*3; // g=250

ctxEncode->keyint_min = 30; // keyint_min=25

ctxEncode->scenechange_threshold = 40; // sc_threshold=40

ctxEncode->i_quant_factor = 0.71; // i_qfactor=0.71

ctxEncode->b_frame_strategy = 1; // b_strategy=1

ctxEncode->qcompress = 0.6; // qcomp=0.6

ctxEncode->qmin = 0; // qmin=10

ctxEncode->qmax = 69; // qmax=51

ctxEncode->max_qdiff = 4; // qdiff=4

ctxEncode->max_b_frames = 3; // bf=3

ctxEncode->refs = 3; // refs=3

ctxEncode->directpred = 1; // directpred=1

ctxEncode->trellis = 1; // trellis=1

ctxEncode->flags2|=CODEC_FLAG2_FASTPSKIP; // flags2=+bpyramid+mixed_refs+wpred+dct8x8+fastpskip

ctxEncode->weighted_p_pred = 0; // wpredp=2

ctxEncode->bit_rate = 32000;

ctxEncode->width = clip_width;

ctxEncode->height = clip_height;

ctxEncode->time_base.num = 1;

ctxEncode->time_base.den = 30;

ctxEncode->pix_fmt = PIX_FMT_YUV420P; 

ctxEncode->dsp_mask = (FF_MM_MMX | FF_MM_MMXEXT | FF_MM_SSE);

ctxEncode->rc_lookahead = 0;

ctxEncode->max_b_frames = 0;

ctxEncode->b_frame_strategy =1;

ctxEncode->chromaoffset = 0;

ctxEncode->thread_count =1;

ctxEncode->bit_rate = (int)(128000.f * 0.80f);

ctxEncode->bit_rate_tolerance = (int) (128000.f * 0.20f);

ctxEncode->gop_size = 30*3; // Each 3 seconds


/* open codec for encoder*/

if (avcodec_open(ctxEncode, codecEncode) < 0) {

printf("could not open codec\n");

exit(1);

}


//open file to read

fin = fopen(filein, "rb");

if (!fin) {

printf("could not open %s\n", filein);

exit(1);

}


/* alloc image and output buffer for encoder*/

pictureEncoded= avcodec_alloc_frame();

avcodec_get_frame_defaults(pictureEncoded);


//encoderOutSize = 100000;

encoderOut = (uint8_t *)malloc(100000);

//int size = ctxEncode->width * ctxEncode->height;

picEncodeBuf = (uint8_t *)malloc(3*pic_size/2); /* size for YUV 420 */

pictureEncoded->data[0] = picEncodeBuf;

pictureEncoded->data[1] = pictureEncoded->data[0] + pic_size;

pictureEncoded->data[2] = pictureEncoded->data[1] + pic_size / 4;

pictureEncoded->linesize[0] = ctxEncode->width;

pictureEncoded->linesize[1] = ctxEncode->width / 2;

pictureEncoded->linesize[2] = ctxEncode->width / 2; 



4. 입력 파일로 부터 데이터를 읽고, avcodec_encode_video 를 사용해서 인코딩 한다.

인코딩된 데이터는 디코더로 보내질 것인데, yuv 포맷으로 디코딩 된 결과가 나올 것이고, avcodec_decode_video2 를 사용할 것이다. 

디코딩 된 데이터는 decoded.yuv 라는 파일로 만들 것 이다.

//encode and decode loop

for(int i=0;i<30;i++) 

{

fflush(stdout);

//read qcif 1 frame to buufer

fread(pictureEncoded->data[0],ctxEncode->width * ctxEncode->height, 1, fin);

fread(pictureEncoded->data[1],ctxEncode->width * ctxEncode->height/4, 1, fin);

fread(pictureEncoded->data[2],ctxEncode->width * ctxEncode->height/4, 1, fin); 

pictureEncoded->pts = AV_NOPTS_VALUE;


/* encode frame */

encoderOutSize = avcodec_encode_video(ctxEncode, encoderOut, 100000, pictureEncoded);

printf("encoding frame %3d (size=%5d)\n", i, encoderOutSize);

if(encoderOutSize <= 0)

continue;


//send encoderOut to decoder

avpkt.size = encoderOutSize;

avpkt.data = encoderOut;

//decode frame

len = avcodec_decode_video2(ctxDecode, pictureDecoded, &got_picture, &avpkt);

if (len < 0) {

printf("Error while decoding frame %d\n", frame);

exit(1);

}

if (got_picture) {

printf("len = %d saving frame %3d\n", len, frame);

fflush(stdout);


avpicture_layout((AVPicture *)pictureDecoded, ctxDecode->pix_fmt

, clip_width, clip_height, decodedOut, pic_size);

fwrite(decodedOut, pic_size, 1, fout);

frame++;

}

}



5. 할당된 메모리를 해제하고, 파일 포인터를 닫아준다.

fclose(fout);

fclose(fin);


avcodec_close(ctxEncode);

avcodec_close(ctxDecode);

av_free(ctxEncode);

av_free(ctxDecode);

av_free(pictureEncoded); 

av_free(pictureDecoded); 



나는 이 구현코드가 libavcodec을 사용해서 C언어로 h264의 인코딩과 디코딩을 올바르게 하는 방법을 보여주는 적절한 예제라고 생각한다.

이 예제코드를 바로 사용하긴 힘들지만, 인코딩/디코딩 하는 시나리오를 이해하는데 도움이 될 것이다.



이 댓글을 비밀 댓글로
    • 밀레니안
    • 2012.10.12 17:42
    안녕하세요..? 저랑 비슷한 프로젝트를 진행중이셔서 질문이 있어 글 남깁니다..

    저도 ffmpeg를 이용해서 동영상 인코딩을 하는데

    안드로이드에서 영상을 받을때 mediarecoder를 이용해 raw data를 받아서 하신건가요?
    미디어레코더와 로컬 소켓을 이용해 받은 것을 어떻게 활용해야 할지 모르겠어서
    현재 카메라 프리뷰 콜백을 이용해 이미지를 영상으로 인코딩해서 하고 있는데

    최저 화질을 사용해도 15초 촬영에 인코딩 시간이 20초 걸려서 활용할 수가 없어 막혀있습니다....

    미디어레코더로 받은 것을 활용하신거라면...노하우좀 전수해주세요 ㅠㅠ
    • 저 역시 카메라 프리뷰 콜백을 이용했습니다. ^^;
      콜백의 파라미터로 들어오는 byte[ ]형태의 1프레임 짜리 이미지를 콜백 호출이 될 때마다 인코더에 넣어서 인코딩 하는 방식으로 구현했습니다.

      저같은 경우는 영상을 파일로 저장하는게 목적이 아니라 네트워크를 통해 Viewer 쪽에 최대한 빠르게 전송 + 1프레임 영상 하나하나에 대한 영상처리(차영상을 통한 detection & tracking)가 목적이었기 때문에 미디어레코더를 애초에 배제하고 raw image를 받을 수 있는 카메라 프리뷰를 이용했네요.
      제 포스팅이 도움이 되지는 못할 듯 해서 아쉽습니다 ^^;
    • 밀레니안
    • 2012.10.13 01:10
    아...감사합니다..

    Dev고양이님의 글이 저에게 충분히 도움이 되고있습니다...;;

    모든 것을 ffmpeg.c에 기대어 할 생각을 했는데 따로 인코딩 소스를 짜서 할 생각이 생기게 해주셨네요..

    한가지 질문 더 할게요 ㅠㅠ

    위에 적은것 처럼 저두 콜백 이용해서 하는 중인데 저의 경우에는 콜백 통해 받은 바이트를 한 파일에 이어 붙여 그걸 인코딩 하는 식으로 했는데 Yuvimage 객체 를 이용해 사이즈를 변환 해 보려구 했지만 jpeg 로 압축하지 않으면 800X480에서 변하질 않더군여;; 352X288 로는 어떻게 변환 하신건가요?
    사이즈가 줄어들면 인코딩 시간이 줄어들까 싶어서..
    • Camera 객체를 open 한 이후에 여러가지 세팅을 하고(set 계열 메소드들을 호출함으로써) 최종적으로 startPreview( ) 메소드를 호출 하여 카메라 콜백을 하셨을 텐데요.

      그 세팅 과정에서 다음과 같이 파라메터를 카메라에 넣음으로써 콜백함수의 byte[ ] 영상의 해상도를 설정 할 수 있습니다. (try catch 는 댓글의 간략화를 위해 생략했습니다)

      Camera mCamera = Camera.open();
      Camera.Parameters parameters = mCamera.getParameters();
      parameters.setPictureSize(width, height); // width 352, height 288 넣으면.
      mCamera.setParameters(parameters);

      ** 여기서 문제가 있습니다. **
      만약 setPictureSize 에 임의의 수치를 넣게 되면 예외가 발생할 수 있는데, 이 코드가 돌고 있는 안드로이드 폰이 지원하지 않는 해상도를 넣게 되면 예외가 발생 하게됩니다.

      넥서스S 는 352x288을 지원하지만(확실) 320x240을 지원하지 않고요.(확실)
      갤럭시S2는 320x240을 지원하지만(확실), 352x288을 지원 하지 않았던것 같습니다.(긴가민가)

      다음과 같이 현재 코드가 돌고 있는 폰이 지원하는 해상도를 List 형태로 얻을수 있습니다.

      // 카메라에서 찍을 수 있는 모든 사이즈를 List 형태로 얻음

      Camera mCamera = Camera.open();
      Camera.Parameters parameters = mCamera.getParameters();
      List<Size> sizes = parameters.getSupportedPictureSizes();
      Size optimalSize;
      optimalSize = getOptimalPreviewSize(sizes, IMAGE_WIDTH, IMAGE_HEIGHT);
      parameters.setPictureSize(optimalSize.width, optimalSize.height);
      mCamera.setParameters(parameters);

      getOptimalPreviewSize( ) 함수는 뭐.. 다음과 같이 구현 하면 될 것 같습니다.
      파라메터는 지원하는 해상도 목록 sizes, 원하는 해상도의 width와 height 를 넣고요.
      그러면, getOptimalPreviewSize(sizes, 352, 288 ) 이렇게 호출했을때 352x288 지원 불가 폰이라면 가장 인접한 해상도를 리턴해주던지 null을 리턴해 줄 겁니다. (아마도 말이죠)

      private Size getOptimalPreviewSize(List<Size> sizes, int width, int height) {
      final double ASPECT_TOLERANCE = 0.05;
      double targetRatio = (double) width / height;
      if (sizes == null) {
      return null;
      }

      Size optimalSize = null;
      double minDiff = Double.MAX_VALUE;

      int targetHeight = height;

      // Try to find an size match aspect ratio and size
      for (Size size : sizes) {
      double ratio = (double) size.width / size.height;
      if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) {
      continue;
      }
      if (Math.abs(size.height - targetHeight) < minDiff) {
      optimalSize = size;
      minDiff = Math.abs(size.height - targetHeight);
      }
      }

      // Cannot find the one match the aspect ratio, ignore the requirement
      if (optimalSize == null) {
      minDiff = Double.MAX_VALUE;
      for (Size size : sizes) {
      if (Math.abs(size.height - targetHeight) < minDiff) {
      optimalSize = size;
      minDiff = Math.abs(size.height - targetHeight);
      }
      }
      }
      Log.i("optimal size", ""+optimalSize.width+" x "+optimalSize.height); //for debugging
      return optimalSize;
      }

      그럼 좋은 결과 있기를 바랍니다 ^^
    • 밀레니안
    • 2012.10.13 01:44
    헐...완전 자세한 답변에 감동했네요.. 감사합니다 (__)

    이걸로 인코딩 속도는 무조건 줄어들겠죠

    그리고 찾아보니 저랑 같은 곳에 소속되어 있는 곳이 있더군요^^; 지역은 틀리지만...

    반갑습니다. ㅋㅋ

    이 프로젝트 진행중이시던데 좋은 결과 얻길 바라겠습니다.
    • 아! 어느 지역에 계신가요?
      저는 곧 이 프로젝트 마감이라 한창 준비하고 있습니다ㅎ.
      아무튼 무척 반갑습니다. ^^
      해상도를 줄이면 아무래도 인코딩에 소요되는 시간은 줄어들거 같네요. 화이팅 입니다 ~
    • 밀레니안
    • 2012.10.14 21:51
    부산이에요..ㅎ

    15초짜리 20초에서 15초짜리 14초 정도로 줄어들었네요 ㅎㅎ 폰을 좀더 좋은걸 쓴다면 사용 하능 할 것 같아요 ^^

    도움 감사합니다.
    Dev고양이님도 화이팅이요~