WebRTC 屏幕共享
功能简介
屏幕共享包括屏幕采集和视频流推送两部分功能。与远程桌面不同,屏幕共享只是将本地桌面内容以视频流的方式分享到网络。本文的重点,是讲解如何应用 WebRTC 的屏幕采集功能。对于WebRTC 视频编码传输功能的应用,需要专门的文章进行讲解,这里暂时不做展开,而是把重点集中在屏幕采集上。WebRTC 提供了多个平台的屏幕共享功能,这里以 windows 10 平台作为开发环境,讲述如何在 window 平台,应用 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 实例一定要在同一个线程内创建,初始化和销毁。
代码示例
#ifndef __RCRTC_DESKTOP_CAPTURER__ #define __RCRTC_DESKTOP_CAPTURER__ #include "api/video/i420_buffer.h" #include "libyuv/convert.h" #include "libyuv/video_common.h" #include "media/base/video_broadcaster.h" #include "media/base/video_common.h" #include "modules/desktop_capture/desktop_and_cursor_composer.h" #include "rtc_base/thread.h" #include "system_wrappers/include/sleep.h" #include "webrtc_desktop_capturer.h" typedef void (*desktop_capture_frame_callback)(int width, int height, int y_stride, int u_stride, int v_stride, const uint8_t* y, const uint8_t* u, const uint8_t* v, void* context); class RcrtcDesktopCapturer : public webrtc::DesktopCapturer::Callback { public: RcrtcDesktopCapturer(const std::map<std::string, std::string>& opts) {} inline ~RcrtcDesktopCapturer() override {} // overide webrtc::DesktopCapturer::Callback void OnCaptureResult(webrtc::DesktopCapturer::Result result, std::unique_ptr<webrtc::DesktopFrame> frame) override { if (result == webrtc::DesktopCapturer::Result::SUCCESS) { int width = frame->size().width(); int height = frame->size().height(); rtc::scoped_refptr<webrtc::I420Buffer> I420buffer = webrtc::I420Buffer::Create(width, height); const int conversionResult = libyuv::ConvertToI420( frame->data(), 0, I420buffer->MutableDataY(), I420buffer->StrideY(), I420buffer->MutableDataU(), I420buffer->StrideU(), I420buffer->MutableDataV(), I420buffer->StrideV(), 0, 0, // width, height, I420buffer->width(), I420buffer->height(), I420buffer->width(), I420buffer->height(), libyuv::kRotate0, ::libyuv::FOURCC_ARGB); if (conversionResult >= 0) { webrtc::VideoFrame videoFrame(I420buffer, webrtc::VideoRotation::kVideoRotation_0, rtc::TimeMicros()); rtc::scoped_refptr<webrtc::I420BufferInterface> buffer( videoFrame.video_frame_buffer()->ToI420()); _frameCallback(buffer->width(), buffer->height(), buffer->StrideY(), buffer->StrideU(), buffer->StrideV(), buffer->DataY(), buffer->DataU(), buffer->DataV(), this->_userContext); } } } void setExccludeWindow(webrtc::DesktopCapturer::Source windowId) { _excludeWindowList.push_back(windowId); } void CaptureThread() { webrtc::DesktopCaptureOptions opts = webrtc::DesktopCaptureOptions::CreateDefault(); opts.set_allow_use_magnification_api(true); //设置过滤窗口选项 // 使用 DesktopAndCursorComposer 可以采集鼠标 std::unique_ptr<webrtc::DesktopCapturer> capturer = std::unique_ptr<webrtc::DesktopCapturer>( new webrtc::DesktopAndCursorComposer( webrtc::DesktopCapturer::CreateScreenCapturer(opts), opts)); // 设置开始采集状态 capturer->Start(this); // 设置要过滤的窗口 for (auto source : _excludeWindowList) { capturer->SetExcludedWindow(source.id); } while (_isrunning) { webrtc::SleepMs(_msPerFrame); // 采集桌面图像 capturer->CaptureFrame(); } } bool Start() { _isrunning = true; _capture_thread = rtc::Thread::Create(); _capture_thread->Start(); _capture_thread->PostTask(RTC_FROM_HERE, [&] { CaptureThread(); }); } void Stop() { if (_isrunning) { _isrunning = false; _capture_thread->Stop(); _capture_thread.reset(); } } public: int _msPerFrame = 100; // 100毫秒采集一次,每秒钟采集10帧 webrtc::DesktopCapturer::SourceList _excludeWindowList; //需要过滤的窗口列表 desktop_capture_frame_callback _frameCallback = nullptr; //视频输出回调 void* _userContext = nullptr; private: std::unique_ptr<rtc::Thread> _capture_thread; bool _isrunning = false; }; #endif
总结
如果理清了 WebRTC 屏幕共享的脉络,整个过程还不算太复杂。但是,如果一开始没有对WebRTC的 屏幕共享有一个概览,冒然逐步趟坑,说不准就会卡在某处,无法前进。希望本文对正准备应用 WebRTC 屏幕共享的人有所帮助。
融云 RCRTC C++ SDK 已经实现了 Windows 的屏幕共享功能,并且处理了很多产品需求层面的细节。有兴趣的同学,可以关注我们的 SDK,关注融云。