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,这也是需要开发者特别注意的地方;

相关文章
|
3月前
|
BI Linux 数据安全/隐私保护
忘了 iOS(iPad、IPhone) 设备上的「屏幕使用时间」密码怎么办?找回屏幕密码
忘了 iOS(iPad、IPhone) 设备上的「屏幕使用时间」密码怎么办?找回屏幕密码
115 0
|
6月前
|
iOS开发
iOS16.1系统由于一个系统弹窗无法取消,导致屏幕卡死无法关机问题及解决方案
iOS16.1系统由于一个系统弹窗无法取消,导致屏幕卡死无法关机问题及解决方案
795 0
|
安全 vr&ar 数据安全/隐私保护
iOS移动设备屏幕镜像电脑软件AirServer2023
AIrServer是一款ios投屏到mac的专用软件,可将iOS上的音频,视频,照片,幻灯片和镜像接收通过AIrPlay投射到Mac。AIrserver 7 mac版可以实现将手机上的媒体文件以及其他操作投射到电脑上进行操作。使用AIrServer,您现在可以从Mac,iOS,PC,
209 0
|
iOS开发
iOS开发 - 滑动控制屏幕亮度和系统音量(附加AVAudioPlayer基本用法和Masonry简单使用)
iOS开发 - 滑动控制屏幕亮度和系统音量(附加AVAudioPlayer基本用法和Masonry简单使用)
217 0
iOS开发 - 滑动控制屏幕亮度和系统音量(附加AVAudioPlayer基本用法和Masonry简单使用)
|
iOS开发 Perl
iOS 屏幕比例适配
iOS 屏幕比例适配
|
编解码 API iOS开发
iOS小技能:获取屏幕坐标的方式
使用iOS API获取在屏幕上的点击坐标
599 0
|
iOS开发
IOS锁定屏幕旋转
IOS锁定屏幕旋转
109 0