FFmpeg錄製MP4

例如錄製手機端URL投屏之類應用的音視頻到本地

ffmpeg-3.4編譯出lib庫和頭文件
配置文件可以是這樣 config.sh

#!/bin/bash

export PREFIX=./../ffmpeg

./configure \
    --disable-yasm \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-ffserver \
    --disable-debug \
    --disable-zlib \
    --disable-bzlib \
    --disable-static \
    --disable-stripping \
    --enable-ffmpeg \
    --enable-shared \
    --enable-gpl \
    --enable-small \
    --target-os=linux \
    --arch=arm \
    --enable-cross-compile \
    --cross-prefix=arm-linux- \
    --cc=arm-linux-gcc \
    --prefix=$PREFIX \
    --enable-encoders \
    --enable-decoders \
    --enable-muxers \
    --enable-demuxers \
    --enable-parsers \
    --enable-bsfs \
    --enable-protocols \
    --disable-filters \
    --disable-avfilter \
    --disable-swscale \
    --disable-swresample \
    --disable-devices \
    --disable-postproc \
    --enable-network \
    --enable-indev=v4l2

ffrecord.cpp 供參考

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <stdint.h>
#include <string>

extern "C" {
    #include <libavdevice/avdevice.h>
    #include <libavformat/avformat.h>
    #include <libavutil/time.h>
}

class FFRecordMP4 {
public:
    FFRecordMP4()
        :
        mStarting(false),
        mInputAvFmtCtx(NULL),
        mOutputAvFmtCtx(NULL),
        mH264bsfc(NULL),
        mAACbsfc(NULL),
        mVIdx(-1),
        mAIdx(-1),
        mWidth(0),
        mHeight(0),
        mFps(0),
        mSampleRate(0),
        mVideoFramesNum(0),
        mInputUrl(""),
        mOutputFile("") {}

    ~FFRecordMP4();

    bool openInputStream(const char *url);
    void closeInputStream(void);
    bool openOutputStream(const char *file);
    void closeOutputStream(void);
    bool getInputVideoInfo(void);
    bool getInputAudioInfo(void);
    void setInputUrl(const char *url) { mInputUrl = url; }
    void setOutputFile(const char *file) { mOutputFile = file; }
    void setStarted(bool status) { mStarting = status; }
    std::string getInputUrl(void) { return mInputUrl; }
    std::string getOutputFile(void) { return mOutputFile; }
    bool getStarted(void) { return mStarting; }
    static void *threadRun(void *data);
    void threadLoop(void);

private:
    bool mStarting;
    AVFormatContext *mInputAvFmtCtx;
    AVFormatContext *mOutputAvFmtCtx;
    AVBitStreamFilterContext *mH264bsfc;
    AVBitStreamFilterContext *mAACbsfc;
    AVPacket mPacket;
    int mVIdx;
    int mAIdx;
    int mWidth;
    int mHeight;
    double mFps;
    int mSampleRate;
    int mVideoFramesNum;
    std::string mInputUrl;
    std::string mOutputFile;
};

FFRecordMP4::~FFRecordMP4() {
    closeInputStream();
    closeOutputStream();
    mInputUrl = "";
    mOutputFile = "";
}

bool FFRecordMP4::openInputStream(const char *url) {
    if(url == NULL) {
        printf("input stream url == NULL!\n");
        return false;
    }

    if(mInputAvFmtCtx) {
        printf("already open input stream!\n");
        return false;
    }

    mAACbsfc = av_bitstream_filter_init("aac_adtstoasc");
    if (NULL == mAACbsfc) {
        printf("can not create aac_adtstoasc filter!\n");
        return false;
    }

    // h264有兩種封裝, 一種是annexb模式, 是傳統模式, 有startcode(0x00000001), SPS和PPS是在ES中
    // 另一種是AVCC模式, 一般用mp4、mkv、flv容器封裝, 沒有startcode, SPS和PPS以及其它信息被封裝在container中
    // 很多解碼器只支持annexb這種模式, 因此需要將mp4模式轉換成annexb模式
    // ffmpeg讀取mp4中的H264數據, 並不能直接得到NALU
    // 因此ffmpeg中提供了一個流過濾器"h264_mp4toannexb"可以完成這項工作
    // 流過濾器"h264_mp4toannexb", 在av_register_all()函數中會被註冊
    mH264bsfc = av_bitstream_filter_init("h264_mp4toannexb");
    if (NULL == mH264bsfc) {
        printf("can not create h264_mp4toannexb filter!\n");
        return false;
    }

    // register_container_codec, register all codecs, demux and protocols
    avcodec_register_all();
    av_register_all();
    avformat_network_init();

    mInputAvFmtCtx = avformat_alloc_context();
    mInputAvFmtCtx->flags |= AVFMT_FLAG_NONBLOCK;

    // open stream
    if (avformat_open_input(&mInputAvFmtCtx, url, NULL, NULL) != 0) {
        printf("avformat_open_input failed! [path]=[%s]\n", url);
        return false;
    }

    // parse stream info
    if (avformat_find_stream_info(mInputAvFmtCtx, NULL) < 0) {
        printf("avformat_find_stream_info failed!\n");
        return false;
    }

    // find video stream
    mVIdx = av_find_best_stream(mInputAvFmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (mVIdx < 0) {
        printf("av_find_best_stream failed! (videoIdx %d)\n", mVIdx);
        return false;
    }

    // find audio stream
    mAIdx = av_find_best_stream(mInputAvFmtCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (mAIdx < 0) {
        printf("av_find_best_stream failed! (audioIdx %d)\n", mAIdx);
        mAIdx = -1;
    }

    printf("av_find_best_stream (v %d, a %d)\n", mVIdx, mAIdx);

    av_dump_format(mInputAvFmtCtx, 0, url, 0);
    for (int i = 0; i < mInputAvFmtCtx->nb_streams; i++)
    {
        AVStream *in_stream = mInputAvFmtCtx->streams[i];

        printf("codec id: %d, URL: %s\n", in_stream->codec->codec_id, url);

        if (in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            mVIdx = i;

            mWidth = in_stream->codec->width;
            mHeight = in_stream->codec->height;

            if((in_stream->avg_frame_rate.den != 0) && (in_stream->avg_frame_rate.num != 0))
            {
                AVRational avgFps = in_stream->avg_frame_rate;
                mFps = av_q2d(avgFps);
            }

            printf("video stream index: %d, width: %d, height: %d, FrameRate: %.2f\n", mVIdx, mWidth, mHeight, mFps);
        }
        else if (in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            mAIdx = i;
            printf("audio stream index: %d\n", mAIdx);
        }
    }

    mVideoFramesNum = 0;
    av_init_packet(&mPacket);

    mInputUrl = url;
    return true;
}

void FFRecordMP4::closeInputStream(void) {
    if (NULL != mInputAvFmtCtx) {
        // release resource
        avformat_close_input(&mInputAvFmtCtx);
        mInputAvFmtCtx = NULL;
    }

    if (NULL != mH264bsfc) {
        av_bitstream_filter_close(mH264bsfc);
        mH264bsfc = NULL;
    }

    if (NULL != mAACbsfc) {
        av_bitstream_filter_close(mAACbsfc);
        mAACbsfc = NULL;
    }
}

// 文件名必須是以.MP4爲後綴, 程序會將收到的視頻(H264)和音頻(AAC)封裝到目標文件容器(MP4)裏面
bool FFRecordMP4::openOutputStream(const char *file) {
    int ret = 0;
    char tmpBuf[AV_ERROR_MAX_STRING_SIZE] = {0};

    if(file == NULL) {
        printf("output stream file == NULL!\n");
        return false;
    }

    if(mOutputAvFmtCtx) {
        printf("already open output stream!\n");
        return false;
    }

    // 爲輸出視頻分配媒體文件句柄
    avformat_alloc_output_context2(&mOutputAvFmtCtx, NULL, "mp4", file);
    if(mOutputAvFmtCtx == NULL) {
        printf("can not alloc output context\n");
        return false;
    }

    AVOutputFormat *outputFmt = mOutputAvFmtCtx->oformat;

    for (int i = 0; i < mInputAvFmtCtx->nb_streams; i++)
    {
        AVStream *in_stream = mInputAvFmtCtx->streams[i];

        AVStream *out_stream = avformat_new_stream(mOutputAvFmtCtx, in_stream->codec->codec);
        if (out_stream == NULL)
        {
            printf("can not new output stream\n");
            return false;
        }

        ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
        if (ret < 0)
        {
            memset(tmpBuf, 0, sizeof(tmpBuf));
            printf("can not copy context, url:%s, error msg:%s\n", mInputUrl.c_str(), av_make_error_string(tmpBuf, sizeof(tmpBuf), ret));
            return false;
        }
        if (outputFmt->flags & AVFMT_GLOBALHEADER)
        {
            out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
        }
    }

    av_dump_format(mOutputAvFmtCtx, 0, file, 1);
    if (!(outputFmt->flags & AVFMT_NOFILE))
    {
        ret = avio_open(&mOutputAvFmtCtx->pb, file, AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            memset(tmpBuf, 0, sizeof(tmpBuf));
            printf("can not open output io, file:%s, error msg:%s\n", file, av_make_error_string(tmpBuf, sizeof(tmpBuf), ret));
            return false;
        }
    }

    // 爲媒體文件句柄寫入頭信息
    ret = avformat_write_header(mOutputAvFmtCtx, NULL);
    if (ret < 0)
    {
        memset(tmpBuf, 0, sizeof(tmpBuf));
        printf("can not write outputstream header, file:%s, error msg:%s\n", file, av_make_error_string(tmpBuf, sizeof(tmpBuf), ret));
        return false;
    }

    mOutputFile = file;
    return true;
}

void FFRecordMP4::closeOutputStream(void) {
    if (mOutputAvFmtCtx)
    {
        // 寫入文件尾
        av_write_trailer(mOutputAvFmtCtx);

        if (!(mOutputAvFmtCtx->oformat->flags & AVFMT_NOFILE))
        {
            if(mOutputAvFmtCtx->pb)
            {
                avio_close(mOutputAvFmtCtx->pb);
            }
        }

        avformat_free_context(mOutputAvFmtCtx);
        mOutputAvFmtCtx = NULL;
    }
}

bool FFRecordMP4::getInputVideoInfo(void) {
    if (NULL == mInputAvFmtCtx) {
        printf("%s failed (mAvFmtCtx is null)\n", __func__);
        return false;
    }
    if (mVIdx < 0) {
        printf("%s failed (no video stream)\n", __func__);
        return false;
    }

    AVStream *vStream = mInputAvFmtCtx->streams[mVIdx];
    AVCodecParameters *vCodecPar = vStream->codecpar;
    if (NULL == vCodecPar) {
        printf("%s failed (vCodecPar is null)\n", __func__);
        return false;
    }

    if (AV_CODEC_ID_MJPEG == vCodecPar->codec_id) {
        printf("video codec is MJPEG\n");
    } else if (AV_CODEC_ID_H264 == vCodecPar->codec_id) {
        printf("video codec is H264\n");
    } else if (AV_CODEC_ID_HEVC == vCodecPar->codec_id) {
        printf("video codec is H265\n");
    } else {
        printf("video codec is 0x%x\n", vCodecPar->codec_id);
    }

    mWidth = vCodecPar->width;
    mHeight = vCodecPar->height;

    AVRational avgFps = vStream->avg_frame_rate;
    mFps = av_q2d(avgFps);
    printf("%s (w %d, h %d, fps %lf)\n", __func__, mWidth, mHeight, mFps);

    AVRational tb = vStream->time_base;
    printf("%s (time base:%lf, start time:%lld)\n", __func__, av_q2d(tb), vStream->start_time);

    return true;
}

bool FFRecordMP4::getInputAudioInfo(void) {
    if (NULL == mInputAvFmtCtx) {
        printf("%s failed (mAvFmtCtx is null)\n", __func__);
        return false;
    }
    if (mAIdx < 0) {
        printf("%s failed (no audio stream)\n", __func__);
        return false;
    }

    AVStream *aStream = mInputAvFmtCtx->streams[mAIdx];
    AVCodecParameters *aCodecPar = aStream->codecpar;
    if (NULL == aCodecPar) {
        printf("%s failed (aCodecPar is null)\n", __func__);
        return false;
    }

    if ((aCodecPar->codec_id >= AV_CODEC_ID_FIRST_AUDIO)
        && (aCodecPar->codec_id <= AV_CODEC_ID_PCM_F24LE)) {
        printf("audio codec is PCM\n");
    } else if (AV_CODEC_ID_MP3 == aCodecPar->codec_id) {
        printf("audio codec is MP3\n");
    } else if (AV_CODEC_ID_AAC == aCodecPar->codec_id) {
        printf("audio codec is AAC\n");
    } else {
        printf("audio codec is 0x%x\n", aCodecPar->codec_id);
    }

    mSampleRate = aCodecPar->sample_rate;
    printf("%s audio sample rate:%d\n", __func__, mSampleRate);

    return true;
}

void *FFRecordMP4::threadRun(void *data) {
    if (data) {
        FFRecordMP4 *record = (FFRecordMP4 *)data;
        printf("create thread FFRecordMP4\n");
        while (record->getStarted()) {
            record->threadLoop();
        }
    }
    return NULL;
}

// 不停地調用av_read_frame接收數據包, 數據包類型分視頻和音頻, 如果av_read_frame返回-1表示斷開連接或流結束, 要退出線程
// 對於MP4容器, 對混合進去的視頻和音頻的編碼格式是有要求的, 視頻可以是MPEG4/H264, 音頻一般是AAC
void FFRecordMP4::threadLoop(void) {
    int ret = 0;

    ret = av_read_frame(mInputAvFmtCtx, &mPacket);
    if (ret >= 0) {
        AVStream *in_stream = mInputAvFmtCtx->streams[mPacket.stream_index];
        AVStream *out_stream = mOutputAvFmtCtx->streams[mPacket.stream_index];

        // 寫文件前必須對時間戳修改一下
        // 因爲收到的數據包的時間戳基線跟FFmpeg錄製容器中的時間戳基線是不一樣的, 這個時間戳基線也叫做時鐘頻率
        mPacket.pts = av_rescale_q_rnd(mPacket.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        mPacket.dts = av_rescale_q_rnd(mPacket.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        mPacket.duration = av_rescale_q(mPacket.duration, in_stream->time_base, out_stream->time_base);
        mPacket.pos = -1;

        if((in_stream->codec->codec_type != AVMEDIA_TYPE_VIDEO) && (in_stream->codec->codec_type != AVMEDIA_TYPE_AUDIO)) {
            return;
        }

        // video case
        if (-1 != mVIdx && mPacket.stream_index == mVIdx) {
            mVideoFramesNum++;

            // ffmpeg讀到的mPacket.data中沒有0x00000001的分隔符
            // 需要 av_bitstream_filter_filter 來轉換一下
            av_bitstream_filter_filter(mH264bsfc, mInputAvFmtCtx->streams[mVIdx]->codec, NULL, &mPacket.data, &mPacket.size, mPacket.data, mPacket.size, 0);

            //printf("Frame time: %lf", ((mPacket.pts) * av_q2d(in_stream->time_base)));

            // write the compressed frame to the output format
            int nError = av_interleaved_write_frame(mOutputAvFmtCtx, &mPacket);
            if (nError != 0)
            {
                char tmpBuf[AV_ERROR_MAX_STRING_SIZE] = {0};
                printf("While writing video frame, %s\n", av_make_error_string(tmpBuf, sizeof(tmpBuf), nError));
            }
        }

        // audio case
        if (-1 != mAIdx && mPacket.stream_index == mAIdx) {
            // write the compressed frame to the output format
            int nError = av_interleaved_write_frame(mOutputAvFmtCtx, &mPacket);
            if (nError != 0)
            {
                char tmpBuf[AV_ERROR_MAX_STRING_SIZE] = {0};
                printf("While writing audio frame, %s\n", av_make_error_string(tmpBuf, sizeof(tmpBuf), nError));
            }
        }

        // av_read_frame would allocate memory on mPacket.data
        av_packet_unref(&mPacket);
        //av_usleep(20000); // 20ms
    } else {
        av_usleep(10000); // 10ms
        if(ret == AVERROR_EOF) {
            printf("END OF FILE, mVideoFramesNum = %d\n", mVideoFramesNum);
            mVideoFramesNum = 0;

        } else {
            printf("av_read_frame() got error:%d\n", ret);
        }
        setStarted(false);
    }
}

int main(int argc, char *argv[])
{
    bool ret = false;
    pthread_t mThread;

    FFRecordMP4 *pRecord = new FFRecordMP4();
    ret = pRecord->openInputStream("http://192.168.199.160:7001/1/dd54fd96-8396-5faa-9cd4-0338ec1373e0.mp4");
    if(!ret) {
        return 1;
    }
    ret = pRecord->openOutputStream("/nfsroot/record.mp4");
    if(!ret) {
        return 1;
    }

    pRecord->getInputVideoInfo();
    pRecord->getInputAudioInfo();
    pRecord->setStarted(true);

    if (pthread_create(&mThread, NULL, pRecord->threadRun, pRecord) != 0) {
        printf("createThread thread failed!\n");
        return -1;
    }

    sleep(1);

    void *joinRes;
    pthread_join(mThread, &joinRes);

    pRecord->closeInputStream();
    pRecord->closeOutputStream();

    return 0;
}

編譯
arm-linux-g++ ffrecord.cpp -I/ffmpeg/include -L/ffmpeg/lib -lpthread -lavdevice -lavformat -lavcodec -lavutil

運行

av_find_best_stream (v 0, a 1)
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'http://192.168.199.160:7001/1/dd54fd96-8396-5faa-9cd4-0338ec1373e0.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41letv
    encoder         : Lavf56.15.102
  Duration: 00:03:56.70, start: 0.000000, bitrate: 3153 kb/s
    Stream #0:0(und): Video: h264 (avc1 / 0x31637661), yuv420p, 1920x1072 [SAR 134:135 DAR 16:9], 3014 kb/s, 25 fps, 25 tbr, 12800 tbn, 50 tbc (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
codec id: 28, URL: http://192.168.199.160:7001/1/dd54fd96-8396-5faa-9cd4-0338ec1373e0.mp4
video stream index: 0, width: 1920, height: 1072, FrameRate: 25.00
codec id: 86018, URL: http://192.168.199.160:7001/1/dd54fd96-8396-5faa-9cd4-0338ec1373e0.mp4
audio stream index: 1
Output #0, mp4, to '/nfsroot/record.mp4':
    Stream #0:0: Unknown: none
    Stream #0:1: Unknown: none
[mp4 @ 0x871f50] Using AVStream.codec.time_base as a timebase hint to the muxer is deprecated. Set AVStream.time_base instead.
[mp4 @ 0x871f50] Using AVStream.codec to pass codec parameters to muxers is deprecated, use AVStream.codecpar instead.
[mp4 @ 0x871f50] Using AVStream.codec.time_base as a timebase hint to the muxer is deprecated. Set AVStream.time_base instead.
[mp4 @ 0x871f50] Using AVStream.codec to pass codec parameters to muxers is deprecated, use AVStream.codecpar instead.
video codec is H264
getInputVideoInfo (w 1920, h 1072, fps 25.000000)
getInputVideoInfo (time base:0.000078, start time:0)
audio codec is AAC
getInputAudioInfo audio sample rate:44100
create thread FFRecordMP4
END OF FILE, mVideoFramesNum = 5915

參考
https://blog.csdn.net/toshiba689/article/details/79426680

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章