2013년 6월 21일 금요일

ffmpeg 을 이용한 비디오 스트림 파일저장 - ffmpeg muxer file writer

http://greenday96.blogspot.kr/2015/08/ffmpeg-muxer-stream-record-by-ffmpeg.html <= 최신 포스트 참조(비디오+오디오 저장)

* ffmpeg muxer 사용 - avformat_alloc_output_context2 함수의 filename 확장자에 따라 avi/mp4/ts 등 포멧으로 저장가능


< FileWriter.h >

class FileWriter
{
public:
    FileWriter();
    virtual ~FileWriter();

    int openFile(char *filename, int codec, int width, int height);
    int writeFile(enum AVMediaType type, char *buf, int len);
    void closeFile();

protected:
    AVStream* addVideoStream(enum CodecID codec_id, int width, int height);
    int writeVideo(char *buf, int len);
    int writeAudio(char *buf, int len);

protected:
    AVFormatContext *m_pFormatContext;
    AVStream *m_pVideoStream;
    AVPacket m_avPacket;

    enum CodecID m_nCodecID;

    __int64 m_nFrameCount;
};


< FileWriter.cpp >

#include "FileWriter.h"

FileWriter::FileWriter()
{
    static int init = 0;
    if (init == 0)
    {
       av_register_all();
       init = 1;
    }

    m_pFormatContext = NULL;
    m_pVideoStream = NULL;
}

FileWriter::~FileWriter()
{
    closeFile();
}

int FileWriter::openFile(char *filename, int codec, int width, int height)
{
    int err;

    m_nFrameCount = 0;
    m_nCodecID = CODEC_ID_NONE;

    err = avformat_alloc_output_context2(&m_pFormatContext, NULL, NULL, filename);
    if (!m_pFormatContext || err < 0) {
       printf("[%s] avformat_alloc_output_context2: error %d\n", __FUNCTION__, err);
       return -1;
    }

    av_init_packet(&m_avPacket);

    AVOutputFormat *fmt = m_pFormatContext->oformat;

    if (codec == 0) {
       fmt->video_codec = CODEC_ID_H264;
       m_nCodecID = CODEC_ID_H264;
    } else if (codec == 1) {
       fmt->video_codec = CODEC_ID_MPEG4;
       m_nCodecID = CODEC_ID_MPEG4;
    }

    m_pVideoStream = addVideoStream(fmt->video_codec, width, height);
    if (!m_pVideoStream) {
       printf("[%s] addVideoStream failed\n", __FUNCTION__);
       return -1;
    }

    err = avio_open(&m_pFormatContext->pb, filename, AVIO_FLAG_WRITE);
    if (err < 0) {
       printf("[%s] avio_open failed: error %d\n", __FUNCTION__, err);
       closeFile();
       return -1;
    }

#ifdef FFMPEG_1_2_1
    err = avformat_write_header(m_pFormatContext, NULL);
#else
    err = av_write_header(m_pFormatContext);
#endif

    return 0;
}

void FileWriter::closeFile()
{
        if (m_pFormatContext) {
           av_write_trailer(m_pFormatContext);
           if (m_pVideoStream) {
               if (m_pVideoStream->codec) {
     avcodec_close(m_pVideoStream->codec);
}
            }

       av_free_packet(&m_avPacket);
       avio_close(m_pFormatContext->pb);
       avformat_free_context(m_pFormatContext);
       m_pFormatContext = NULL;
    }
}

int FileWriter::writeFile(enum AVMediaType type, char *buf, int len)
{
    if (type == AVMEDIA_TYPE_VIDEO)
        return writeVideo(buf, len);
    else if (type == AVMEDIA_TYPE_AUDIO)
       return writeAudio(buf, len);

    return -1;
}

int FileWriter::writeVideo(char *buf, int len)
{
    if (!m_pFormatContext)
       return -1;

    int ret = 0;

    av_init_packet(&m_avPacket);

    m_avPacket.stream_index = m_pVideoStream->index;
    m_avPacket.data = (uint8_t *)buf;
    m_avPacket.size = len;

    if (checkKeyFrame(buf, len) == 1)
       m_avPacket.flags |= AV_PKT_FLAG_KEY;

    ret = av_interleaved_write_frame(m_pFormatContext, &m_avPacket);
    //ret = av_write_frame(m_pFormatContext, &m_avPacket);

    m_nFrameCount++;
    return ret;
}

AVStream* FileWriter::addVideoStream(enum CodecID codec_id, int width, int height)
{
    AVStream *st;
    AVCodecContext *c;

    st = av_new_stream(m_pFormatContext, 0);
    if (!st) {
       printf("[%s] Could not alloc stream\n", __FUNCTION__);
       return NULL;
    }

    c = st->codec;
    c->codec_id = codec_id;
    c->codec_type = AVMEDIA_TYPE_VIDEO;

    c->width = width;
    c->height = height;

    c->time_base.den = 30;
    c->time_base.num = 1;
    c->gop_size = 30;

    c->pix_fmt = PIX_FMT_YUV420P;

    // some formats want stream headers to be separate
    if(m_pFormatContext->oformat->flags & AVFMT_GLOBALHEADER)
       c->flags |= CODEC_FLAG_GLOBAL_HEADER;

    return st;
}

int FileWriter::writeAudio(char *buf, int len)
{
    return -1;
}

댓글 14개:

  1. checkKeyFrame()의 정의부를 잘 모르겠습니다.

    답글삭제
    답글
    1. checkKeyFrame() 함수는 key 프레임인지 여부를 판단하는 함수인데 h264/mpeg4 여부에 따라 구현이 달라지고 조금 복잡해서 뺐습니다. 키 프레임을 처리해주는 이유는 키 프레임별로 인덱스 정보를 만들기 때문입니다.

      삭제
  2. Hi,

    Does this example works OK (I did'nt find the call to writefile, closefile, just the declerations) ?
    Can it be used for streaming too ?
    What about the pts, dts ?
    Thanks!
    Ran

    답글삭제
  3. Hi,

    Does this example works OK (I did'nt find the call to writefile, closefile, just the declerations) ?
    Can it be used for streaming too ?
    What about the pts, dts ?
    Thanks!
    Ran

    답글삭제
    답글
    1. yes it works. it's not perfect source code but you can learn how to use ffmpeg muxer.
      av_interleaved_write_frame writes stream buffer to file and av_write_trailer adds indexes to the end of the file so you don't need to care about file writing functions or dts/pts.
      if you want to know more, read ffmpeg muxer example source code.

      삭제
  4. 안녕하세요. 질문이 있습니다. audio stream 추가하고 로컬 pc의 마이크 raw pcm 데이터를 함께 저장하려고 하는데 Avpacket data에 넣고 av_interleaved_write_frame() 하면 영상이 제대로 저장이 안됩니다. 시간은 완전 틀리고, 영상이 한번에 막 재생이 됩니다. 혹시 이유 좀 알 수 있을까요? 파일 포멧은 AVI 이고 비디오 포멧 h264 음성은 raw h264입니다.

    답글삭제
    답글
    1. av_interleaved_write_frame 함수는 타임스탬프에 따라 write를 하는데 타임스탬프가 틀리면 싱크가 맞지않을수 있습니다. 대신에 av_write_frame 함수를 사용하면 호출즉시 write를 하는데 입력 데이터가 싱크가 맞다면 저장 데이터도 싱크가 맞습니다. 그리고 오디오에 raw h264라는 코덱은 없습니다

      삭제
    2. 작성자가 댓글을 삭제했습니다.

      삭제
  5. 작성자가 댓글을 삭제했습니다.

    답글삭제
  6. 안녕하세요.
    혹시 테스트 가능한 코드를 공유 가능 하신지 궁금합니다.

    답글삭제
  7. 작성자가 댓글을 삭제했습니다.

    답글삭제
  8. 영상통화를 저장하려는데요. 스트림에는 sps,pps 가 있는데 sdp만을 이용해서 sps,pps정보를 얻어서 먹싱가능한가요? 아니라면 raw264로 변환을 하는 코드를 작성해서 떼어낸 순수 sps, pps를 컨텍스트 핸들러를 위해 만들기수 밖에 없는것인지 궁금하네요. 쉬운방법이 없는지.... 아 그리고 영상에서 음성은 opus코덱으로 오는데 이건 mp4 컨테이너에 안들어가는걸로 아는데 mp3로 트렌스 코딩해서 넣을면 될런지요?
    감사합니다.

    답글삭제
    답글
    1. sdp에 있는 sps,pps로 먹싱은 가능하나 sps,pps는 바뀔수 있기때문에 보통 스트림에 있는 sps,pps를 이용하는것이 좋습니다. mp4컨테이너 스펙에 없는 오디오 포멧은 스펙에 속한 포멧으로 트랜스코딩 후에 저장하면 됩니다.

      삭제
  9. raw264라면 00 00 00 01 + SPS, PPS, Iframe, Pframe 요런 형태로 가공하지 않고 rtp 를 헤더만 잘라서 avformat에 보낼수 있는 간단한 방법을 찾고있어요. 왜냐면 비트헤더 파싱이 아시겠지만 만만치 않는 작업이라서요.

    답글삭제