iOS 屏幕共享

简介: 前言:由于最近项目中需要使用到屏幕共享,所以对iOS屏幕共享进行了一番调研,在这里也分享下踩坑之路。
前言:由于最近项目中需要使用到屏幕共享,所以对iOS屏幕共享进行了一番调研,在这里也分享下踩坑之路。

屏幕共享简介

屏幕共享是把屏幕上的内容分享给其他人观看,这分为两项关键技术:

  • 屏幕内容采集:涉及到系统权限,以及需要系统提供采集屏幕内容的Api;(苹果提供的屏幕录制框架ReplayKit
  • 流媒体服务:通过采集到的视频流以及音频流推送给流媒体服务器广播给用户;(通常是以RTC的推流形式进行处理)

屏幕共享的应用场景:
手机游戏直播、客服指导、商务会议、教学白板等;

屏幕共享整体流程如下:

  • 触发录屏
  • 准备工作
  • 开始录屏
  • 处理数据流(音频、视频等)
  • ExtensionApp 数据共享到 MainApp中(或者直接对此数据进行操作)
  • 结束录屏

屏幕共享采集:

  • 添加Target
  • 创建Broadcast Upload Extension

  • 添加AppGroups

可以在苹果开发者官网申请AppGroupID,并且把相关的profile文件,证书相关联,也可以在Xcode中添加后,由Xcode自动生成;
添加AppGroups需要在主App中和拓展中都添加;

  • 添加完成后

添加完
最终项目中会存在两个文件,AppGroupID要保持一致;

到这里就可以开始处理屏幕共享了。由于涉及到进程之间(主App与拓展App之间)的通信问题,所以这里采用通知的方式来处理开始、结束等事件;屏幕共享采集数据进程通信目前采用NSUserDefaultSocket等方式,但是苹果扩展App有50M的内存限制在先,如果不需要帧数太高,可以使用NSUserDefault传输sampleBuffer。如果对屏幕共享要求很高(帧率高、分辨率高),可以直接在拓展App中直接上传流,或者使用socket。使用socket可以在一定程度上不依赖内存,但是需要处理帧堆积导致的内存爆增问题,可以避免扩展程序被系统强制KILL,但Socket有一定的不稳定性,需要额外处理断线以及网络异常等问题;

推荐的进程通信方式:

  • 事件通知:CFNotification;
  • 简单的值传递:NSUserDefault
  • 复杂的数据传递:Socket

开始屏幕共享

这个方法有不确定性,🤷不知道未来某天是否还能使用,能用就先用吧!苹果并没有给出一个明确的方法,只有一个控件可以调起屏幕共享弹窗,所以这里是以一种取巧的方式封装起来的方法,给App调用;
在调起屏幕共享后,到我们真正开启屏幕共享,还有一步用户确认操作,所以我们需要知道屏幕共享扩展程序的事件回调后,再去处理某些逻辑,才能真正形成闭环;

// 系统弹窗
- (RPSystemBroadcastPickerView *)systemPicker {
    if (!_systemPicker) {
        RPSystemBroadcastPickerView* picker =
        [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
        picker.showsMicrophoneButton = NO;
        picker.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
        _systemPicker = picker;
    }
    return _systemPicker;
}

- (void)launchBroadcaster API_AVAILABLE(ios(12.0)) {
    NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtPath:plugInPath error:nil];
    for (NSString *content in contents) {
        NSURL *url = [NSURL fileURLWithPath:plugInPath];
        NSBundle *bundle = [NSBundle bundleWithPath:[url URLByAppendingPathComponent:content].path];
        
        NSDictionary *extension = [bundle.infoDictionary objectForKey:@"NSExtension"];
        if (extension == nil) { continue; }
        NSString *identifier = [extension objectForKey:@"NSExtensionPointIdentifier"];
        if ([identifier isEqualToString:@"com.apple.broadcast-services-upload"]) {
            self.systemPicker.preferredExtension = bundle.bundleIdentifier;
            break;
        }
    }
    for (UIView *view in self.systemPicker.subviews) {
        UIButton *button = (UIButton *)view;
        [button sendActionsForControlEvents:UIControlEventAllTouchEvents];
    }
}

停止屏幕共享

停止屏幕共享方法是由ReplayKit扩展中的提供的,从MainApp中无法直接调用,可以通过通知的方式去调用ExtensionApp 的方法;

// MainApp 发送通知给 ExtensionApp, 扩展程序收到通知调用停止方法
// MainApp
- (void)stopBroadcaster {
    CFNotificationName notificationName = CFNotificationName(TScreenShareHostRequestStopNotification);
    CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}

// ExtensionApp
// 监听通知
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
                                    (__bridge const void *)(self),
                                    onHostRequestFinishBroadcast,
                                    (__bridge CFStringRef)ScreenShareHostRequestStopNotification,
                                    NULL,
                                    CFNotificationSuspensionBehaviorDeliverImmediately);

static void onHostRequestFinishBroadcast(CFNotificationCenterRef center,
                                void *observer,
                                CFStringRef name,
                                const void *object,
                                         CFDictionaryRef
                                         userInfo) {
    ScreenShareSampleHandler *self = (__bridge ScreenShareSampleHandler *)(observer);
    NSError *error = [NSError errorWithDomain:NSStringFromClass(self.class)
                                         code:0
                                     userInfo:@{
                                         NSLocalizedFailureReasonErrorKey:NSLocalizedString(@"您已停止屏幕共享。", nil)
                                     }];
    [self finishBroadcastWithError:error];
}

ExtensionApp事件回调

- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
    // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
    CFNotificationName notificationName = CFNotificationName(ScreenShareBroadcastStartedNotification);
    CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}

- (void)broadcastPaused {
    // User has requested to pause the broadcast. Samples will stop being delivered.
    CFNotificationName notificationName = CFNotificationName(ScreenShareBroadcastPausedNotification);
    CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}

- (void)broadcastResumed {
    // User has requested to resume the broadcast. Samples delivery will resume.
    CFNotificationName notificationName = CFNotificationName(ScreenShareBroadcastResumedNotification);
    CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}

- (void)broadcastFinished {
    // User has requested to finish the broadcast.
    CFNotificationName notificationName = CFNotificationName(ScreenShareBroadcastFinishedNotification);
    CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    //使用NSUserDefault
}

MainApp监听屏幕共享事件通知

// 屏幕共享开始
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
                                (__bridge const void *)(self),
                                onBroadcastStarted,
                                (__bridge CFStringRef)TScreenShareBroadcastStartedNotification,
                                NULL,
                                CFNotificationSuspensionBehaviorDeliverImmediately);
// 屏幕共享完成
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
                                (__bridge const void *)(self),
                                onBroadcastFinished,
                                (__bridge CFStringRef)TScreenShareBroadcastFinishedNotification,
                                NULL,
                                CFNotificationSuspensionBehaviorDeliverImmediately);
// 屏幕共享暂停
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
                                (__bridge const void *)(self),
                                onBroadcastPaused,
                                (__bridge CFStringRef)TScreenShareBroadcastPausedNotification,
                                NULL,
                                CFNotificationSuspensionBehaviorDeliverImmediately);
// 屏幕共享暂停
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
                                (__bridge const void *)(self),
                                onBroadcastResumed,
                                (__bridge CFStringRef)TScreenShareBroadcastResumedNotification,
                                NULL,
                                CFNotificationSuspensionBehaviorDeliverImmediately);
// 实现方法
static void onBroadcastStarted(CFNotificationCenterRef center,
                               void *observer,
                               CFStringRef name,
                               const void *object,
                               CFDictionaryRef
                               userInfo) {
}

static void onBroadcastFinished(CFNotificationCenterRef center,
                                void *observer,
                                CFStringRef name,
                                const void *object,
                                CFDictionaryRef
                                userInfo) {
}

static void onBroadcastPaused(CFNotificationCenterRef center,
                              void *observer,
                              CFStringRef name,
                              const void *object,
                              CFDictionaryRef
                              userInfo) {
}

static void onBroadcastResumed(CFNotificationCenterRef center,
                               void *observer,
                               CFStringRef name,
                               const void *object,
                               CFDictionaryRef
                               userInfo) {
}

流媒体服务:

这里大多都是使用第三方的服务,并且每家SDK的方法都大同小异;RTC屏幕共享方法有开始屏幕共享,停止屏幕共享,以及推流等方法;基本上集成上都有傻瓜式教程,就不在这里展开叙述;

总结:

iOS系统实现屏幕共享的功能太曲折了,就ReplayKit整体而言,对开发者并不是很友好。iOS版本之间兼容也是很头疼的,实现屏幕共享的细节还有很多,在屏幕共享采集后的处理也尤为关键,比如对视频帧和音频帧的处理;还有就是屏幕共享扩展的内存限制50M,这也是需要开发者特别注意的地方;

相关文章
iOS- 关于AVAudioSession的使用——后台播放音乐
iOS- 关于AVAudioSession的使用——后台播放音乐
447 0
iOS- 关于AVAudioSession的使用——后台播放音乐
|
7月前
|
iOS开发
iOS的推送通知,DeviceToken
iOS的推送通知,DeviceToken
93 2
|
iOS开发
IOS播放音乐
IOS播放音乐
41 0
|
iOS开发
ios 音乐后台播放
ios 音乐后台播放
54 0
|
存储 调度 iOS开发
iOS 推送通知(上)
iOS 推送通知(上)
600 0
iOS 推送通知(上)
|
缓存 应用服务中间件 API
iOS 推送通知(下)
iOS 推送通知
215 0
iOS 推送通知(下)
iOS11问题: 定位服务在iOS11系统上不能使用?
iOS11问题: 定位服务在iOS11系统上不能使用? Q:我刚刚用iOS11 SDK重新构建了应用程序,发现定位服务现在根本不起作用。 原因:A:因为苹果现在增加了一项新的隐私保护功能 NSLocationAlwaysAndWhenInUseUsageDeion, 并且原有的 NSLocationAlwaysUsageDeion 被降级为 NSLocationWhenInUseUsageDeion。
1164 0
|
iOS开发 流计算
iOS - MPMoviePlayer 视频播放
前言 MP_EXTERN_CLASS_AVAILABLE(3_2) NS_DEPRECATED_IOS(3_2, 9_0, "Use AVPlayerViewController in AVKit.
1024 0
|
iOS开发
iOS - AVPlayer 音视频播放
前言 NS_CLASS_AVAILABLE(10_7, 4_0) @interface AVPlayer : NSObject @available(iOS 4.0, *) public class AVPlayer : NSObject NS_C...
2079 1

热门文章

最新文章