第二十六章 视频轨道和视频捕捉器

视频轨道添加

在建立完成P2P连接后,我们最主要的一步内容是增加视频通道,这可能是整个环节中最重要的内容了。很多问题都是关于视频流内容和画面的内容。

在WebRTC中默认提供2种视频源捕获器。分别是DesktopCapturer和 VCMCapturer, 即桌面捕获器和摄像头捕获器。

桌面捕捉器

DesktopCapturer 目前可以多次使用,没有发现什么问题(当然多个之后有CPU性能瓶颈)

以下是我们的流式系统设计和使用桌面捕捉器的流转架构图。

第二十六章 视频轨道和视频捕捉器

在webrtc默认提供的桌面捕捉器中,有三种采集方式,有全屏采集,窗口采集,和指定区域采集。每种采集方式,都有自己特定的使用场景,这里主要关注的,是如何使用 WebRTC 实现这三种采集方式。

WebRTC 中屏幕采集的源码在 webrtc/src/modules/desktop_capture/目录下。在 desktop_capture 目录中的 desktop_capturer.h 中定义了 DesktopCapturer 类,DesktopCapturer 类抽象了屏幕采集要用到的接口。windows 平台的屏幕采集实现,在 webrtc/src/modules/desktop_capture/win 目录下,其中有ScreenCapturerWinGdi类,ScreenCapturerWinMagnifier类,DesktopAndCursorComposer类,WindowCapturerWinGdi 类,WgcCapturerWin 类。这些实现类,分别实现了 windows 平台的屏幕采集和窗口采集功能。

全屏采集

ScreenCapturerWinGdi 类只实现了单纯的屏幕采集功能,如果需要在全屏采集时过滤掉指定的窗口,则需要使用ScreenCapturerWinMagnifier类,通过 SetExcludedWindow 接口设置需要过滤的窗口。ScreenCapturerWinMagnifier 类只实现了过滤窗口的功能,如果需要在过滤窗口的同时还要显示鼠标位置,就必须使用 DesktopAndCursorComposer 类,DesktopAndCursorComposer 类实现了将鼠标位置与屏幕图像合并的功能。

窗口采集

WindowCapturerWinGdi 类最早实现了采集指定窗口的功能,但是对于启用了硬件加速的窗口,则无法采集到窗口内的内容,只能采集到窗口的边框。在最新版本的 WebRTC 中,提供了 WgcCapturerWin 类,WgcCapturerWin 实现了采集全屏和采集窗口功能,重要的是,WgcCapturerWin 可以采集开启了硬件加速的窗口,比如 chrome 浏览器。

采集区域

DesktopCapturer 类没有提供采集指定区域的接口,所以,需要在 DesktopCapturer 类中添加一个非纯虚函数,函数接受四个参数, 分别是指定区域的左上角坐标x和y,还有区域大小width和height。然后再创建一个继承 ScreenCapturerWinGdi 的新类,然后重载 CaptureFrame 方法,可以拷贝 ScreenCapturerWinGdi 类中的 CaptureFrame 实现,然后把采集的区域指定为自定义的区域(把原来的全屏区域修改为自定义的区域)。这样就实现了采集指定区域。

趟过的坑:指定区域的 width 最好为 16 的整数倍,不可以为奇数。height 最好是 2 的整数倍。

屏幕共享流程

1.创建 DesktopCapturer 实例,可以根据需求创建不同的 DesktopCapturer 实现类,比如 ScreenCapturerWinMagnifier 类或者 DesktopAndCursorComposer类或者 WgcCapturerWin 类。

2.获取屏幕ID列表或窗口ID列表。

3.指定要采集的屏幕ID或窗口ID列表。

4.注册数据回调,开始采集。

5.将回调中的屏幕图像编码传输。

趟过的坑:DesktopCapturer 实例一定要在同一个线程内创建,初始化和销毁。

摄像头捕捉器

VCMCapturer 问题较多,比如

  1. 如果机器上没有摄像头,就会报错。因为CreateDevice失败
  2. 该摄像头只能被使用一次,第二次创建就同样会报错。
  3. 在某些环境下,针对USB的读写有控制,读写摄像头会受到某种限制,导致在Createdevice的时候失败。

增加的代码展示了用VCM作为视频轨道源,具体的VCM实现可以参考webrtc源代码中的示意代码,网上也很多,这里不再重复贴出。

        void QLSignalConnection::CreateVCMTrack(bool& ret)
        {

                QL::VcmCapturerWrapper* myCaptureDevice = QL::VcmCapturerWrapper::Create(1280, 720, QL::QLGlobalConfig::Get().FPS());
                if (myCaptureDevice)
                {
                        int windowIndex = 0;
                        char myLabel[255] = { 0 };
                        sprintf(myLabel, "VCMVideoLabelID%d", windowIndex);
                        char myTrackLabel[255] = { 0 };
                        sprintf(myTrackLabel, "VCMVideoTrackLabelID%d", windowIndex);

                        mCaptureBaseInterface = (ICaptureBaseInterface*)myCaptureDevice;
                        rtc::scoped_refptr<webrtc::VideoTrackInterface> myVideoTrack(mPeerConnectionFactory->CreateVideoTrack(myLabel, myCaptureDevice));

                        //添加流媒体
                        auto result_or_error = mPeerConnection->AddTrack(myVideoTrack, { myTrackLabel });
                        if (!result_or_error.ok())
                        {

                                QL_ERROR((std::string("Failed to add VCM video track to PeerConnection:") + std::string(result_or_error.error().message())).c_str());
                                ret = false;
                                return;
                        }

                        QL_LOG("Add VCM capturer successfully.");
                }
                else
                {
                        QL_ERROR("Create VCM mode failure.");
                        ret = false;
                        return;
                }

                ret = true;
        }

上述的代码中,需要注意的是,几个string代表的名字对象非常重要,它会出现在最终的SDP交换数据中。后续实际的应用场景你可能需要分析对应轨道的情况。就会用到了。

自定义视频捕捉器

然而我们大多数情况下,并不需要用到VCMCapturer或者DesktopCapturer ,我们的数据源是自定义的,行为也想自定义。此时就需要用到自定义的视频源捕获器了。

现在国内的技术氛围真差,找了很久没有找到相关的直接的代码,这里我完整写出来没有什么可以保密的。技术本来分享了才有价值。

主要设计思路是

主要内容备注
1创建一个自定义视频捕获器的类,继承rtc::AdaptedVideoTrackSource该基类在Webrtc源代码的media/base/adapted_video_track_source.h文件中
2利用VisualStudio的重构工具自动化一键实现该基类的所有虚函数。这是一个标准模板行为,必须要主要函数的作用:AddRef和Release主要是针对指针对象的操作state: 一般的返回 webrtc::MediaSourceInterface:kLive 状态即可remote : 返回false 即可is_screencast : 返回true,代表自适应码流或分辨率调整,当然也可以falseneeds_denoising : 编码器在编码前是否去噪视频。一般为false,看你需求
3要有个StartCapture()和StopCapture()函数来控制捕获行为的开始和结束
4初始化的时候传入FPS,从而控制捕获的周期
5在StartCapture函数中,实现一个根据FPS来持续调用的线程,做视频帧数据相关的处理。然后将数据准备好后调用OnFrame函数给到webrtc流程中
6具体处理数据的函数是CaptureOurImage ,该函数你可以自定义行为,调用时机和参数控制。-》我们完整的代码中,是读取本地文件夹中的一堆BMP文件。
7在AddTrack之类的方法将这个视频源捕获器添加到peerconnection中

完整源代码头文件

#pragma once
 
#include <string>
#include <thread>
 
#include "absl/memory/memory.h"
#include "absl/types/optional.h"
#include "api/audio/audio_mixer.h"
#include "api/audio_codecs/audio_decoder_factory.h"
#include "api/audio_codecs/audio_encoder_factory.h"
#include "api/audio_codecs/builtin_audio_decoder_factory.h"
#include "api/audio_codecs/builtin_audio_encoder_factory.h"
#include "api/audio_options.h"
#include "api/create_peerconnection_factory.h"
#include "api/rtp_sender_interface.h"
#include "api/video_codecs/builtin_video_decoder_factory.h"
#include "api/video_codecs/builtin_video_encoder_factory.h"
#include "api/video_codecs/video_decoder_factory.h"
#include "api/video_codecs/video_encoder_factory.h"
 
#include "modules/audio_device/include/audio_device.h"
#include "modules/audio_processing/include/audio_processing.h"
#include "modules/video_capture/video_capture.h"
#include "modules/video_capture/video_capture_factory.h"
#include "p2p/base/port_allocator.h"
#include "pc/video_track_source.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/ref_counted_object.h"
#include "rtc_base/rtc_certificate_generator.h"
#include "rtc_base/strings/json.h"
#include "modules/desktop_capture/desktop_capturer.h"
#include "modules/desktop_capture/desktop_frame.h"
#include "modules/desktop_capture/desktop_capture_options.h"
#include "modules/desktop_capture/cropping_window_capturer.h"
#include "media/base/adapted_video_track_source.h"
#include "api/video/i420_buffer.h"
 
 
namespace QL
{
    class FakeVideoCapturer :public rtc::AdaptedVideoTrackSource
    {
    public:
        void CaptureOurImage();
        //void StartCapture();
        void StopCapture();
 
 
        // 通过 AdaptedVideoTrackSource 继承virtual webrtc::MediaSourceInterface::SourceState state() const override;
        virtualbool remote() const override;
        virtualbool is_screencast() const override;
        virtual absl::optional<bool> needs_denoising() const override;
 
        virtualvoid AddRef() const override;
        virtual rtc::RefCountReleaseStatus Release() const override;
 
    protected:
        explicit FakeVideoCapturer(size_t fps)
            : mFPS(fps)
        {
 
        }
 
    private:
        mutablevolatileint ref_count_;
 
 
        //开线程做捕捉行为
        std::unique_ptr<std::thread> mCaptureThread;
        //是否已经启动捕捉的标志位
        std::atomic_bool mIsStarted;
        //我们输出视频的FPSsize_t mFPS;
 
        rtc::scoped_refptr<webrtc::I420Buffer> mI420YUVBbuffer;
 
        unsigned int mImageIndex;
    };
}

完整源代码cpp文件

这里要注意的是数据源,我们通过这句代码

sprintf_s(imgFileName, “%s%d.bmp”, “testimages/img_display_out”, mImageIndex);

去获取testimages目录下文件名规律化的img_display_out1,2,3,4.bmp这样子的规律每秒30帧的读取测试数据源座作为给编码器的原始数据。

#include "FakeVideoCapturer.h"
#include <rtc_base/atomic_ops.h>
#include "../CommonHelper.h"
#include <libyuv.h>
 
#define TEST_IMAGE_COUNT 639
 
 
namespace QL
{
 
    void FakeVideoCapturer::AddRef() const
    {
        rtc::AtomicOps::Increment(&ref_count_);
    }
 
    rtc::RefCountReleaseStatus FakeVideoCapturer::Release() const
    {
        constint count = rtc::AtomicOps::Decrement(&ref_count_);
        if (count == 0) {
            return rtc::RefCountReleaseStatus::kDroppedLastRef;
        }
        return rtc::RefCountReleaseStatus::kOtherRefsRemained;
    }
 
    webrtc::MediaSourceInterface::SourceState FakeVideoCapturer::state() const
    {
        return webrtc::MediaSourceInterface::kLive;
        //              return SourceState();
    }
 
    bool FakeVideoCapturer::remote() const
    {
        returnfalse;
    }
 
    bool FakeVideoCapturer::is_screencast() const
    {
        returntrue;
    }
 
    absl::optional<bool> FakeVideoCapturer::needs_denoising() const
    {
        returnfalse;
 
    }
 
 
    void FakeVideoCapturer::CaptureOurImage()
    {
        //获取本地Bitmap图片数据
        uint8_t* myImageData;
        unsigned int imageWidth;
        unsigned int imageHeight;
        unsigned int imagePixelFormat;
        char imgFileName[MAX_PATH] = { 0 };
        sprintf_s(imgFileName, "%s%d.bmp", "testimages/img_display_out", mImageIndex);
        char targetImageFullPath[MAX_PATH] = { 0 };
        CommonHelper::GetCurrentPath(targetImageFullPath, imgFileName);
        CommonHelper::ReadBitmapImage(targetImageFullPath, &myImageData, &imageWidth, &imageHeight, &imagePixelFormat);
 
 
        //CommonHelper::SaveBitmapToFile(myImageData, imageWidth, imageHeight, 32, 0, "d:\\b.bmp");int width = imageWidth;
        int height = imageHeight;
 
        if (!mI420YUVBbuffer.get() || mI420YUVBbuffer->width() * mI420YUVBbuffer->height() < width * height)
        {
            mI420YUVBbuffer = webrtc::I420Buffer::Create(width, height);
        }
 
        int stride = width;
        uint8_t* yplane = mI420YUVBbuffer->MutableDataY();
        uint8_t* uplane = mI420YUVBbuffer->MutableDataU();
        uint8_t* vplane = mI420YUVBbuffer->MutableDataV();
        libyuv::ConvertToI420(myImageData, 0, yplane, stride, uplane,
            (stride + 1) / 2, vplane, (stride + 1) / 2, 0, 0,
            width, height, width, height, libyuv::kRotate0,
            libyuv::FOURCC_ARGB);
 
        //here will copy data , should relase by API level
        webrtc::VideoFrame myYUVFrame = webrtc::VideoFrame(mI420YUVBbuffer, 0, 0, webrtc::kVideoRotation_0);
 
        mImageIndex++;
 
        if (mImageIndex > TEST_IMAGE_COUNT)
        {
            mImageIndex = 0;
        }
        this->OnFrame(myYUVFrame);
 
        //Release memorydelete[] myImageData;
        myImageData = nullptr;
    }
 
    /// <summary>////// </summary>void FakeVideoCapturer::StartCapture()
    {
        if (mIsStarted)
        {
            return;
        }
        mIsStarted = true;
 
        // Start new thread to capture
        mCaptureThread.reset(new std::thread([this]()
            {
                mImageIndex = 0;
 
                while (mIsStarted)
                {
                    CaptureOurImage();
                    std::this_thread::sleep_for(std::chrono::milliseconds(1000 / mFPS));
                }
            }));
    }
 
    /// <summary>////// </summary>void FakeVideoCapturer::StopCapture()
    {
        mIsStarted = false;
 
        if (mCaptureThread && mCaptureThread->joinable())
        {
            mCaptureThread->join();
        }
 
    }
}

RA/SD 衍生者AI训练营。发布者:chris,转载请注明出处:https://www.shxcj.com/archives/6576

(0)
上一篇 4天前
下一篇 4天前

相关推荐

发表回复

登录后才能评论
本文授权以下站点有原版访问授权 https://www.shxcj.com https://www.2img.ai https://www.2video.cn