1、应用内分享 即只能分享当前 App 的画面,该特性需要 iOS 13 及以上版本的操作系统才能支持。由于无法分享当前 App 之外的屏幕内容,因此适用于对隐私保护要求高的场景。
2、跨应用分享 基于苹果的 Replaykit 方案,能够分享整个系统的屏幕内容,但需要当前 App 额外提供一个 Extension 扩展组件,因此对接步骤也相对应用内分享要多一点。
iOS | Android | Mac OS | Windows | Electron | 微信小程序 | Chrome 浏览器 |
---|---|---|---|---|---|---|
✓ | ✓ | ✓ | ✓ | ✓ | × | ✓ |
应用内分享的方案非常简单,只需要调用 AVD SDK 提供的接口[self.mscreen publishScreen:screen_resolution_1080p fps:10] 并传入分辨率参数 即可。
由于屏幕分享的内容一般不会剧烈变动,所以设置较高的 FPS 并不经济,推荐10 FPS即可。
如果您要分享的屏幕内容包含大量文字,可以适当提高分辨率和码率设置。
最高码率(videoBitrate)是指画面在剧烈变化时的最高输出码率,如果屏幕内容变化较少,实际编码码率会比较低。
iOS 系统上的跨应用屏幕分享,需要增加 Broadcast Upload Extension 录屏进程以配合主 App 进程进行推流。Extension 录屏进程由系统在需要录屏的时候创建,并负责接收系统采集到屏幕图像
该指南主要通过demo集成屏幕共享相关的功能。 包括:建立屏幕共享、开启屏幕共享、关闭屏幕共享;ReplayKit 2仅支持iOS 12.0 以上共享系统屏幕。基本流程是添加ReplayKit扩展、SDK创建加入房间、使用Socket在宿主App(主工程,这里是baseVideo)和扩展ReplayKit程序之间进行视频数据(音频使用宿主APP中AVD SDK采集)和控制指令传输
1、 在您的工程中,新建一个 Broadcast Upload Extension 的 Target
2、设置Extension 中bitcode 为NO
3、将avd_sdk.framework
添加到Extension中并添加系统库 libc++.tbd
4、确认是否添加成功
5、在宿主app中添加avd_sdk.framework
6、在宿主App和Extension之间建立Socket连接,用于进程间发送视频帧数据,并且注册相同进程间通知用于处理宿主和extension直接相互传递消息
7、在宿主app初始化引擎或者初始化引擎成功回调中调用初始化Socket
xxxxxxxxxx
- (void)onInitResult:(AVDResult)result {
if (AVD_Success == result) {//初始化成功并设置相关参数 具体定义参考AVDEngine.h
[[AVDClientBufferSocketManager sharedManager] setupSocket];
}else {
}
}
8、注册Extension事件通知
xxxxxxxxxx
- (void)addUploaderEventMonitor {
//屏幕共享开始
[self registerForNotificationsWithIdentifier:@"broadcastStartedWithSetupInfo"];
//屏幕共享停止
[self registerForNotificationsWithIdentifier:@"broadcastFinished"];
//这里同时注册了纷发消息的通知,在宿主App中使用
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(broadcastInfo:) name:ScreenHoleNotificationName object:nil];
}
//注册进程间通知
- (void)registerForNotificationsWithIdentifier:(nullable NSString *)identifier {
CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
CFStringRef str = (__bridge CFStringRef)identifier;
CFNotificationCenterAddObserver(center,
(__bridge const void *)(self),
MyHoleNotificationCallback,
str,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
}
9、在Extension中SampleBuffer.m
文件中调用接口初始化Socket和发送屏幕流
xxxxxxxxxx
//
// SampleHandler.m
// uploadExtenion
//
// Created by 张启金 on 2020/3/31.
// Copyright © 2020年 Elliot. All rights reserved.
//
@interface SampleHandler ()
@property(nonatomic, assign)double nextFrameTime;
@end
@implementation SampleHandler
//屏幕开始直播时会回调这个方法,因此在此回调中初始化Socket并注册宿主App发送notification的监听
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter (), (__bridge const void *)(self), broadcastStopCallback,CFSTR("broadcastStop"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fishionBroadNotification:) name:@"FISHIONBROADCASTNOTIFICATION" object:nil];
[self sendNotificationForMessageWithIdentifier:@"broadcastStartedWithSetupInfo" userInfo:nil];
[[AVDSampleHandlerSocketManager sharedManager] setUpSocket];
self.nextFrameTime = 0;
}
void broadcastStopCallback (CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo){
[[NSNotificationCenter defaultCenter] postNotificationName:@"FISHIONBROADCASTNOTIFICATION" object:nil userInfo:nil];
}
- (void)broadcastPaused {
// User has requested to pause the broadcast. Samples will stop being delivered.
[self sendNotificationForMessageWithIdentifier:@"broadcastPaused" userInfo:nil];
}
- (void)broadcastResumed {
// User has requested to resume the broadcast. Samples delivery will resume.
[self sendNotificationForMessageWithIdentifier:@"broadcastResumed" userInfo:nil];
}
- (void)broadcastFinished {
// User has requested to finish the broadcast.
[self sendNotificationForMessageWithIdentifier:@"broadcastFinished" userInfo:nil];
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
CFSTR("broadcastUnpublish"),
NULL);
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"FISHIONBROADCASTNOTIFICATION" object:nil];
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVideo:{
double currentTime = [[NSDate date] timeIntervalSince1970]*1000;
if (currentTime < self.nextFrameTime) {
return;
}
self.nextFrameTime = currentTime + frameTime;
// Handle video sample buffer
[[AVDSampleHandlerSocketManager sharedManager] sendVideoBufferToHostApp2:sampleBuffer];
}
break;
case RPSampleBufferTypeAudioApp:
// Handle audio sample buffer for app audio
break;
case RPSampleBufferTypeAudioMic:
// Handle audio sample buffer for mic audio
break;
default:
break;
}
}
- (void)sendNotificationForMessageWithIdentifier:(nullable NSString *)identifier userInfo:(NSDictionary *)info {
CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
CFDictionaryRef userInfo = (__bridge CFDictionaryRef)info;
BOOL const deliverImmediately = YES;
CFStringRef identifierRef = (__bridge CFStringRef)identifier;
CFNotificationCenterPostNotification(center, identifierRef, NULL, userInfo, deliverImmediately);
}
- (void)fishionBroadNotification:(NSNotification *)noti{
NSError *error = [NSError errorWithDomain:@"直播已经停止" code:-1088 userInfo:@{NSLocalizedDescriptionKey:@"您主动停止了直播"}];
[self finishBroadcastWithError:error];
}
@end
10、ReplayKit 采集到的屏幕视频数据通过 processSampleBuffer:withType:给用户,忽略音频数据回调(我们使用SDK音频采集),将视频数据通过Socket发送到宿主App,然后再通过SDK自定义视频数据进行发送,因为横竖屏切换时屏幕采集帧较多会超过Extension内存限制50m的要求,所以需要在发生帧时做一个限制逻辑
xxxxxxxxxx
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVideo:{
double currentTime = [[NSDate date] timeIntervalSince1970]*1000;
if (currentTime < self.nextFrameTime) {
return;
}
self.nextFrameTime = currentTime + frameTime;
// Handle video sample buffer
[[AVDSampleHandlerSocketManager sharedManager] sendVideoBufferToHostApp2:sampleBuffer];
}
break;
case RPSampleBufferTypeAudioApp:
// Handle audio sample buffer for app audio
break;
case RPSampleBufferTypeAudioMic:
// Handle audio sample buffer for mic audio
break;
default:
break;
}
}
11、应用层开启屏幕共享
xxxxxxxxxx
//系统级别的屏幕共享点击界面上共享按钮是调用
- (IBAction)startRecord:(UIButton *)sender {
if (@available(iOS 12, *)) {//ios12 系统级别录制
if (!self.broadPickerView) {
self.broadPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(-100,-100, 100, 100)];
self.broadPickerView.preferredExtension = @"cn.tee3.baseVideo.baseVideoExtension";//这里的id必须和Extension中的Bundle identiter一致
self.broadPickerView.showsMicrophoneButton = NO;
[self.view addSubview:self.broadPickerView];
}
for (UIView *subView in self.broadPickerView.subviews) {
if ([subView isKindOfClass:[UIButton class]]) {
[(UIButton *)subView sendActionsForControlEvents:UIControlEventAllEvents];
break;
}
}
}else{
NSLog(@"ios12以下暂不支持系统级别的录制");
}
}
Caution
苹果在 iOS 12.0 中增加了 RPSystemBroadcastPickerView
可以从应用中弹出启动器供用户确认启动屏幕分享,到目前为止, RPSystemBroadcastPickerView
尚不支持自定义界面,也没有官方的唤起方法。
TRTCBroadcastExtensionLauncher 的原理就是遍历 RPSystemBroadcastPickerView
的子 View 寻找 UIButton 并触发了其点击事件。
但该方案不被苹果官方推荐,并可能在新一轮的系统更新中失效
12、停止屏幕分享
xxxxxxxxxx
- (IBAction)stopRecord:(UIButton *)sender {
//这个是为了通知extension结束屏幕共享采集
CFNotificationCenterRef notification = CFNotificationCenterGetDarwinNotifyCenter ();
CFNotificationCenterPostNotification(notification, CFSTR("broadcastStop"), NULL,NULL, YES);
[self.mscreen unpublishScreen];
}
13、用户主动关闭房间时需要通知Extension停止屏幕直播
xxxxxxxxxx
CFNotificationCenterRef notification = CFNotificationCenterGetDarwinNotifyCenter ();
CFNotificationCenterPostNotification(notification, CFSTR("broadcastStop"), NULL,NULL, YES);
14、异常处理,比如用户主动杀掉应用进程需要监听应用程序被杀死然后通知Extension停止屏幕直播
xxxxxxxxxx
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil];
-(void)applicationWillTerminate{
//
CFNotificationCenterRef notification = CFNotificationCenterGetDarwinNotifyCenter ();
CFNotificationCenterPostNotification(notification, CFSTR("broadcastStop"), NULL,NULL, YES);
NSLog(@"程序被杀死");
}
Caution
注意:进程间通讯文档中只是示列代码,可以替换其他进程通讯的方法都行