iOS屏幕共享

作者: 新生代农民工No1 | 来源:发表于2021-07-10 21:18 被阅读0次

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

    屏幕共享简介

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

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

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

    屏幕共享采集:

    • 添加Target

    • 创建Broadcast Upload Extension

    • 添加AppGroups

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

    • 添加完成后
      添加完

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

    到这里就可以开始处理屏幕共享了,但是由于涉及到进程之间(主App与拓展App之间)的通信问题,所以这里采用通知的方式来处理开始、结束等事件;对于屏幕共享采集数据目前有采用NSUserDefaultSocket等方式,但是鉴于苹果扩展App有50M的内存限制,如果不需要帧数太高,可以使用NSUserDefault传输sampleBuffer。但是如果对屏幕共享要求很高(帧率高、分辨率高),使用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屏幕共享

        本文链接:https://www.haomeiwen.com/subject/ldrsyltx.html