Qt/C++ オーディオおよびビデオ開発 53 - ローカル カメラ プッシュ/デスクトップ プッシュ/ファイル プッシュ/モニタリング プッシュなど。

I.はじめに

このストリーミング プログラムを作成したとき、最初の設計はビデオ ファイルを使用してストリームをプッシュすることでしたが、その後、監視カメラ ストリーム (実際には rtsp ビデオ ストリーム)、インターネット ラジオ ステーション、およびビデオ ストリーム (通常は m3u8 で始まり m3u8 で終わる rtmp または http) が使用されるようになりました。ビデオ ストリーミング)、ローカル カメラ ストリーミング (ローカル USB カメラまたはラップトップ自体のカメラなど)、デスクトップ ストリーミング (現在実行中の環境のシステム デスクトップをキャプチャしてプッシュ)。分類によれば、実際には 3 つの主要なカテゴリがあり、1 つ目のカテゴリはビデオ ファイル (ローカル ビデオ ファイルとネットワーク ビデオ ファイルを含み、ファイル期間も含む)、2 つ目はさまざまなリアルタイム ビデオ ストリーム (監視カメラの rtsp、インターネット ラジオ局 rtmp、ネットワーク リアルタイム ビデオ m3u8 など)、3 番目のカテゴリはローカル デバイス コレクション (ローカル カメラ コレクションおよびローカル コンピュータ デスクトップ コレクションを含む) です。各カテゴリは、対応する一般的なコードで処理できます。基本的には、違いは、収集、デコード、ストリーミングの後の段階のコードがまったく同じであることです。

2.エフェクト描画

ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します

3. 体験アドレス

  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。

4. 機能的特徴

  1. さまざまなローカルビデオファイルとネットワークビデオファイルをサポートします。
  2. さまざまなネットワーク ビデオ ストリーム、Web カメラ、rtsp、rtmp、http などのプロトコルをサポートします。
  3. ローカルカメラデバイスからのストリーミングをサポートし、解像度、フレームレートなどを指定できます。
  4. ローカルデスクトップのストリーミングに対応し、画面領域やフレームレートなどを指定可能。
  5. ストリーミング メディア サービス プログラムを自動的に開始します。デフォルトは mediamtx (旧 rtsp-simple-server)、srs、EasyDarwin、LiveQing、ZLMediaKit などを選択できます。
  6. リアルタイムでプレビュー ビデオ ファイルを切り替えたり、ビデオ ファイルの再生の進行状況を切り替えたり、切り替えた場所でストリームをプッシュしたりできます。
  7. プッシュストリームの明瞭さと品質は調整可能です。
  8. ファイル、ディレクトリ、アドレスは動的に追加できます。
  9. ビデオ ファイルは自動的にループにプッシュされ、ビデオ ソースがビデオ ストリームの場合は、切断された後、自動的に再接続されます。
  10. ネットワーク ビデオ ストリームは自動的に再接続され、再接続が成功するとストリームは引き続きプッシュされます。
  11. ネットワークビデオストリームはリアルタイム性が非常に高く、遅延が非常に少なく、遅延時間は約100msです。
  12. CPU 使用率が非常に低く、4 チャネルのメイン ストリーム プッシュは CPU の 0.2% しか使用しません。理論的には、通常の通常の PC マシンは何の圧力もかけることなく 100 チャネルをプッシュできますが、主なパフォーマンスのボトルネックはネットワークです。
  13. ストリーミングには、rtsp/rtmp の 2 つのオプションがあります。プッシュされたデータは、rtsp/rtmp/hls/webrtc の 4 つの直接アクセス方法をサポートしており、ブラウザで直接開いてリアルタイム画像を表示できます。
  14. ストリームを外部ネットワーク サーバーにプッシュし、携帯電話、コンピュータ、タブレット、その他のデバイスを通じて対応するビデオ ストリームを再生できます。
  15. 各プッシュ ストリームは一意の識別子を使用して手動で指定できます (ストリーミングを容易にするため/ユーザーは複雑なアドレスを覚える必要がありません)。指定しない場合、ハッシュ値は戦略に従ってランダムに生成されます。
  16. テスト Web ページを自動的に生成し、直接開いて再生すると、リアルタイムの効果が確認でき、数値に応じて自動的にグリッドに表示されます。
  17. ストリーミング処理中に、表内の対応するストリーミング項目を切り替えたり、プッシュされているビデオをリアルタイムでプレビューしたり、ビデオ ファイルの再生の進行状況を切り替えたりすることができます。
  18. 音声と映像を同時にプッシュし、264/265/aac形式に準拠した元のデータは自動的にプッシュされ、準拠していないデータは自動的にトランスコードされてからプッシュされます(ある程度のCPUを占有します)。
  19. トランスコーディング戦略は、自動処理 (要件を満たす元のデータ/要件を満たさないトランスコーディング)、ファイルのみ (ファイル タイプのトランスコードされたビデオ)、およびすべてのトランスコーディングの 3 つのタイプをサポートします。
  20. この表には、各ストリームの解像度とオーディオおよびビデオ データのステータスがリアルタイムで表示されます。灰色は入力ストリームなし、黒は出力ストリームなし、緑は元のデータ ストリーム、赤はトランスコードされたデータ ストリームを表します。
  21. ビデオ ソースとストリーミング メディア サーバーに自動的に再接続し、起動後にプッシュ アドレスとオープン アドレスがリアルタイムで再接続されるようにします。復元されている限り、すぐに接続して収集とプッシュを続行します。
  22. ループプッシュの例が提供されています. ビデオソースは同時に複数のストリーミングメディアサーバーにプッシュされます. たとえば、ビデオを開いて同時にDouyin/Kuaishou/Bilibiliにプッシュされます. 録画として使用できますと再生を押すと、リストがループするので、非常に便利で実用的です。
  23. さまざまなストリーミング メディア サーバーの種類に応じて、対応する rtsp/rtmp/hls/flv/ws-flv/webrtc アドレスが自動的に生成され、ユーザーはそのアドレスをプレーヤーまたは Web ページに直接コピーしてプレビューすることができます。
  24. エンコードされたビデオ形式は、自動的に処理(ソースが 264 の場合は 264、ソースが 265 の場合は 265)、H264 への変換(264 への強制変換)、または H265 への変換(265 への強制変換)が可能です。
  25. Qt4/Qt5/Qt6 の任意のバージョンと任意のシステム (windows/linux/macos/android/embedded linux など) をサポートします。

5. 関連コード

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

おすすめ

転載: blog.csdn.net/feiyangqingyun/article/details/132894273