레이블이 live stream인 게시물을 표시합니다. 모든 게시물 표시
레이블이 live stream인 게시물을 표시합니다. 모든 게시물 표시

2016년 2월 21일 일요일

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);