视频轨道添加
在建立完成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 问题较多,比如
- 如果机器上没有摄像头,就会报错。因为CreateDevice失败
- 该摄像头只能被使用一次,第二次创建就同样会报错。
- 在某些环境下,针对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