一、概述
在视频会议、线上课堂、游戏直播等场景,屏幕共享是一个最常见的性能。屏幕共享就是对屏幕画面的实时共享,端到端次要有几个步骤:录屏采集、视频编码及封装、实时传输、视频解封装及解码、视频渲染。
一般来说,实时屏幕共享时,共享发动端以固定采样频率(个别 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 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 实现流程图示)
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
对象。
@Override
public 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 在桌面端上的利用,敬请期待!