一、概述
在视频会议、线上课堂、游戏直播等场景,屏幕共享是一个最常见的性能。屏幕共享就是对屏幕画面的实时共享,端到端次要有几个步骤:录屏采集、视频编码及封装、实时传输、视频解封装及解码、视频渲染。
一般来说,实时屏幕共享时,共享发动端以固定采样频率(个别 8 - 15帧足够)抓取到屏幕中指定源的画面(包含指定屏幕、指定区域、指定程序等),通过视频编码压缩(应抉择放弃文本/图形边缘信息不失真的计划)后,在实时网络上以相应的帧率散发。
因而,屏幕采集是实现实时屏幕共享的根底,它的利用场景也是十分宽泛的。
现如今 Flutter 的利用越来越宽泛,纯 Flutter 我的项目也越来越多,那么本篇内容咱们次要分享的是 Flutter 的屏幕采集的实现。
二、实现流程
在具体介绍实现流程前,咱们先来看看原生零碎提供了哪些能力来进行屏幕录制。
1、iOS 11.0 提供了 ReplayKit 2 用于采集跨 App 的全局屏幕内容,但仅能通过控制中心启动;iOS 12.0 则在此基础上提供了从 App 内启动 ReplayKit 的能力。
2、Android 5.0 零碎提供了 MediaProjection 性能,只需弹窗获取用户的批准即可采集到全局屏幕内容。
咱们再看一下 Android / iOS 的屏幕采集能力有哪些区别。
1、iOS 的 ReplayKit 是通过启动一个 Broadcast Upload Extension 子过程来采集屏幕数据,须要解决主 App 过程与屏幕采集子过程之间的通信交互问题,同时,子过程还有诸如运行时内存最大不能超过 50M 的限度。
2、Android 的 MediaProjection 是间接在 App 主过程内运行的,能够很容易获取到屏幕数据的 Surface。
尽管无奈防止原生代码,但咱们能够尽量以起码的原生代码来实现 Flutter 屏幕采集。将两端的屏幕采集能力形象封装为通用的 Dart 层接口,只需一次部署实现后,就能开心地在 Dart 层启动、进行屏幕采集了。
接下来咱们别离介绍一下 iOS 和 Android 的实现流程。
1、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 notificationCFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), onBroadcastFinish, (CFStringRef)@"ZGFinishReplayKitBroadcastNotificationName", NULL, CFNotificationSuspensionBehaviorDeliverImmediately);// Handle stop broadcast notification from main app processstatic 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 实现流程图示)
2、Android
Android 的实现绝对 iOS 比较简单,在启动屏幕采集时,能够间接应用 Flutter 的 MethodChannel 在原生侧通过 MediaProjectionManager
弹出一个向用户申请屏幕采集权限的弹窗,收到确认后即可调用 MediaProjectionManager.getMediaProjection()
函数拿到 MediaProjection
对象。
须要留神的是,因为 Android 对权限治理日渐收紧,如果你的 App 的指标 API 版本 (Target SDK) 大于等于 29,也就是 Android Q (10.0) 的话,还须要额定启动一个前台服务。依据 Android Q 的迁徙文档显示,诸如 MediaProjection
等须要应用前台服务的性能,必须在独立的前台服务中运行。
首先须要本人实现一个继承 android.app.Service
类,在 onStartCommand
回调中调用上述的 getMediaProjection()
函数获取 MediaProjection
对象。
@Overridepublic int onStartCommand(Intent intent, int flags, int startId) { int resultCode = intent.getIntExtra("code", -1); Intent resultData = intent.getParcelableExtra("data"); String notificationText = intent.getStringExtra("notificationText"); int notificationIcon = intent.getIntExtra("notificationIcon", -1); createNotificationChannel(notificationText, notificationIcon); MediaProjectionManager manager = (MediaProjectionManager)getSystemService(Context.MEDIA_PROJECTION_SERVICE); MediaProjection mediaProjection = manager.getMediaProjection(resultCode, resultData); RequestMediaProjectionPermissionManager.getInstance().onMediaProjectionCreated(mediaProjection, RequestMediaProjectionPermissionManager.ERROR_CODE_SUCCEED); return super.onStartCommand(intent, flags, startId);}
而后还须要在 AndroidManifest.xml
中注册这个类。
<service android:name=".internal.MediaProjectionService" android:enabled="true" android:foregroundServiceType="mediaProjection"/>
而后在启动屏幕采集时判断零碎版本,如果运行在 Android Q 以及更高版本的零碎中,则启动前台服务,否则能够间接获取 MediaProjection
对象。
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)private void createMediaProjection(int resultCode, Intent intent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { service = new Intent(this.context, MediaProjectionService.class); service.putExtra("code", resultCode); service.putExtra("data", intent); service.putExtra("notificationIcon", this.foregroundNotificationIcon); service.putExtra("notificationText", this.foregroundNotificationText); this.context.startForegroundService(service); } else { MediaProjectionManager manager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE); MediaProjection mediaProjection = manager.getMediaProjection(resultCode, intent); this.onMediaProjectionCreated(mediaProjection, ERROR_CODE_SUCCEED); }}
紧接着,依据业务场景需要从屏幕采集 buffer 的消费者拿到 Surface
,例如,要保留屏幕录制的话,从 MediaRecoder
拿到 Surface,要录屏直播的话,可调用音视频直播 SDK 的接口获取 Surface。
有了 MediaProjection
和消费者的 Surface
,接下来就是调用 MediaProjection.createVirtualDisplay()
函数传入 Surface 来创立 VirtualDisplay
实例,从而获取到屏幕采集 buffer。
VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", width, height, 1,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, handler);
最初是完结屏幕采集,相比 iOS 简单的操作,Android 仅须要将 VirtualDisplay
和 MediaProjection
实例对象开释即可。
三、实战示例
上面为大家筹备了一个实现了 iOS/Android 屏幕采集并应用 Zego RTC Flutter SDK 进行推流直播的示例 Demo。
下载链接:https://github.com/zegoim/zego-express-example-screen-capture-flutter
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 在桌面端上的利用,敬请期待!