레이블이 비디오인 게시물을 표시합니다. 모든 게시물 표시
레이블이 비디오인 게시물을 표시합니다. 모든 게시물 표시

2016년 2월 21일 일요일

MPEG-4 Part 10 (H.264/AVC) - NAL(Network Abstraction Layer) 분석

Introduction

Video Coding Layer

: H.264/AVC 표준은 애초부터 다양한 네트워크 전송환경에 대응하기 위해 NAL 포맷을 적용하였다. ( 적절한 헤더정보와 함께, VCL(video coding layer) 포맷으로 비디오를 표현하는 방식 ) 즉, NAL은 "network friendliness" 를 표방하여, 단순하고 효과적으로 VCL을 커스터마이징 할 수 있게 한다. 
 즉 단순히 얘기하면, 비디오 인코딩/디코딩 과정을 네트워크 어플리케이션에 최적화(좀 더 유연하고, 간편하게) 되도록 만든 작업이다. 


avc/h264 stream 구성


NAL : Network Abstraction Layer
NAL = NAL unit + RBSP
NAL unit : start prefix
    Network Coding Layer
  • SPS : 프로파일 , 레벨 ,해상도 , 포맷 등 파일 전체에 대한 포괄적인 정보가 부화화하여 저장되어 있다. ( 00 00 00 01 67  ~ )
  • PPS : PPS는 SPS가 정의하는 내용보다 조금 더 세부적인 내용을 포함한다. 디코딩되는 픽처에 적용되는 파라미터를 포함한다. ( 00 00 00 01 68 ~ )
  • IDR Picture :  디코딩에대한 기본정보가 초기화되는 Picture 이다.  Reference 이미지를 사용하지 않고 독자적으로 디코딩 될 수 있는 Picture. I-slice 나 SI-slice 를 나타낸다. ( 00 00 00 01 65 ~ ) ( IDR picture 앞에는 반드시 SPS , PPS 정보가 들어가 있어야 한다. )

NAL types


NAL types table 에서 나타낸 types number 는 실제 byte stream 을 열어보면


AUD : 00 00 00 01 09
SPS : 00 00 00 01 67
PPS : 00 00 00 01 68


이런식으로 나타난다. 00 00 00 01 이 일종의 구분자가 되기 때문이다. 즉, SPS NAL 을 표현하고 싶다면, 해당 NAL unit 에 00 00 00 01 67을 적어주고, RBSP 부분에 SPS 정보를 적어주면 된다. 

Slice는 마찬가지로 NAL 의 일종인데, 실제로 picture를 저장하는 부분이며, 헤더와 Micoblock로 이루어진 data 파트로 나뉘어진다. 익히 아는 I,B,P picture ( 이전 게시물을 참조 )를 포함한 정보를 담고있다.





Slice types
더 자세한 정보 :
http://gentlelogic.blogspot.kr/2011/11/exploring-h264-part-2-h264-bitstream.html


ffmpeg 라이브러리를 활용하여 x264 규격으로 transcoding , live streaming server 제작 (encoding)

: 이미 디코딩 된 raw image를 encoding 하기 위해선, 필터링 ( 혹은 software scaling ) , 코덱 설정 등의 과정이 필요하다.

우선 코덱 format 을 지정할 컨텍스트와, 데이터를 받을 frame 과 packet 자료형이 필요하며, software scaling에 쓰일 자료형도 필요하다.

특이한 점은 raw 데이터를 받을 frame에 미리 buffer 를 연결해야 한다는 것 이다.

여기서 우리는 AVC 규격으로 인코딩 할 것임으로, x264 함수를 직접 작성하여 인코딩을 해보도록 한다. ( 원래는 ffmpeg 설치폴더에 libavutil/libx264.c에 작성되어있는 코드이지만, 필요한 부분만 발췌하여 사용하기 위해 따로 x264function.c를 만들었다. )

( 아래 예제 코드는 이전 게시물과 연동되어 사용됩니다. )


  • 자료형 선언
  AVCodecContext     *pCodecCtx = NULL;
  AVCodecContext     *pCodecCtxOut= NULL;
  AVFrame               *pFrameOut = NULL;
  AVPacket                packetOut;
  uint8_t                  *buffer = NULL;
  struct SwsContext    *sws_ctx = NULL;
  int                         frameFinished;
  int                         numBytes;
  int                         got_output;
  int                         frame_count;
  int                         i, videoStream;
  • 송출할 frame 과 buffer 를 연결한다.
  pFrameOut=av_frame_alloc();
  if(pFrameOut==NULL)
    return -1;
  // Determine required buffer size and allocate buffer
  numBytes=avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width,pCodecCtx->height);
  buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
  printf("Required buffer size : %d bytes\n",numBytes);
  avpicture_fill((AVPicture *)pFrameOut, buffer, AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height);
 : picture를 AVC 규격으로 저장할 것 이기 떄문에 YUV420P로 저장할것을 약속한다.
  • 인코딩에 필요한 codec context 를 구성한다.
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!pCodec || pCodec==NULL) {
   fprintf(stderr, "Codec not found\n");
   exit(1);
pCodecCtxOut = avcodec_alloc_context3(pCodec);
if (!pCodecCtxOut || pCodecCtxOut==NULL) {
   fprintf(stderr, "Could not allocate video codec context\n");
   exit(1);
} // put sample parameters 
pCodecCtxOut->bit_rate = BIT_RATE;
pCodecCtxOut->width = WIDTH;
pCodecCtxOut->height = HEIGHT;
pCodecCtxOut->time_base = (AVRational){1,FPS};
pCodecCtxOut->gop_size = GOP_SIZE;
pCodecCtxOut->max_b_frames = 1;
pCodecCtxOut->pix_fmt = AV_PIX_FMT_YUV420P;
av_opt_set(pCodecCtxOut->priv_data, "preset", "slow", 0);
if(avcodec_open2(pCodecCtxOut, pCodec, NULL)<0) return -1;
pFrameOut->width = WIDTH;
pFrameOut->height = HEIGHT;
pFrameOut->format = pCodecCtxOut->pix_fmt;
ret = av_image_alloc(pFrameOut->data, pFrameOut->linesize, pCodecCtxOut->width,pCodecCtxOut->height,pCodecCtxOut->pix_fmt, 32);
if (ret < 0) {
fprintf(stderr, "Could not allocate raw picture buffer\n");
exit(1);  
}

코덱과 picture 를받을 frame 환경설정을 한다. BIT_RATE WIDTH HEIGHT FPS GOP_SIZE 등을 조절하여 AVC 의 프로필레벨을 조절할 수 있다.
( 필자는 800000, 176, 144, 15, 1로 하여 쓸만한 화질을 얻을 수 있었다.) 
  • X264 코덱 적용
X264_init(pCodecCtxOut);
: ffmpeg/libavcodec/libx264.c 에서 정의된 함수이다. 위에서 정의한 코덱 환경설정을 avc 코덱 규격에 맞게 적용시켜주는 역할을 한다. ( 인코딩 시 x264_frame 함수로 할 것 이기 때문에 규격을 맞춰주는것은 필수이다. )
  • software scaling 준비 ( 스케일러 환경설정 )
sws_ctx = sws_getContext(pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
pCodecCtxOut->width,
pCodecCtxOut->height,
pCodecCtxOut->pix_fmt,
SWS_FAST_BILINEAR,
NULL,
NULL,
NULL
);
: 소프트웨어 스케일링이란 일종의 필터링 같은 개념이다. 디코딩되어 얻은 raw image를 인코딩 규격에 맞게 변환시켜 주는 과정이다. ffmpeg 는 스케일링 기능을 포함한 필터링 기능을 지원하지만, 우선은 스케일링만으로 사용해본다.
 
  • 인코딩 

while(av_read_frame(pFormatCtx, &packet)>=0 )
{
  if(packet.stream_index==videoStream)
  {
   // Decode video frame
   avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
   av_init_packet(&packetOut);
   fflush(stdout);
   if(frameFinished)
   {
   pFrameOut->pts=frame_count;
   // software scaleling
   sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,pFrame->linesize, 0, pCodecCtx->height,pFrameOut->data, pFrameOut->linesize);
   // Encode video frame by x264
   ret = X264_frame(pCodecCtxOut, &packetOut, pFrameOut, &got_output);
   if (ret < 0) {
     fprintf(stderr, "Error encoding frame\n");
     exit(1);
   }
   if (got_output) {
      // ... 패킷 처리 ( 전송 or 파일쓰기 ) ... //
     }
   }
   frame_count++;
   //To avoid memory leak
   av_free_packet(&packet);
   av_init_packet(&packet);
   av_free_packet(&packetOut);
   }
}
: 흐름을 간단히 설명하면 , 파일 읽음 -> 디코딩 -> 스케일링 -> 인코딩 -> 반복 , 으로 구성된다. 지난 글에서 디코딩까지 진행했음으로 인코딩 부분만 적용시키면 된다. 이미지 컨데이너는 packet -> frame -> FrameOut -> packetOut 순으로 변경된다. 메모리 leak을 피하기 위해 반복문이 끝나면 항상 packet을 free 해준다. got_output은 packet을 통해 인코딩된 이미지가 정상적으로 받았다는것을 의미함으로, got_output을 확인하여 패킷단위로 데이터를 처리하면 될 것 이다.
int state = sendto(pkt_sock,pPacket,packetOut.size+2,0,(struct sockaddr*)&client_addr,sizeof(client_addr));
필자는 sendto 로 전송하여 실시간 스트리밍서버를 구성하였다.



2016년 2월 18일 목요일

ffmpeg 라이브러리를 활용하여 x264 규격으로 transcoding , live streaming server 제작 (extract - decoding)


라즈베리 파이에 ffmpeg을 활용해 라이브 스트리밍 서버를 탑재하면서 배웠던 지식을 글로 남기고자 작성한다. 서버는 단순 udp서버임으로 생략하기로 하고, ffmpeg 라이브러리를 활용하여 decoding 하고 encoding 하는 과정을 소개해 보기로 한다.


환경


grep . /etc/*-release

PRETTY_NAME="Raspbian GNU/Linux 7 (wheezy)"
NAME="Raspbian GNU/Linux"
VERSION_ID="7"
VERSION="7 (wheezy)"
ID=raspbian
ID_LIKE=debian
ANSI_COLOR="1;31"
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

라즈베리 파이 2로 작업하였다. 

update 시 공개키 문제가 있어서 

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys
(or)
wget http://archive.raspberrypi.org/debian/raspberrypi.gpg.key
apt-key add raspberrypi.gpg.key
이렇게 해결하였다.

구성요소 : 라즈베리 파이2, 내부망, Usb camera
설치 라이브러리 : ffmpeg
ffmpeg 설치가이드 :
https://www.bitpi.co/2015/08/19/how-to-compile-ffmpeg-on-a-raspberry-pi/
꼭 x264 라이브러리도 함께 설치해 주길 바란다. 


  • 관련 헤더파일
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/common.h>
#include <libavutil/avutil.h>
#include <libavfilter/avfilter.h>
#include <libavutil/eval.h>
#include <libavutil/opt.h>
#include <libavutil/mem.h>
#include <libavutil/pixdesc.h>
#include <libavutil/stereo3d.h>
#include <libavutil/intreadwrite.h>
#include <libavutil/pixfmt.h>

ffmpeg 라이브러리를 사용하여 /dev/video0 이미지를 받아서 디코딩 해보자


  • 가장 기본적으로 필요한 자료구조를 선언한다
AVFormatContext   *pFormatCtx = NULL;
AVCodecContext    *pCodecCtxOrig = NULL;
AVCodec              *pCodec = NULL;
AVFrame              *pFrame = av_frame_alloc();
AVPacket               packet;
 각각이 어떤역할을 하는지 대략적으로 알아야한다. 다음 예제를 따라가며 짐작해보길 바란다.  
  • 등록을 수행한다. 
  av_register_all();
  avcodec_register_all();
  avdevice_register_all();
  • /dev/video 에서 받기위한 작업
  AVInputFormat *inputFormat = av_find_input_format("v4l2");
  AVDictionary *options = NULL;
  av_dict_set(&options,"framerate","25",0);
  int ret;
  ret = avformat_open_input(&pFormatCtx,"/dev/video0",inputFormat,&options);
  if(ret != 0)
        exit(1);
   이부분에서 ret 값이 -16이 되면서 실행이 안되는 경우가 있는데, 이는 usb camera가 이미 점유당하고 있다는 뜻이다. 꺼져있는게 분명한데 점유가 되있는 상황이 있는 경우 재량껏 해결하시길 바란다. ( dev/video0 권한을 775 로 수정하는것이 도움이 되었지만 완벽히 해결해주진 않았다. )
  • 스트림 정보를 추출
  if(avformat_find_stream_info(pFormatCtx, NULL)<0) {
    printf("Couldn't find stream information\n");
      return -1;
  }
  • 첫번째 비디오 스트림을 찾는다.
  int videoStream=-1;
  for(int i=0; i<pFormatCtx->nb_streams; i++)
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
      videoStream=i;
      break;
    }
  • 비디오 스트림에서 디코딩에 필요한 코덱 정보를 찾아낸다.
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }
  • 해당 코덱을 받아오자.
   pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1;
  }
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1;
  •  이제 디코딩을 시작하지
  while(av_read_frame(pFormatCtx, &packet)>=0 )//&& frame_count<=100 )
  {
    if(packet.stream_index==videoStream)
    {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
/... Raw image in packet 처리 ( ex. 파일에 쓴다던지 ).../ 
    }
      av_free_packet(&packet); // memory leak을 피하기 위해
      av_init_packet(&packet); 
}
 Read 할 경우 pFormatCtx 의 정보를 바탕으로 packet 에 데이터를 넣는것을 추측할 수 있다. 이때 받는 이미지는 raw 이미지가 아니고 카메라 자체에서 나름대로 인코딩된 규격을 받아온다. ( 콘솔창에서 정보를 확인할 수 있다. 카메라마다 다르기때문에 일정한 규격으로 전송하기 위해선 transcoding 이 필요하다. )
 packet 으로 읽은 데이터를 pCodecCtx 정보를 바탕으로 pFrame에 디코딩 시킨다. 이때 받아온 데이터는 raw 이미지임을 알 수 있다. 디코딩 직후 frame에 남아있는 raw image data를 확인하고 싶다면 파일에 써서 보길 바란다.

  • 빠짐없이 free 해준다.
  av_frame_free(&pFrame);
  avcodec_close(pCodecCtxOrig);
  avformat_close_input(&pFormatCtx);
  av_free(pCodecCtxOrig);

비디오 format 이해

1. 컨테이너


동영상 파일은 재생과 편집을 월활하게 하기위한 일련의 규격을 담는다. 이러한 규격을 가진 파일을 컨테이너라 칭한다. 컨테이너는 하나이상의 스티림을 지니는데, 스트림은 시간에따라 변하는 비디오 혹은 오디오 데이터를 의미한다. 컨테이너에 스트림을 담는 과정을 멀티플랙싱(Muxing) 이라 하고, 반대로 추출하는 과정을 디멀티플랙싱(Demuxing) 이라 한다.

 컨테이너에 속한 정보

  • 메타 정보 (촬영날짜, 위치)
  • 가지고있는 스트림 갯수
  • 동영상 전체 길이
  • 자막 정보
  • 실시간 스트리밍을 위한 스트림 위치정보
ex ) MPEG-PS, MPEG-TS, MPEG-4 Part 14, QuickTime, 3GPP, Ogg, WebM ..

Container format

2. 코덱(codec)


아날로그 신호로 이루어진 비디오, 오디오 를 압축된 부호로 변환하기 위한 압축 규격

압축 : analogue-> digital :인코딩 (encoding)
복원 : digital -> analogue : 디코딩 (decoding)
재압축 : digital -> analogue -> digital : 트랜스코딩 (transcoding : format 변경을 위함)

ex ) MPEG-1 Part 2, MPEG-2 Part 2, MPEG-4, Divx, MPEG-4 part 10 (H.264/AVC)..

3. 비디오 압축


비디오는 수많은 프레임이 시간축을 기준으로 모여 저장된 것이다. 이러한 프레임을 하나하나 개별적으로 압축한다면 용량낭비가 심할 것 이다. 따라서 비디오를 압축할 경우 하나의 프레임과 주변프레임의 상관관계를 이용하여 압축한다.

  • I-frame : 기준이 되는 프레임 : 하나의 온전한 이미지를 저장한다. 따라서 디코딩할 때 다른 프레임이 필요하지 않다. ( 디코딩 시간 절약 )
  • P-frame : I-frame 이후 다음 I-frame 까지의 변경된 전보만을 담는다. 비교적 적은 데이터를 저장한다. 그러나 P-frame을 디코딩하려면 연결된 I-frame이 필요하다.
  • B-frame : 다음 P 또는 I - frame에 변경된 정보만을 담고있다. 가장적은 데이터를 지니고 있으나, 디코딩 할 때 가장 많은 정보가 필요하며 디코딩 시 부하가 크다.
  • GOP(group of picture) : I-frame 과 I-frame 사이의 frame들의 수. 비디오 프레임은 GOP단위로 압축된다. 


4. 비트레이트 ( Bitrate )


멀티미디어를 코덱을 통해 인코딩 할 때는 비트레이트를 할당하게 된다. 비트레이트는 특정한 시간단위 (초) 마다 처리할 수 있는 비트의 수를 나타낸다. 인코딩 시 비트레이트를 어떻게 할당하냐에 따라 품질이 크게 달라진다.

작은해상도와 낮은 셈플링 레이트를 가진 영상을 인코딩할 떄는 많은 비트레이트가 필요하지 않다. 눈에띄는 변화가 없으며 용량만 잡아먹게 된다.

반대로 큰 해상도와 높은 샘플링 레이트를 가진 영상에 낮은 비트레이트를 할당하여 인코딩하면 화면이 뭉개지고 잡음이 샘해진다.


  • 가변 비트레이트 ( VBR, Variable Bitrate ) : 영상의 복잡도에 따라 할당하는 비트레이트의 양이 결정된다. 움직임이 많은 구간에선 압축률이 낮아짐으로 높은 비트레이트를 할당한다. 영상의 복잡도는 인코딩 중 실시간으로 판단가능하기 때문에 인코딩시간이 구배정도 더 걸릴 수 있다.
  • 고정 비트레이트 ( CBR, Constant Bitrate ) : 항상 같은양의 비트레이트를 할당한다. 품질은 상대적으로 떨어지나 일정한 인코딩 시간과, 실시간 스트리밍에 필요한 최소 대역폭을 알 수 있다.
  • 평균 비트레이트 ( ABR, Average Bitrate ) : 고정과 가변 비트레이트의 장점을 지닌다. 영상 복잡도에 따라 비트레이트를 할당하지만 평균적으로 지정된 비트레이트를 유지하려 한다. 스트리밍에도 무리가 없으며 비교적 높은 품질을 보장한다.