제임스딘딘의
Tech & Life

수행 프로젝트 이력/스마트폰을 이용한 감시시스템 [2011.09~ 2012.09]

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

제임스-딘딘 2012. 10. 3. 05:58


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


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



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

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의 인코딩과 디코딩을 올바르게 하는 방법을 보여주는 적절한 예제라고 생각한다.

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