什么是推流?
推流,指的是把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号传到网络的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观众观看直播时就会发生卡顿等现象,观看体验很是糟糕。(我们的流式中,系统目前是后端做渲染,通过自定义的编码器实现视频流内容,然后交给客户端去显示。
本文中的截图或者说配图都是用微信小程序【字形绘梦】制作,谢谢该软件的免费支持。
我们尝试捕捉某个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; } }
核心函数讲解
我们主要介绍下细节内容:
- 首先我们需要将视频轨道的具体实现添加进入P2P的连接后。然后在具体的视频捕获器中实现具体的几个重要节点函数。
- 第一个重要函数是:视频处理类的初始化,并且在尽可能早的时候调用StartCapture 函数。整个函数不是必须,但是一般我们建议这么设计。好处在于,初始化视频捕捉类后,可能不一定适合立即处理视频流内容,我们还需要延迟一段时间进行正式处理,避免短暂的有黑屏的情况发生。并且有了StartCapture函数,就会有StopCapture,保证逻辑上完整。开始和结束对应。
- 在Capture视频流内容的函数中,我们需要开启一个线程专门处理这个业务。避免影响其余的主要业务。同时,我们通过外部设置的FPS来控制调用这个捕捉视频源数据函数的频率。从而实现外部要求的FPS,也就是帧率。一般的,如果是视频数据,我们只需要保证>24FPS即可。但是考虑到很多实际的场合应用30FPS是比较好的设置。但是也考虑到视频捕获的耗时,这方面可能需要具体根据需求来进行控制。
- 视频捕捉的具体逻辑中,我们将原始的数据转换成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个主要的行为,
- 获取webrtc::VideoFrame 这个对象
- 调用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