什么是推流?
推流,指的是把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号传到网络的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观众观看直播时就会发生卡顿等现象,观看体验很是糟糕。(我们的流式中,系统目前是后端做渲染,通过自定义的编码器实现视频流内容,然后交给客户端去显示。
我们尝试捕捉某个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
RA/SD 衍生者AI训练营。发布者:chris,转载请注明出处:https://www.shxcj.com/archives/6579