第二十七章 视频推流细节

什么是推流?

推流,指的是把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号传到网络的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观众观看直播时就会发生卡顿等现象,观看体验很是糟糕。(我们的流式中,系统目前是后端做渲染,通过自定义的编码器实现视频流内容,然后交给客户端去显示。

我们尝试捕捉某个Window的界面内容)

要想用于推流还必须把音视频数据使用传输协议进行封装,变成流数据。常用的流传输协议有RTSP、RTMP、HLS等,使用RTMP传输的延时通常在1–3秒,对于手机直播这种实时性要求非常高的场景,RTMP也成为手机直播中最常用的流传输协议。最后通过一定的Qos算法将音视频流数据推送到网络断,通过CDN进行分发。

在直播场景中,网络不稳定是非常常见的,这时就需要Qos来保证网络不稳情况下的用户观看直播的体验,通常是通过主播端和播放端设置缓存,让码率均匀。另外,针对实时变化的网络状况,动态码率和帧率也是最常用的策略。

直播中使用广泛的“推流协议”一般是RTMP(Real Time Messaging Protocol——实时消息传输协议)。该协议是一个基于TCP的协议族,是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red5等。

什么是拉流?

拉流是指服务器已有直播内容,根据协议类型(如RTMP、RTP、RTSP、HTTP等),与服务器建立连接并接收数据,进行拉取的过程。拉流端的核心处理在播放器端的解码和渲染,在互动直播中还需集成聊天室、点赞和礼物系统等功能。

拉流端现在支持RTMP、HLS、HDL(HTTP-FLV)三种协议,其中,在网络稳定的情况下,对于HDL协议的延时控制可达1s,完全满足互动直播的业务需求。RTMP是Adobe的专利协议,开源软件和开源库都支持的比较好,延时一般在1-3秒。HLS是苹果提出的基于HTTP的流媒体传输协议,优先是跨平台性比较好,HTML5可以直接打开播放,移动端兼容性良好,但是缺点是延迟比较高。

源代码展示

上源代码头文件部分

#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"
#include "third_party/libyuv/include/libyuv.h"
#include "../CaptureInterface/BaseCaptureInterface.h"

#include "modules\desktop_capture\win\window_capturer_win_gdi.h"

namespace QL
{

        //class QLWindowCaptureEXImpl :public webrtc::WindowCapturerWinGdi
        //{

        //};


        /// <summary>
        /// 封装了捕捉指定窗口名字的截屏类 , 目前只支持在主屏幕上
        /// 由于我们不是使用使用摄像头作为数据源,而是自定义的内容,因此必须实现此接口VideoTrackSourceInterface, 也就是其基类 AdaptedVideoTrackSource 也可以
        /// ICaptureBaseInterface是 我们自己的接口,为了统计数据
        /// </summary>
    class QLWindowCaptureImpl : public rtc::AdaptedVideoTrackSource, public webrtc::DesktopCapturer::Callback, ICaptureBaseInterface
        {

        public:
                // Inherited via Callback
        void OnCaptureResult (webrtc::DesktopCapturer::Result result, std::unique_ptr<webrtc::DesktopFrame> desktopframe) ;

                // 自己封装的函数,用于开始和停止捕捉数据
                void StartCapture () override;
                void StopCapture () override;

                //静态函数,创建一个本类。其实就是做了一些封装而已
                //Title和processID是个2选1的选择
                static QLWindowCaptureImpl* Create (size_t target_fps, std::string inTargetWindowTitle);

                //--RefCountedObject 这几个函数重载了AdaptedVideoTrackSource对象
                virtual void AddRef() const override;
                virtual rtc::RefCountReleaseStatus Release() const override;


        public:

                bool static WeThinkTheyAreEqual(std::string src, std::string dest);

                void SetState (SourceState new_state);

                SourceState state () const override
                {
                        return state_;
                }
                bool remote () const override
                {
                        return remote_;
                }

                bool is_screencast () const override
                {
                        return false;
                }
                absl::optional<bool> needs_denoising () const override
                {
                        return absl::nullopt;
                }

                bool GetStats (Stats* stats) override
                {
                        return false;
                }

        protected:
                explicit QLWindowCaptureImpl (std::unique_ptr<webrtc::DesktopCapturer> dc, size_t fps, std::string window_title)
                        : mDC (std::move (dc)),
                        mTargetFPS(fps),
                        mWinTitle (std::move (window_title)),
                        mIsStarted (false),
                        remote_ (false),
                        mRefCount(0)
                {
                }

        private:
                // RefCountedObject
                SourceState state_;
                const bool remote_;

                mutable webrtc::webrtc_impl::RefCounter mRefCount;

                //捕捉对象
                std::unique_ptr<webrtc::DesktopCapturer> mDC;
                //我们输出视频的FPS
                size_t mTargetFPS;
                //窗体Title
                std::string mWinTitle;
                rtc::scoped_refptr<webrtc::I420Buffer> mI420YUVBuffer;
                //开线程做捕捉行为
                std::unique_ptr<std::thread> mCaptureThread;
                //是否已经启动捕捉的标志位
                std::atomic_bool mIsStarted;

        };


}

Cpp部分

#include "QLWindowCapture.h"
#include "QLGlobalConfig.h"
#include <../microprofiler/microprofile.h>
#include "../CommonHelper/QLGlobalConfig.h"
#include "modules/desktop_capture/desktop_capture_options.h"
#include <boost/algorithm/string.hpp> 

#include "modules\desktop_capture\win\wgc_capturer_win.h"

namespace QL
{

        /// <summary>
        /// 抓取的帧数据,我们需要再次处理下
        /// 截屏后得到的数据格式是rgb,需要使用libyuv将数据从rgb转换为yuv420,然后传入编码器和进行本地渲染。
        /// 转换时注意填写正确的原始数据类型,windows下格式为webrtc::kARGB
        /// </summary>
        /// <param name="result"></param>
        /// <param name="desktopframe">回調準備好的桌面的幀數據了</param>
        void QLWindowCaptureImpl::OnCaptureResult(webrtc::DesktopCapturer::Result result, std::unique_ptr<webrtc::DesktopFrame> desktopframe)
        {
                switch (result)
                {
                case webrtc::DesktopCapturer::Result::SUCCESS:
                        //Only this is valid.
                        break;
                case webrtc::DesktopCapturer::Result::ERROR_TEMPORARY:
                        QL_ERROR("Error from capture frame source temporary.");
                        return;
                case webrtc::DesktopCapturer::Result::ERROR_PERMANENT:
                        QL_ERROR("Error from capture frame source permanent.");
                        return;
                default:
                        return;
                }

                int width = desktopframe->stride() / 4;
                int startYPosition = 0;
                int height = desktopframe->size().height();

                if (QL::QLGlobalConfig::Get().IsHideWindowTitleInAppCaptureType())
                {
                        //This is offical window title height;
                        startYPosition = 30;
                        height -= startYPosition;
                }

                if (!mI420YUVBuffer.get() || mI420YUVBuffer->width() * mI420YUVBuffer->height() < width * height)
                {
                        mI420YUVBuffer = webrtc::I420Buffer::Create(width, height);
                }

                int stride = width;
                uint8_t* yplane = mI420YUVBuffer->MutableDataY();
                uint8_t* uplane = mI420YUVBuffer->MutableDataU();
                uint8_t* vplane = mI420YUVBuffer->MutableDataV();

                libyuv::ConvertToI420(desktopframe->data(), 0, yplane, stride, uplane,
                        (stride + 1) / 2, vplane, (stride + 1) / 2, 0, startYPosition,
                        width, height, width, height, libyuv::kRotate0,
                        libyuv::FOURCC_ARGB);

                webrtc::VideoFrame frame = webrtc::VideoFrame(mI420YUVBuffer, 0, 0, webrtc::kVideoRotation_0);

                //将这个VideoFrame数据压倒流中,传出去
                this->OnFrame(frame);
        }



        /// <summary>
        /// 开始截屏
        /// </summary>
        void QLWindowCaptureImpl::StartCapture()
        {
                if (mIsStarted)
                {
                        return;
                }

                mIsStarted = true;
                // Start new thread to capture
                //We have to calc the time of capture method,  set this as timeSpan
                //if we set fps=30, then sleep time will be 1000/fps- timeSpan 
                //
                mCaptureThread.reset(new std::thread([this]()
                        {
                                //第一步開始捕捉
                                mDC->Start(this);
                                //auto ifps = 1000 / mFPS;
                                std::chrono::milliseconds ifps = std::chrono::milliseconds(1000 / mTargetFPS);

                                while (mIsStarted)
                                {
                                        auto aTime = webrtc::Clock::GetRealTimeClock()->TimeInMilliseconds();
                                        //第二步,每幀捕捉,這樣會進入OnCaptureResult
                                        mDC->CaptureFrame();


                                        auto bTime = webrtc::Clock::GetRealTimeClock()->TimeInMilliseconds();

                                        auto timeSpan = std::chrono::duration<double, std::micro>(bTime - aTime);
                                        auto fpsSpan = std::chrono::duration<double, std::micro>(ifps - timeSpan);

                                        std::this_thread::sleep_for(fpsSpan);

                                        auto cTime = webrtc::Clock::GetRealTimeClock()->TimeInMilliseconds();
                                        auto timeForRealFPS = std::chrono::duration<double, std::micro>(cTime - aTime);
#if _DEBUG        
#if TEST_FPS_TIME
                                        std::cout << "Capture Processed time (ms):" << (bTime - aTime) << std::endl;
                                        std::cout << "Sleep time (ms):" << (ifps.count() - timeSpan.count()) << std::endl;

                                        std::cout << "Real FPS:" << 1000 / timeForRealFPS.count() << std::endl;
#endif
#endif
                                        this->SetRealFPS(timeForRealFPS.count());
                                        MICROPROFILE_SCOPEI(__FUNCTION__, "FPS", 0xff3399ff);
                                        MICROPROFILE_META_CPU("FPS", timeForRealFPS.count());
                                        MicroProfileFlip();
                                }
                        }));
        }

        /// <summary>
        /// 停止截屏
        /// </summary>
        void QLWindowCaptureImpl::StopCapture()
        {
                mIsStarted = false;

                if (mCaptureThread && mCaptureThread->joinable())
                {
                        mCaptureThread->join();
                }
        }

        bool QLWindowCaptureImpl::WeThinkTheyAreEqual(std::string src, std::string dest)
        {
                std::string mySrc = src;
                boost::algorithm::to_lower(mySrc);
                std::string myDest = dest;
                boost::algorithm::to_lower(myDest);

                std::string::size_type myIndex = myDest.find(mySrc);
                if (myIndex == std::string::npos)
                {
                        return false;
                }
                else
                {
                        return true;
                }
        }

        /// <summary>
        /// 创建捕捉指定窗体名字的视频流对象
        /// </summary>
        /// <param name="target_fps">目标FPS</param>
        /// <param name="inTargetWindowTitle">对应窗口名字.无视这个窗体在哪个屏幕中都可以获取到</param>
        /// <param name="processID">请注意sources[i].id中的不是ProcessID</param>
        /// <returns></returns>
        QLWindowCaptureImpl* QLWindowCaptureImpl::Create(size_t target_fps, std::string inTargetWindowTitle)
        {
                /*webrtc::DesktopCaptureOptions myOption;
                myOption.set_allow_cropping_window_capturer(true);
                myOption.set_enumerate_current_process_windows(true);
                myOption.set_use_update_notifications(true);*/
                //auto myDC = webrtc::DesktopCapturer::CreateWindowCapturer(myOption);

                auto myWindowCapturer = webrtc::DesktopCapturer::CreateWindowCapturer(webrtc::DesktopCaptureOptions::CreateDefault());

                //std::unique_ptr<webrtc::DesktopCapturer> myScreenCapturer;
                //myScreenCapturer =std::unique_ptr<webrtc::DesktopCapturer>(new webrtc::WgcCapturerWin(webrtc::DesktopCaptureOptions::CreateDefault()));


                if (!myWindowCapturer)
                {
                        return nullptr;
                }


                webrtc::DesktopCapturer::SourceList sources;
                webrtc::DesktopCapturer::Source mySelectedSource;
                //首先可以确定的是这个对象的ID 既不是进程的PID,也不是窗口句柄
                //貌似是webrtc的随机ID
                myWindowCapturer->GetSourceList(&sources);

                //Since it can be detect with == , sources[i].title property is not precise ,
                //sometimes it contain wrong content ,sometimes it is empty.
                //we don't have any other option to detect, so we have to detect it with our logic.
                for (size_t i = 0; i < sources.size(); i++)
                {
                        //if (WeThinkTheyAreEqual(inTargetWindowTitle ,""))
                        //{ 
                        //        //由于这个sources[i].id不是Processid,这里的代码永远不会走到。
                        //        if (sources[i].id == processID)
                        //        {
                        //                mySelectedSource = sources[i];
                        //                QL_LOG(std::string("Find target process id %d", processID));
                        //                break;
                        //        }
                        //}
                        //else
                        //{
                                if (WeThinkTheyAreEqual(inTargetWindowTitle, sources[i].title))
                                {
                                        mySelectedSource = sources[i];
                                        QL_LOG(std::string("Find target window name ") + inTargetWindowTitle);
                                        break;
                                }
                                else
                                {
                                        //QL_LOG(std::string("Existed window name :") + sources[i].title);
                                }
                        //}
                }
                if (mySelectedSource.title == "")
                {
                        QL_ERROR("not found target window ,name is %s", inTargetWindowTitle.c_str());
                        return NULL;
                }
                
                //這是最關鍵的。根據window title找到了window id 進行針對這個窗體的截屏
                //很奇怪的是這個ID并不是WIndow的Handle
                myWindowCapturer->SelectSource (mySelectedSource.id);

                auto targetWinTitle = mySelectedSource.title;
                auto targetFPS = target_fps;



                RTC_LOG(LS_INFO) << "Init DesktopCapture finish";
                // Start new thread to capture
                //主要是第一個參數,新建了捕捉器
                return new QLWindowCaptureImpl(std::move(myWindowCapturer), targetFPS, std::move(targetWinTitle));
        }

        void QLWindowCaptureImpl::AddRef() const
        {
                mRefCount.IncRef();
        }

        rtc::RefCountReleaseStatus QLWindowCaptureImpl::Release() const
        {
                rtc::RefCountReleaseStatus status = mRefCount.DecRef();
                return status;
        }

}

核心函数讲解

我们主要介绍下细节内容:

  1. 首先我们需要将视频轨道的具体实现添加进入P2P的连接后。然后在具体的视频捕获器中实现具体的几个重要节点函数。
  2. 第一个重要函数是:视频处理类的初始化,并且在尽可能早的时候调用StartCapture 函数。整个函数不是必须,但是一般我们建议这么设计。好处在于,初始化视频捕捉类后,可能不一定适合立即处理视频流内容,我们还需要延迟一段时间进行正式处理,避免短暂的有黑屏的情况发生。并且有了StartCapture函数,就会有StopCapture,保证逻辑上完整。开始和结束对应。
  3. 在Capture视频流内容的函数中,我们需要开启一个线程专门处理这个业务。避免影响其余的主要业务。同时,我们通过外部设置的FPS来控制调用这个捕捉视频源数据函数的频率。从而实现外部要求的FPS,也就是帧率。一般的,如果是视频数据,我们只需要保证>24FPS即可。但是考虑到很多实际的场合应用30FPS是比较好的设置。但是也考虑到视频捕获的耗时,这方面可能需要具体根据需求来进行控制。
  4. 视频捕捉的具体逻辑中,我们将原始的数据转换成RGBA的图像,有时候也可能是Gray的这中灰度图,但不管如何都是二进制的数据,无非就是组成这个二进制数据的数据格式,描述这些数据的RGBA4个通道还是RGB的三个通道,每个通道一般是8bit,但是也有10bit的图形数据,这种特殊的情况,我们后续再议。有了这个图像元数据,我们一般调用YUV420,或者444等视频格式的数据,因为最终推送到webrtc中的是一个Frame对象,它一般需要视频格式。至于什么是YUV420,444,等格式区别,本文不扩展。

核心代码结构

总体来看,这个主要的函数非常有意义

        void FakeVideoCapturer::CaptureOurImage()
        {
                MicroProfileFlip();
                webrtc::VideoFrame myYUVFrame = QLTestImageQuality::Get().DoImageQualityChecking(this->GetStatusObserver());

                MICROPROFILE_SCOPEI(__FUNCTION__, "OnFrame ", 0xeeee00);
                this->OnFrame(myYUVFrame);
                MICROPROFILE_SCOPEI(__FUNCTION__, "OnFrame(End)", 0xeedd44);
        }

上述核心代码中MicroProfile是我们度量性能消耗的一个衡量宏。可以帮助我们分析每一步的时间消耗。除此之外就只有2个主要的行为,

  1. 获取webrtc::VideoFrame 这个对象
  2. 调用OnFrame函数推送出去这个YUV 的Frame数据。

这个OnFrame是接口中的定义函数。我们的实现中只要具体实现,webrtc的工作流就会自动推送出去。 见webrtc中源代码

namespace rtc {

template <typename VideoFrameT>
class VideoSinkInterface {
 public:
  virtual ~VideoSinkInterface() = default;

  virtual void OnFrame(const VideoFrameT& frame) = 0;

  // Should be called by the source when it discards the frame due to rate
  // limiting.
  virtual void OnDiscardedFrame() {}

  // Called on the network thread when video constraints change.
  // TODO(crbug/1255737): make pure virtual once downstream project adapts.
  virtual void OnConstraintsChanged(
      const webrtc::VideoTrackSourceConstraints& constraints) {}
};

}  // namespace rtc

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

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

相关推荐

发表回复

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