Flutter 屏幕采集如何实现(提供示例demo)
在视频会议、线上课堂、游戏直播等场景,屏幕共享是一个最常见的功能。屏幕共享就是对屏幕画面的实时共享,端到端主要有几个步骤:录屏采集、视频编码及封装、实时传输、视频解封装及解码、视频渲染。
一般来说,实时屏幕共享时,共享发起端以固定采样频率(一般 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)的更多相关文章
- flutter屏幕适配
现在的手机品牌和型号越来越多,导致我们平时写布局的时候会在个不同的移动设备上显示的效果不同, 比如我们的设计稿一个View的大小是300px,如果直接写300px,可能在当前设备显示正常,但到了其他设 ...
- 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 ...
- iOS之ProtocolBuffer搭建和示例demo
这次搭建iOS的ProtocolBuffer编译器和把*.proto源文件编译成*.pbobjc.h 和 *.pbobjc.m文件时,碰到不少问题! 搭建pb编译器到时没有什么问题,只是在把*.pro ...
- 利用webuploader插件上传图片文件,完整前端示例demo,服务端使用SpringMVC接收
利用WebUploader插件上传图片文件完整前端示例demo,服务端使用SpringMVC接收 Webuploader简介 WebUploader是由Baidu WebFE(FEX)团队开发的一 ...
- 使用Nancy搭建简单的Http服务的示例demo
刚刚接触Nancy没几天,暂时还不会使用Nancy来做web开发,只是使用Nancy实现了一个简单的Http服务的Demo程序,实现对Post和Get请求的处理. Demo的示例代码地址如下:http ...
- Windows上配置Mask R-CNN及运行示例demo.ipynb
最近做项目需要用到Mask R-CNN,于是花了几天时间配置.简单跑通代码,踩了很多坑,写下来分享给大家. 首先贴上官方Mask R-CNN的Github地址:https://github.com/m ...
- 百度编辑器UEditor ASP.NET示例Demo 分类: ASP.NET 2015-01-12 11:18 346人阅读 评论(0) 收藏
在百度编辑器示例代码基础上进行了修改,封装成类库,只需简单配置即可使用. 完整demo下载 版权声明:本文为博主原创文章,未经博主允许不得转载.
- java原生实现屏幕设备遍历和屏幕采集(捕获)等功能
前言:本章中屏幕捕获使用原生java实现,屏幕图像显示采用javacv1.3的CanvasFrame 一.实现的功能 1.屏幕设备遍历 2.本地屏幕图像采集(也叫屏幕图像捕获) 3.播放本地图像(采用 ...
- asp.net core 上使用redis探索(3)--redis示例demo
由于是基于.net-core平台,所以,我们最好是基于IDistributedCache接口来实现.ASP.NET-CORE下的官方redis客户端实现是基于StackExchange的.但是官方提供 ...
- 微信红包功能(含示例demo)
开通支付权限 登录微信公众平台管理后台,找到“微信支付”一栏,进行开通会跳转到“微信支付商户平台”,根据提示提交相关证明,完成支付权限的开通开通之后,“微信支付”一栏会显示相关信息,在“开发-接口权限 ...
随机推荐
- mysql迁移:mysqldump导出表结构及数据
问题描述:有需要mysql某几张表的需求,某个数据库某几张表,导出先检查相应的数据库和表是否存在 数据泵用法:默认导出的是表结构以及表中的数据 mysqldump -uroot -p -S /data ...
- Vue中Key值的一些问题
1. Vue里面的key是一个特殊的变量,在元素当中是不体现出来的 2. 在解析成虚拟DOM的是,如果我们没有写key值,那么这个key就类似于下标 0 , 1 , 2 , 3.... 3. 使用列表 ...
- 手写 HashSet的底层 和 迭代器
1 package Test.CollectionIterator; 2 import java.util.Iterator; 3 public class MyHashSet2<E> i ...
- Node工程的依赖包管理方式
作者:京东零售 陈震 在前端工程化中,JavaScript 依赖包管理是非常重要的一环.依赖包通常是项目所依赖的第三方库.工具和框架等资源,它们能够帮助我们减少重复开发.提高效率并且确保项目可以正确的 ...
- Natasha V5.2.2.1 稳定版正式发布.
DotNetCore.Natasha.CSharp v5.2.2.1 使用 NMS Template 接管 CI 的部分功能. 取消 SourceLink.GitHub 的继承性. 优化几处内存占用问 ...
- React 富文本编辑 braft-editor
推荐一种react-富文本编辑器,braft-editor braft-editor的github:https://github.com/margox/braft-editor braft-edito ...
- [Opencv-C++] 1.1Opencv环境准备
Opencv环境准备 一.Opencv各版本下载 二.安装: 1.先下载OpenCV的源码: 2.解压到服务器任意目录: 3.进入源码目录 4.事先安装下列软件 5.进入到cmake 6.cmake编 ...
- 基于 Rainbond 的混合云管理解决方案
内容概要:文章探讨了混合云场景中的难点.要点,以及Rainbond平台在跨云平台的混合云管理方面的解决方案.包括通过通过统一控制台对多集群中的容器进行编排和管理,实现了对混合云中应用的一致性管理.文章 ...
- Unity快速接入bugly, 支持Unity2021
鹅厂提供的bugly官方demo工程打包后台也查不到日志,N年不更新(官方已经说不再维护),为此本人做了部分修改测试,提供一个快速接入工程的demo. Unity2021因为版本原因腾讯官方工程不能使 ...
- 2022-11-27:超过经理收入的员工。编写一个SQL查询来查找收入比经理高的员工。以下数据的结果输出是Joe,因为Joe是唯一挣得比经理多的雇员。 DROP TABLE IF EXISTS `em
2022-11-27:超过经理收入的员工.编写一个SQL查询来查找收入比经理高的员工.以下数据的结果输出是Joe,因为Joe是唯一挣得比经理多的雇员. DROP TABLE IF EXISTS `em ...