Qt/C++音視頻開發53-本地攝像頭推流/桌面推流/文件推流/監控推流等

一、前言

編寫這個推流程序,最開始設計的時候是用視頻文件推流,後面陸續增加了監控攝像頭推流(其實就是rtsp視頻流)、網絡電臺和視頻推流(一般是rtmp或者http開頭m3u8結尾的視頻流)、本地攝像頭推流(本地USB攝像頭或者筆記本自帶攝像頭等)、桌面推流(將當前運行環境的系統桌面抓拍推流)。按照分類的話其實就是三大類,第一類就是視頻文件(包括本地視頻文件和網絡視頻文件,就是帶了文件時長的),第二類就是各種實時視頻流(包括監控攝像頭rtsp,網絡電臺rtmp,網絡實時視頻m3u8等),第三類就是本地設備採集(包括本地攝像頭採集和本地電腦桌面採集),每一種類別都可以用對應的通用的代碼去處理,基本上就是在打開階段有所區別,後面採集和解碼並推流階段,代碼是一模一樣的。

二、效果圖






三、體驗地址

  1. 國內站點:https://gitee.com/feiyangqingyun
  2. 國際站點:https://github.com/feiyangqingyun
  3. 個人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 體驗地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取碼:01jf 文件名:bin_video_push。

四、功能特點

  1. 支持各種本地視頻文件和網絡視頻文件。
  2. 支持各種網絡視頻流,網絡攝像頭,協議包括rtsp、rtmp、http。
  3. 支持將本地攝像頭設備推流,可指定分辨率和幀率等。
  4. 支持將本地桌面推流,可指定屏幕區域和幀率等。
  5. 自動啓動流媒體服務程序,默認mediamtx(原rtsp-simple-server),可選用srs、EasyDarwin、LiveQing、ZLMediaKit等。
  6. 可實時切換預覽視頻文件,可切換視頻文件播放進度,切換到哪裏就推流到哪裏。
  7. 推流的清晰度和質量可調。
  8. 可動態添加文件、目錄、地址。
  9. 視頻文件自動循環推流,如果視頻源是視頻流,在掉線後會自動重連。
  10. 網絡視頻流自動重連,重連成功自動繼續推流。
  11. 網絡視頻流實時性極高,延遲極低,延遲時間大概在100ms左右。
  12. 極低CPU佔用,4路主碼流推流只需要佔用0.2%CPU。理論上常規普通PC機器推100路毫無壓力,主要性能瓶頸在網絡。
  13. 推流可選推流到rtsp/rtmp兩種,推流後的數據支持直接rtsp/rtmp/hls/webrtc四種方式訪問,可以直接瀏覽器打開看實時畫面。
  14. 可以推流到外網服務器,然後通過手機、電腦、平板等設備播放對應的視頻流。
  15. 每個推流都可以手動指定唯一標識符(方便拉流/用戶無需記憶複雜的地址),沒有指定則按照策略隨機生成hash值。
  16. 自動生成測試網頁直接打開播放,可以看到實時效果,自動按照數量對應宮格顯示。
  17. 推流過程中可以在表格中切換對應推流項,實時預覽正在推流的視頻,並可以切換視頻文件的播放進度。
  18. 音視頻同步推流,符合264/265/aac格式的自動原數據推流,不符合的自動轉碼再推流(會佔用一定CPU)。
  19. 轉碼策略支持三種,自動處理(符合要求的原數據/不符合的轉碼),僅限文件(文件類型的轉碼視頻),所有轉碼。
  20. 表格中實時顯示每一路推流的分辨率和音視頻數據狀態,灰色表示沒有輸入流,黑色表示沒有輸出流,綠色表示原數據推流,紅色表示轉碼後的數據推流。
  21. 自動重連視頻源,自動重連流媒體服務器,保證啓動後,推流地址和打開地址都實時重連,只要恢復後立即連上繼續採集和推流。
  22. 提供循環推流示例,一個視頻源同時推流到多個流媒體服務器,比如打開一個視頻同時推流到抖音/快手/B站等,可以作爲錄播推流,列表循環,非常方便實用。
  23. 根據不同的流媒體服務器類型,自動生成對應的rtsp/rtmp/hls/flv/ws-flv/webrtc地址,用戶可以直接複製該地址到播放器或者網頁中預覽查看。
  24. 編碼視頻格式可以選擇自動處理(源頭是264就264/源頭是265就265),轉H264(強制轉264),轉H265(強制轉265)。
  25. 支持Qt4/Qt5/Qt6任意版本,支持任意系統(windows/linux/macos/android/嵌入式linux等)。

五、相關代碼

bool FFmpegSave::initVideoCtx()
{
    //沒啓用視頻編碼或者不需要視頻則不繼續
    if (!encodeVideo || !needVideo) {
        return true;
    }

    //查找視頻編碼器(自動處理的話如果源頭是H265則採用HEVC作爲編碼器)
    AVCodecx *videoCodec;
    if (videoFormat == 0) {
        AVCodecID codecID = FFmpegHelper::getCodecId(videoStreamIn);
        if (codecID == AV_CODEC_ID_HEVC) {
            videoCodec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
        } else {
            videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
        }
    } else if (videoFormat == 1) {
        videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    } else if (videoFormat == 2) {
        videoCodec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
    }

    //RTMP流媒體目前只支持H264
    if (fileName.startsWith("rtmp://")) {
        videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    }

    if (!videoCodec) {
        debug(0, "視頻編碼", "avcodec_find_encoder");
        return false;
    }

    //創建視頻編碼器上下文
    videoCodecCtx = avcodec_alloc_context3(videoCodec);
    if (!videoCodecCtx) {
        debug(0, "視頻編碼", "avcodec_alloc_context3");
        return false;
    }

    //AVCodecContext結構體參數: https://blog.csdn.net/weixin_44517656/article/details/109707539
    //放大係數是爲了小數位能夠正確放大到整型
    int ratio = 1000000;
    videoCodecCtx->time_base.num = 1 * ratio;
    videoCodecCtx->time_base.den = frameRate * ratio;
    videoCodecCtx->framerate.num = frameRate * ratio;
    videoCodecCtx->framerate.den = 1 * ratio;

    //下面這種方式對編譯器有版本要求(c++11)
    //videoCodecCtx->time_base = {1, frameRate};
    //videoCodecCtx->framerate = {frameRate, 1};

    //參數說明 https://blog.csdn.net/qq_40179458/article/details/110449653
    //大分辨率需要加上下面幾個參數設置(否則在32位的庫不能正常編碼提示 Generic error in an external library)
    if ((videoWidth >= 3840 || videoHeight >= 2160)) {
        videoCodecCtx->qmin = 10;
        videoCodecCtx->qmax = 51;
        videoCodecCtx->me_range = 16;
        videoCodecCtx->max_qdiff = 4;
        videoCodecCtx->qcompress = 0.6;
    }

    //需要轉換尺寸的啓用目標尺寸
    int width = videoWidth;
    int height = videoHeight;
    if (encodeVideoScale != "1") {
        QStringList sizes = WidgetHelper::getSizes(encodeVideoScale);
        if (sizes.count() == 2) {
            width = sizes.at(0).toInt();
            height = sizes.at(1).toInt();
        } else {
            float scale = encodeVideoScale.toFloat();
            width = videoWidth * scale;
            height = videoHeight * scale;
        }
    }

    //初始化視頻編碼器參數(如果要文件體積小一些畫質差一些可以降低碼率)
    videoCodecCtx->bit_rate = FFmpegHelper::getBitRate(width, height) * encodeVideoRatio;
    videoCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    videoCodecCtx->width = width;
    videoCodecCtx->height = height;
    videoCodecCtx->level = 50;
    //多少幀一個I幀(關鍵幀)
    videoCodecCtx->gop_size = frameRate;
    //去掉B幀只留下I幀和P幀
    videoCodecCtx->max_b_frames = 0;
    //videoCodecCtx->bit_rate_tolerance = 1;
    videoCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    videoCodecCtx->profile = FF_PROFILE_H264_MAIN;
    if (saveVideoType == SaveVideoType_Mp4) {
        videoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
        //videoCodecCtx->flags |= (AV_CODEC_FLAG_GLOBAL_HEADER | AV_CODEC_FLAG_LOW_DELAY);
    }

    //加速選項 https://www.jianshu.com/p/034f5b3e7f94
    //加載預設 https://blog.csdn.net/JineD/article/details/125304570
    //速度選項 ultrafast/superfast/veryfast/faster/fast/medium/slow/slower/veryslow/placebo
    //視覺優化 film/animation/grain/stillimage/psnr/ssim/fastdecode/zerolatency

    //設置零延遲(本地採集設備視頻流保存如果不設置則播放的時候會越來越模糊)
    //測試發現有些文件需要開啓纔不會慢慢變模糊/有些開啓後在部分系統環境會偶爾卡頓(webrtc下)/根據實際需求決定是否開啓
    av_opt_set(videoCodecCtx->priv_data, "tune", "zerolatency", 0);

    //文件類型除外(保證文件的清晰度)
    if (videoType > 2) {
        av_opt_set(videoCodecCtx->priv_data, "preset", "ultrafast", 0);
        //av_opt_set(videoCodecCtx->priv_data, "x265-params", "qp=20", 0);
    }

    //打開視頻編碼器
    int result = avcodec_open2(videoCodecCtx, videoCodec, NULL);
    if (result < 0) {
        debug(result, "視頻編碼", "avcodec_open2");
        return false;
    }

    //創建編碼用臨時包
    videoPacket = FFmpegHelper::creatPacket(NULL);

    //設置了視頻縮放則轉換
    if (encodeVideoScale != "1") {
        videoFrame = av_frame_alloc();
        videoFrame->format = AV_PIX_FMT_YUV420P;
        videoFrame->width = width;
        videoFrame->height = height;

        int align = 1;
        int flags = SWS_BICUBIC;
        AVPixelFormat format = AV_PIX_FMT_YUV420P;
        int videoSize = av_image_get_buffer_size(format, width, height, align);
        videoData = (quint8 *)av_malloc(videoSize * sizeof(quint8));
        av_image_fill_arrays(videoFrame->data, videoFrame->linesize, videoData, format, width, height, align);
        videoSwsCtx = sws_getContext(videoWidth, videoHeight, format, width, height, format, flags, NULL, NULL, NULL);
    }

    debug(0, "視頻編碼", "初始化完成");
    return true;
}

//https://blog.csdn.net/irainsa/article/details/129289254
bool FFmpegSave::initAudioCtx()
{
    //沒啓用音頻編碼或者不需要音頻則不繼續
    if (!encodeAudio || !needAudio) {
        return true;
    }

    AVCodecx *audioCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    if (!audioCodec) {
        debug(0, "音頻編碼", "avcodec_find_encoder");
        return false;
    }

    //創建音頻編碼器上下文
    audioCodecCtx = avcodec_alloc_context3(audioCodec);
    if (!audioCodecCtx) {
        debug(0, "音頻編碼", "avcodec_alloc_context3");
        return false;
    }

    //初始化音頻編碼器參數
    audioCodecCtx->bit_rate = FFmpegHelper::getBitRate(audioStreamIn);
    audioCodecCtx->codec_type = AVMEDIA_TYPE_AUDIO;
    audioCodecCtx->sample_rate = sampleRate;
    audioCodecCtx->channel_layout = AV_CH_LAYOUT_STEREO;
    audioCodecCtx->channels = channelCount;
    //audioCodecCtx->profile = FF_PROFILE_AAC_MAIN;
    audioCodecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP;
    audioCodecCtx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
    if (saveVideoType == SaveVideoType_Mp4) {
        audioCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    //打開音頻編碼器
    int result = avcodec_open2(audioCodecCtx, audioCodec, NULL);
    if (result < 0) {
        debug(result, "音頻編碼", "avcodec_open2");
        return false;
    }

    //創建編碼用臨時包
    audioPacket = FFmpegHelper::creatPacket(NULL);
    debug(0, "音頻編碼", "初始化完成");
    return true;
}

bool FFmpegSave::initStream()
{
    AVDictionary *options = NULL;
    QByteArray fileData = fileName.toUtf8();
    const char *url = fileData.data();

    //既可以是保存到文件也可以是推流(對應格式要區分)
    const char *format = "mp4";
    if (videoIndexIn < 0 && audioCodecName == "mp3") {
        format = "mp3";
    }
    if (fileName.startsWith("rtmp://")) {
        format = "flv";
    } else if (fileName.startsWith("rtsp://")) {
        format = "rtsp";
        av_dict_set(&options, "stimeout", "3000000", 0);
        av_dict_set(&options, "rtsp_transport", "tcp", 0);
    }

    //如果存在祕鑰則啓用加密
    QByteArray cryptoKey = this->property("cryptoKey").toByteArray();
    if (!cryptoKey.isEmpty()) {
        av_dict_set(&options, "encryption_scheme", "cenc-aes-ctr", 0);
        av_dict_set(&options, "encryption_key", cryptoKey.constData(), 0);
        av_dict_set(&options, "encryption_kid", cryptoKey.constData(), 0);
    }

    //開闢一個格式上下文用來處理視頻流輸出(末尾url不填則rtsp推流失敗)
    int result = avformat_alloc_output_context2(&formatCtx, NULL, format, url);
    if (result < 0) {
        debug(result, "創建格式", "");
        return false;
    }

    //創建輸出視頻流
    if (!this->initVideoStream()) {
        goto end;
    }

    //創建輸出音頻流
    if (!this->initAudioStream()) {
        goto end;
    }

    //打開輸出文件
    if (!(formatCtx->oformat->flags & AVFMT_NOFILE)) {
        result = avio_open(&formatCtx->pb, url, AVIO_FLAG_WRITE);
        if (result < 0) {
            debug(result, "打開輸出", "");
            goto end;
        }
    }

    //寫入文件開始符
    result = avformat_write_header(formatCtx, &options);
    if (result < 0) {
        debug(result, "寫文件頭", "");
        goto end;
    }

    return true;

end:
    //關閉釋放並清理文件
    this->close();
    this->deleteFile(fileName);
    return false;
}

bool FFmpegSave::initVideoStream()
{
    if (needVideo) {
        videoIndexOut = 0;
        AVStream *stream = avformat_new_stream(formatCtx, NULL);
        if (!stream) {
            return false;
        }

        //設置旋轉角度(沒有編碼的數據是源頭帶有旋轉角度的/編碼後的是正常旋轉好的)
        if (!encodeVideo) {
            FFmpegHelper::setRotate(stream, rotate);
        }

        //複製解碼器上下文參數(不編碼從源頭流拷貝/編碼從設置的編碼器拷貝)
        int result = -1;
        if (encodeVideo) {
            stream->r_frame_rate = videoCodecCtx->framerate;
            result = FFmpegHelper::copyContext(videoCodecCtx, stream, true);
        } else {
            result = FFmpegHelper::copyContext(videoStreamIn, stream);
        }

        if (result < 0) {
            debug(result, "複製參數", "");
            return false;
        }
    }

    return true;
}

bool FFmpegSave::initAudioStream()
{
    if (needAudio) {
        audioIndexOut = (videoIndexOut == 0 ? 1 : 0);
        AVStream *stream = avformat_new_stream(formatCtx, NULL);
        if (!stream) {
            return false;
        }

        //複製解碼器上下文參數(不編碼從源頭流拷貝/編碼從設置的編碼器拷貝)
        int result = -1;
        if (encodeAudio) {
            result = FFmpegHelper::copyContext(audioCodecCtx, stream, true);
        } else {
            result = FFmpegHelper::copyContext(audioStreamIn, stream);
        }

        if (result < 0) {
            debug(result, "複製參數", "");
            return false;
        }
    }

    return true;
}

bool FFmpegSave::init()
{
    //必須存在輸入視音頻流對象其中一個
    if (fileName.isEmpty() || (!videoStreamIn && !audioStreamIn)) {
        return false;
    }

    //檢查推流地址是否正常
    if (saveMode != SaveMode_File && !WidgetHelper::checkUrl(fileName, 1000)) {
        debug(0, "地址不通", "");
        if (!this->isRunning()) {
            this->start();
        }
        return false;
    }

    //獲取媒體信息及檢查編碼處理
    this->getMediaInfo();
    this->checkEncode();

    //ffmpeg2不支持重新編碼的推流
#if (FFMPEG_VERSION_MAJOR < 3)
    if (saveMode != SaveMode_File && (encodeVideo || encodeAudio)) {
        return false;
    }
#endif

    //初始化對應視音頻編碼器
    if (!this->initVideoCtx()) {
        return false;
    }
    if (!this->initAudioCtx()) {
        return false;
    }

    //保存264數據直接寫文件
    if (saveVideoType == SaveVideoType_H264) {
        return true;
    }

    //初始化視音頻流
    if (!this->initStream()) {
        return false;
    }

    debug(0, "索引信息", QString("視頻: %1/%2 音頻: %3/%4").arg(videoIndexIn).arg(videoIndexOut).arg(audioIndexIn).arg(audioIndexOut));
    return true;
}

void FFmpegSave::save()
{
    //從隊列中取出數據處理
    //qDebug() << TIMEMS << packets.count() << frames.count();

    if (packets.count() > 0) {
        mutex.lock();
        AVPacket *packet = packets.takeFirst();
        mutex.unlock();

        this->writePacket2(packet, packet->stream_index == videoIndexIn);
        FFmpegHelper::freePacket(packet);
    }

    if (frames.count() > 0) {
        mutex.lock();
        AVFrame *frame = frames.takeFirst();
        mutex.unlock();

        if (frame->width > 0) {
            FFmpegHelper::encode(this, videoCodecCtx, videoPacket, frame, true);
        } else {
            FFmpegHelper::encode(this, audioCodecCtx, audioPacket, frame, false);
        }
        FFmpegHelper::freeFrame(frame);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章