在视频会议、线上课堂、游戏直播等场景,屏幕共享是一个最常见的功能。屏幕共享就是对屏幕画面的实时共享,端到端主要有几个步骤:录屏采集、视频编码及封装、实时传输、视频解封装及解码、视频渲染。

一般来说,实时屏幕共享时,共享发起端以固定采样频率(一般 8 - 15帧足够)抓取到屏幕中指定源的画面(包括指定屏幕、指定区域、指定程序等),经过视频编码压缩(应选择保持文本/图形边缘信息不失真的方案)后,在实时网络上以相应的帧率分发。
 
因此,屏幕采集是实现实时屏幕共享的基础,它的应用场景也是非常广泛的。
 
现如今 Flutter 的应用越来越广泛,纯 Flutter 项目也越来越多,那么本篇内容我们主要分享的是 Flutter 的屏幕采集的实现。

在详细介绍实现流程前,我们先来看看原生系统提供了哪些能力来进行屏幕录制。

  • iOS 11.0 提供了  ReplayKit 2用于采集跨 App 的全局屏幕内容,但仅能通过控制中心启动;iOS 12.0 则在此基础上提供了从 App 内启动 ReplayKit 的能力。
  • Android 5.0 系统提供了 MediaProjection  功能,只需弹窗获取用户的同意即可采集到全局屏幕内容。

我们再看一下 Android / iOS 的屏幕采集能力有哪些区别。

  • iOS 的ReplayKit  是通过启动一个Broadcast Upload Extension子进程来采集屏幕数据,需要解决主 App 进程与屏幕采集子进程之间的通信交互问题,同时,子进程还有诸如运行时内存最大不能超过 50M 的限制。
  • Android 的 MediaProjection  是直接在 App 主进程内运行的,可以很容易获取到屏幕数据的Surface。

虽然无法避免原生代码,但我们可以尽量以最少的原生代码来实现 Flutter 屏幕采集。将两端的屏幕采集能力抽象封装为通用的 Dart 层接口,只需一次部署完成后,就能开心地在 Dart 层启动、停止屏幕采集了。

接下来我们已 iOS 实现流程为例进行讲解

打开 Flutter App 工程中 ios目录下的Runner Xcode Project,新建一个 Broadcast Upload Extension  Target,在此处理 ReplayKit 子进程的业务逻辑。
 
首先需要处理主 App 进程与 ReplayKit 子进程的跨进程通信问题,由于屏幕采集的 audio/video buffer 回调非常频繁,出于性能与 Flutter 插件生态考虑,在原生侧处理音视频 buffer 显然是目前最靠谱的方案,那剩下要解决的就是启动、停止信令以及必要的配置信息的传输了。
 
对于启动ReplayKit 的操作,可以通过 Flutter 的 MethodChannel 在原生侧 new 一个RPSystemBroadcastPickerView,这是一个系统提供的 View,包含一个点击后直接弹出启动屏幕采集窗口的 Button。通过遍历 Sub View 的方式找到 Button 并触发点击操作,便解决了启动ReplayKit 的问题。

static Future<bool?> launchReplayKitBroadcast(String extensionName) async {
return await _channel.invokeMethod(
'launchReplayKitBroadcast', {'extensionName': extensionName});
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"launchReplayKitBroadcast" isEqualToString:call.method]) {
[self launchReplayKitBroadcast:call.arguments[@"extensionName"] result:result];
} else {
result(FlutterMethodNotImplemented);
}
} - (void)launchReplayKitBroadcast:(NSString *)extensionName result:(FlutterResult)result {
if (@available(iOS 12.0, *)) {
RPSystemBroadcastPickerView *broadcastPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:extensionName ofType:@"appex" inDirectory:@"PlugIns"];
if (!bundlePath) {
NSString *nullBundlePathErrorMessage = [NSString stringWithFormat:@"Can not find path for bundle `%@.appex`", extensionName];
NSLog(@"%@", nullBundlePathErrorMessage);
result([FlutterError errorWithCode:@"NULL_BUNDLE_PATH" message:nullBundlePathErrorMessage details:nil]);
return;
} NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
if (!bundle) {
NSString *nullBundleErrorMessage = [NSString stringWithFormat:@"Can not find bundle at path: `%@`", bundlePath];
NSLog(@"%@", nullBundleErrorMessage);
result([FlutterError errorWithCode:@"NULL_BUNDLE" message:nullBundleErrorMessage details:nil]);
return;
} broadcastPickerView.preferredExtension = bundle.bundleIdentifier;
for (UIView *subView in broadcastPickerView.subviews) {
if ([subView isMemberOfClass:[UIButton class]]) {
UIButton *button = (UIButton *)subView;
[button sendActionsForControlEvents:UIControlEventAllEvents];
}
}
result(@(YES));
} else {
NSString *notAvailiableMessage = @"RPSystemBroadcastPickerView is only available on iOS 12.0 or above";
NSLog(@"%@", notAvailiableMessage);
result([FlutterError errorWithCode:@"NOT_AVAILIABLE" message:notAvailiableMessage details:nil]);
}
}

然后是配置信息的同步问题:
 
方案一:使用 iOS 的App Group 能力,通过 NSUserDefaults 持久化配置在进程间共享配置信息,分别在 Runner Target 和 Broadcast Upload Extension Target 内开启 App Group 能力并设置同一个 App Group ID,然后就能通过-[NSUserDefaults initWithSuiteName] 读写此 App Group 内的配置了。

Future<void> setParamsForCreateEngine(int appID, String appSign, bool onlyCaptureVideo) async {
await SharedPreferenceAppGroup.setInt('ZG_SCREEN_CAPTURE_APP_ID', appID);
await SharedPreferenceAppGroup.setString('ZG_SCREEN_CAPTURE_APP_SIGN', appSign);
await SharedPreferenceAppGroup.setInt("ZG_SCREEN_CAPTURE_SCENARIO", 0);
await SharedPreferenceAppGroup.setBool("ZG_SCREEN_CAPTURE_ONLY_CAPTURE_VIDEO", onlyCaptureVideo);
}
- (void)syncParametersFromMainAppProcess {
// Get parameters for [createEngine]
self.appID = [(NSNumber *)[self.userDefaults valueForKey:@"ZG_SCREEN_CAPTURE_APP_ID"] unsignedIntValue];
self.appSign = (NSString *)[self.userDefaults valueForKey:@"ZG_SCREEN_CAPTURE_APP_SIGN"];
self.scenario = (ZegoScenario)[(NSNumber *)[self.userDefaults valueForKey:@"ZG_SCREEN_CAPTURE_SCENARIO"] intValue];
}

方案二:使用跨进程通知CFNotificationCenterGetDarwinNotifyCenter 携带配置信息来实现进程间通信。
 
接下来是停止 ReplayKit 的操作。也是使用上述的 CFNotification 跨进程通知,在 Flutter 主 App 发起结束屏幕采集的通知,ReplayKit 子进程接收到通知后调用-[RPBroadcastSampleHandler finishBroadcastWithError:]  来结束屏幕采集。

static Future<bool?> finishReplayKitBroadcast(String notificationName) async {
return await _channel.invokeMethod(
'finishReplayKitBroadcast', {'notificationName': notificationName});
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"finishReplayKitBroadcast" isEqualToString:call.method]) {
NSString *notificationName = call.arguments[@"notificationName"];
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)notificationName, NULL, nil, YES);
result(@(YES));
} else {
result(FlutterMethodNotImplemented);
}
} // Add an observer for stop broadcast notification
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
onBroadcastFinish,
(CFStringRef)@"ZGFinishReplayKitBroadcastNotificationName",
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
// Handle stop broadcast notification from main app process
static void onBroadcastFinish(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) { // Stop broadcast
[[ZGScreenCaptureManager sharedManager] stopBroadcast:^{
RPBroadcastSampleHandler *handler = [ZGScreenCaptureManager sharedManager].sampleHandler;
if (handler) {
// Finish broadcast extension process with no error
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnonnull"
[handler finishBroadcastWithError:nil];
#pragma clang diagnostic pop
} else {
NSLog(@"️ RPBroadcastSampleHandler is null, can not stop broadcast upload extension process");
}
}];
}

实战示例

下面为大家准备了一个实现了 iOS/Android 屏幕采集并使用 ZEGO RTC Flutter SDK (https://pub.dev/packages/zego_express_engine)进行推流直播的示例 Demo。

ZEGO RTC Flutter SDK 在原生侧提供了视频帧数据的对接入口,可以将上述流程中获取到的屏幕采集 buffer 发送给 RTC SDK 从而快速实现屏幕分享、推流。
 
iOS 端在获取到系统给的 SampleBuffer 后可以直接发送给 RTC SDK,SDK 能自动处理视频和音频帧。

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
[[ZGScreenCaptureManager sharedManager] handleSampleBuffer:sampleBuffer withType:sampleBufferType];
}

Android 端需要先向 RTC SDK 获取一个 SurfaceTexture 并初始化所需要的 Surface, Handler 然后通过上述流程获取到的 MediaProjection 对象创建一个 VirtualDisplay 对象,此时 RTC SDK 就能获取到屏幕采集视频帧数据了。

SurfaceTexture texture = ZegoCustomVideoCaptureManager.getInstance().getSurfaceTexture(0);
texture.setDefaultBufferSize(width, height);
Surface surface = new Surface(texture);
HandlerThread handlerThread = new HandlerThread("ZegoScreenCapture");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper()); VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", width, height, 1,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, handler);

最后,我们来总结一下 Flutter 屏幕采集实现的主要内容
 
首先从原理上要了解 iOS / Android 原生提供的屏幕采集能力,其次介绍了 Flutter 与原生之间的交互,如何在 Flutter 侧控制屏幕采集的启动与停止。最后示例了如何对接 ZEGO RTC SDK 实现屏幕分享推流。
 
目前,Flutter on Desktop 趋于稳定,ZEGO RTC Flutter SDK 已经提供了 Windows 端的初步支持,我们将持续探索 Flutter 在桌面端上的应用,敬请期待!

 

Flutter 屏幕采集如何实现(提供示例demo)的更多相关文章

  1. flutter屏幕适配

    现在的手机品牌和型号越来越多,导致我们平时写布局的时候会在个不同的移动设备上显示的效果不同, 比如我们的设计稿一个View的大小是300px,如果直接写300px,可能在当前设备显示正常,但到了其他设 ...

  2. c#实例化继承类,必须对被继承类的程序集做引用 .net core Redis分布式缓存客户端实现逻辑分析及示例demo 数据库笔记之索引和事务 centos 7下安装python 3.6笔记 你大波哥~ C#开源框架(转载) JSON C# Class Generator ---由json字符串生成C#实体类的工具

    c#实例化继承类,必须对被继承类的程序集做引用   0x00 问题 类型“Model.NewModel”在未被引用的程序集中定义.必须添加对程序集“Model, Version=1.0.0.0, Cu ...

  3. iOS之ProtocolBuffer搭建和示例demo

    这次搭建iOS的ProtocolBuffer编译器和把*.proto源文件编译成*.pbobjc.h 和 *.pbobjc.m文件时,碰到不少问题! 搭建pb编译器到时没有什么问题,只是在把*.pro ...

  4. 利用webuploader插件上传图片文件,完整前端示例demo,服务端使用SpringMVC接收

    利用WebUploader插件上传图片文件完整前端示例demo,服务端使用SpringMVC接收 Webuploader简介   WebUploader是由Baidu WebFE(FEX)团队开发的一 ...

  5. 使用Nancy搭建简单的Http服务的示例demo

    刚刚接触Nancy没几天,暂时还不会使用Nancy来做web开发,只是使用Nancy实现了一个简单的Http服务的Demo程序,实现对Post和Get请求的处理. Demo的示例代码地址如下:http ...

  6. Windows上配置Mask R-CNN及运行示例demo.ipynb

    最近做项目需要用到Mask R-CNN,于是花了几天时间配置.简单跑通代码,踩了很多坑,写下来分享给大家. 首先贴上官方Mask R-CNN的Github地址:https://github.com/m ...

  7. 百度编辑器UEditor ASP.NET示例Demo 分类: ASP.NET 2015-01-12 11:18 346人阅读 评论(0) 收藏

    在百度编辑器示例代码基础上进行了修改,封装成类库,只需简单配置即可使用. 完整demo下载 版权声明:本文为博主原创文章,未经博主允许不得转载.

  8. java原生实现屏幕设备遍历和屏幕采集(捕获)等功能

    前言:本章中屏幕捕获使用原生java实现,屏幕图像显示采用javacv1.3的CanvasFrame 一.实现的功能 1.屏幕设备遍历 2.本地屏幕图像采集(也叫屏幕图像捕获) 3.播放本地图像(采用 ...

  9. asp.net core 上使用redis探索(3)--redis示例demo

    由于是基于.net-core平台,所以,我们最好是基于IDistributedCache接口来实现.ASP.NET-CORE下的官方redis客户端实现是基于StackExchange的.但是官方提供 ...

  10. 微信红包功能(含示例demo)

    开通支付权限 登录微信公众平台管理后台,找到“微信支付”一栏,进行开通会跳转到“微信支付商户平台”,根据提示提交相关证明,完成支付权限的开通开通之后,“微信支付”一栏会显示相关信息,在“开发-接口权限 ...

随机推荐

  1. day04-商家查询缓存03

    功能02-商铺查询缓存03 3.功能02-商铺查询缓存 3.6封装redis工具类 3.6.1需求说明 基于StringRedisTemplate封装一个工具列,满足下列需求: 方法1:将任意Java ...

  2. MySQL-存储引擎架构

    MySQL是一种分层体系结构的关系数据库. 一共有三层:客户(连接)层,Server层,存储引擎层. 简单理解就是这三层架构.官网的解释在这里.(这个部分建议看8.0的文档,8.0文档补充了架构图,5 ...

  3. intellij IDEA安装JDBC报错 No suitable driver found for jdbc:mysql://localhost:3306

    项目场景: 本地尝试使用intellij IDEA加载JDBC连接MySQL,尝试实现增删改查,本来想做一个小Demo. 问题描述 报错: java.lang.ClassNotFoundExcepti ...

  4. Node + Express 后台开发 —— 登录标识

    登录标识 系统通常只有登录成功后才能访问,而 http 是无状态的.倘若直接请求需要登录才可访问的接口,假如后端反复查询数据库,而且每个请求还得带上用户名和密码,这都是不很好. 作为前端,我们听过 c ...

  5. 2020-11-12:java中as-if-serial语义和happen-before语义有什么区别?

    福哥答案2020-11-12: as-if-serial语义单线程执行结果不被改变.happen-before语义正确同步的多线程执行结果不被改变.***这道题网上已经说烂了,就不必重复了.[happ ...

  6. 2020-11-02:go中,s:=make([]string,10);s=append(s,“test“);fmt.Println(s[0]),打印什么?

    福哥答案2020-11-02: 打印空字符串.s:=make([]string,10),s中已经有10个元素,append元素,s就有11个元素了.前10个元素没初始化,就是10个空字符串,最后1个字 ...

  7. es笔记一之es安装与介绍

    本文首发于公众号:Hunter后端 原文链接:es笔记一之es安装与介绍 首先介绍一下 es,全名为 Elasticsearch,它定义上不是一种数据库,是一种搜索引擎. 我们可以把海量数据都放到 e ...

  8. 【汇编】老师太hun

    老师只是随手发实验项目卡,从未提过实验报告的事情 可是 他却要在 复习周 一下子 收6次 实验报告 也不发资料,不说每次的时间点,不讲实验 这人心中有 学生 吗? 上课发 上个班直播的录播 一节课就发 ...

  9. vue横向导航条滚动到顶部固定同时瞄点对应内容(copy即用)

    这里监听window 的scroll实现一个页面滚动,导航菜单定位,内容联动的一个简单组件,结合一些案例,按需进行了整合,在此记录一下 效果图如下 具体实现如下 一.先创建一个NavigateTool ...

  10. 【Haxe】(一)VSCode 搭建 Haxe 开发环境

    前言 咱换工作啦! 新工作这边需要用到的开发语言是 Haxe,最近大概会写几篇笔记.Haxe 的介绍就不写了,打算记录点有用的学习内容,先从搭建开发环境开始吧! 当前适用版本: VSCode:Curr ...