访问 2img.ai 官网以获取更多AI/AIGC信息 访问 个人技术博客: fuqifai.github.io
本文大部分配图使用微信小程序【字形绘梦】免费生成。
AIGC技术讨论群
什么是推流?
推流,指的是把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号传到网络的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观众观看直播时就会发生卡顿等现象,观看体验很是糟糕。(我们的流式中,系统目前是后端做渲染,通过自定义的编码器实现视频流内容,然后交给客户端去显示。
我们尝试捕捉某个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可以直接打开播放,移动端兼容性良好,但是缺点是延迟比较高。
源代码展示
上源代码头文件部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#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部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#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,等格式区别,本文不扩展。
核心代码结构
总体来看,这个主要的函数非常有意义
1
2
3
4
5
6
7
8
9
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中源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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