本文作者: clc

0x00 引言

在客户端开发的生涯里,有时会遇到这样一些场景,需求对用户在运用内的操作做进行屏幕录制,乃至是体系层级的跨运用屏幕录制来完成某种特殊需求,例如在线监考、运用问题反应、游戏直播等。
苹果供给了 ReplayKit Framework 来满足这些需求,目前云音乐 LOOK 直播客户端内便是采用这个体系框架来完成跨运用录屏直播的。

0x01 ReplayKit简史

ReplayKit 的故事要从 iOS 9 说起。

iOS ReplayKit 与 屏幕录制

iOS 9 供给了 ReplayKit Extension 进行运用内的录制以及运用声响收集。首要触及两个类:一个是 RPScreenRecorder,作为录制 Task 的管理者;另一个是 RPPreviewViewController,录制状况的视觉反应。运用内直接调用 ReplayKit API 来操控开端与停止,在 Extension 中将捕获的音频/视频流推向服务器,这便是运用内录制(In-App Boardcast)。
WWDC 课程:Going Social with ReplayKit and Game Center

iOS ReplayKit 与 屏幕录制

在 iOS 11 中,ReplayKit 供给了更强大的才能:将体系作为一个全体进行直播。用户在操控中心内敞开屏幕录制后,ReplayKit2 Extension 能够获取到整个体系级的屏幕画面、以及设备所发生的一切音频,完成跨运用录屏(iOS System Boardcast),同时 ReplayKit 也供给了麦克风收集。这种体系级的直播在运用间切换时也不会停止。(留意提醒你的用户维护好自己的隐私!)。
音视频数据仍然是在 Extension 内直接获取并上传至服务器,文章的后边将重点聊一下这块内容在 LOOK 直播中的实践。
WWDC 课程:Live Screen Broadcast with ReplayKit

iOS ReplayKit 与 屏幕录制

在 iOS 15之后,ReplayKit 供给了 Loop Buffer 功用,根据 WWDC 的描绘,在运用内敞开 Loop Buffer 后 ReplayKit 会创立一个最长 15 秒的 Buffer 并开端继续录制,运用能够随时调用 API 将这一部分导出(对直播运用而言,这能够用来随时截获精彩瞬间,很酷)。这一部分不需求创立 Extension,直接在运用内完成。
WWDC 课程:Discover rolling clips with ReplayKit

0x02 体系级录制的流程简述

  1. 用户在 App 内做好前置预备(例如:开播)。
  2. 用户从操控中心启动 ReplayKit。
  3. ReplayKit Extension 开端承受录屏视频流、App音频流,同时开端向服务器推流。
  4. 用户自动从操控中心封闭录制,流程结束。

iOS ReplayKit 与 屏幕录制

0x03 创立并接入ReplayKit Extension

下面咱们在 Xcode 14.1 中演示一下如何接入 ReplayKit。
首要,在 Xcode 中新建一个 Target,挑选 Broadcast Upload Extension。

iOS ReplayKit 与 屏幕录制

因为体系录制不需求 UI Extension,所以取消勾选 Include UI Extension 这一默认选项。

iOS ReplayKit 与 屏幕录制

生成的文件很简略,只要一对 SampleHandler.h 和 SampleHandler.m。

iOS ReplayKit 与 屏幕录制

在 SampleHandler.m 中,包含了录制事件的各种回调办法。

- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. 
}
- (void)broadcastPaused {
// User has requested to pause the broadcast. Samples will stop being delivered.
}
- (void)broadcastResumed {
// User has requested to resume the broadcast. Samples delivery will resume.
}
- (void)broadcastFinished {
// User has requested to finish the broadcast.
}

接下来便是接纳体系的音视频帧回调了,在这里对音视频帧进行处理和推流就能够了。其中,体系供给的音视频帧一共分为三类:

  • RPSampleBufferTypeVideo:体系视频帧,包含了整个屏幕的视频内容,无任何删减。
  • RPSampleBufferTypeAudioApp:体系内录音频帧,包含了体系实施播映的声响。
  • RPSampleBufferTypeAudioMic:麦克风音频帧,用户翻开了麦克风录制按钮后开端回调。

回调办法如下:

// 音视频回调    
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:
            // Handle video sample buffer
            break;
        case RPSampleBufferTypeAudioApp:
            // Handle audio sample buffer for app audio
            break;
        case RPSampleBufferTypeAudioMic:
            // Handle audio sample buffer for mic audio
            break;
        default:
            break;
    }
}

到此整个框架就搭建完成了,接下来运行 Extension,长按操控中心里的屏幕录制开关(假如没有,则需求在“设置”=>“操控中心”中手动添加。

iOS ReplayKit 与 屏幕录制

然后选中对应运用,就能够开端了!

iOS ReplayKit 与 屏幕录制

0x04 LOOK 直播内的实践

1.Extension 中的功用集成 在 Extension 中,除了音视频处理和推流功用外,其他功用应该尽量少,来保证内存在一个稳定值,咱们集成的几个首要的才能是:

  1. 网络请求才能,首要担任直播心跳保活,保证在宿主App被杀死后,直播仍然能正常进行。以及一系列需求根据接口进行的判别和校验。
  2. IM 长连接才能,保证风控能够及时经过 IM 音讯来中断有风险的直播内容,在一些场景下接纳房间用户的弹幕与送礼音讯。
  3. 本地 Push(可选开关,由宿主 App 操控),作为主播与观众进行弹幕/礼物互动的首要媒介,也是风控警告等告诉的才能的提示手法。在灵动岛推出后,能够挑选将这部分才能向灵动岛迁移。
  4. Local Socket 加上 AppGroup 两者合作完成了宿主 App 和 Extension 间的数据同步,在下一章节会对数据通讯展开讲解。

iOS ReplayKit 与 屏幕录制

经过测验与线上验证,目前这些功用的总内存占用大约在 20Mb 左右,占 Extension 内存上限大约一半,但不可防止的是在小部分状况下体系会将 Extension 线程堵塞,发生音视频帧内存挤压超越阈值,导致 Extension 被杀死。

2.宿主App与 Extension 间的通讯

App间的进程间通讯办法首要有两种,一种是经过创立 Local Socket 互发数据。

iOS ReplayKit 与 屏幕录制

另一种是经过 AppGroup 进行资源共享(简略的说,经过 AppGroup,宿主运用和 Extension 能够拜访到同一份 UserDefault)。

iOS ReplayKit 与 屏幕录制

在技能方案选型时,咱们考虑过独自运用 Local Socket 或是独自运用 AppGroup 来完成通讯,但发现两者都有坏处:

  1. 仅运用 Local Socket 通讯时,考虑到宿主和 Extension 各自的重开场景以及部分数据需求耐久化,数据同步会较为复杂。

  2. 仅运用 AppGroup 时,两边需求经过轮询来进行数据同步,包含文件读写操作,有效率问题。

所以最终挑选两者并用,一方改写 UserDefault 数据后,经过 Local Socket 告诉另一方,进行同步。

iOS ReplayKit 与 屏幕录制

**3.引导用户翻开录制 **

ReplayKit2 的敞开流程比较繁琐,对用户不友好:回忆上文,敞开屏幕录制需求用户中断在运用中的操作流程,到操控中心长按“屏幕录制”按钮,选中你的运用,点击开端;假如用户还没有向操控中心添加“屏幕录制”按钮,则需求引导用户到“设置”中添加。

LOOK 直播在设计开播流程时,首要想到的是放置一个引导视频进行引导:经过 Local Socket 轮询 Extension 状况,假如还没有敞开,则放置一块播映区域,循环播映敞开引导视频。这样尽管和用户讲理解了如何敞开,但仍是无法防止复杂的流程,咱们有没有办法在流程上简化用户操作呢?

答案是有,在 iOS 13 开端,Replaykit2 供给了 RPSystemBroadcastPickerView 体系控件,经过点击控件,用户能够直接引发本应由长按“屏幕录制”引发的体系界面,并只包含你指定的选项了

iOS ReplayKit 与 屏幕录制

这样,整个流程就变成线性的了,不需求用户再离开你的开播流程去操作体系操控中心。

那么问题结束了吗?还没有, RPSystemBroadcastPickerView 是一个体系控件,出于隐私维护的前提,体系并不想让这个控件能够被随意的修改款式。在不修改款式的状况下,它长这样:

iOS ReplayKit 与 屏幕录制

惋惜的是,这个款式和 LOOK 直播的开播界面视觉格格不入。经过剖析层级,发现这是一个 UIView 上带了一个 UIControl,所以咱们能够经过遍历 subviews 并传递一个事件的办法自动触发 touchUpInside 来弹起体系的录制入口。

if (@available(iOS 12.0, *)) {
    // 创立一个按钮
    RPSystemBroadcastPickerView *picker = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
    //指定要翻开的录制选项
    NSString *bundleId = [NSBundle thisBundle_bundleId];
    picker.preferredExtension = [bundleId stringByAppendingString:@".broadcast"];
    picker.showsMicrophoneButton = YES;
    //遍历找到按钮,点他!
    for (id subview in picker.subviews) {
        if ([subview isKindOfClass:[UIButton class]]) {
            [(UIButton *)subview sendActionsForControlEvents:UIControlEventTouchUpInside];
        }
    }
}

这样咱们就将 RPSystemBroadcastPickerView 的点击行为包装出来了,能够处理成自动引发或是由自定义控件引发了。

让咱们来看一下完成的作用

iOS ReplayKit 与 屏幕录制

4.隐私维护

因为 Replaykit 是体系级的录制,用户在进行直播时一切的操作都会被观众看到,假如主播操作不当,一些比较隐私的信息(例如短信验证码、通讯录、聊天记录和相册)就会被泄漏出去,这是主播和渠道方都不希望发生的。

在 LOOK 直播内,咱们供给了“隐私方式”这一功用。在隐私方式下,体系供给的视频帧将被舍弃,推流组件从一张本地图片中取帧,并继续向服务器推送,这样观众端就看不到主播的隐私内容操作了。

iOS ReplayKit 与 屏幕录制

隐私方式只针对画面,音频方面,由主播自己操控是否静音(部分主播需求在隐私方式下坚持观众互动,防止直播间人数流失)。

咱们无法识别运用外的用户操作和界面停留,只能文字提醒用户留意。而在运用内,咱们能够人工划分出哪些界面是触及用户隐私的,例如直播间背景挑选页,需求在运用内拜访体系相册。

所以咱们设计了两种触发办法,从运用的视点来看,分为自动触发和被迫触发。自动触发指的是主播在运用内进入包含隐私信息的界面时,运用自动进入隐私方式,退出界面时封闭隐私方式。被迫触发则是由主播操作直播间内的“敞开隐私方式”按钮来敞开和封闭隐私方式。

0x05 困难与应战

正如前文所说,iOS Extension 中有 50Mb 的最大内存阈值,假如超越了,将会被体系收回。假如因为频频到达内存阈值而导致 Extension 被体系强制封闭收回就得不偿失了,所以关于 50 Mb 的边界状况就必须小心应对。

开发过程中,因为模拟器中没有操控中心,咱们只能用真机设备开发调试。因为 Xcode 的断点优化并不好,在开发过程中经常会遇到断点导致进程堵塞,引发内存超越阈值的状况,排查一些偶现的问题非常痛苦,所以需求做好 Debug 日志打印,保证在内存超限的状况下也能有满足的日志来剖析问题。

内存约束对音视频处理也是一个应战。假如网络欠安,推流堵塞,这时对音视频帧的消费速度远不及体系吐帧的生产速度,编码后的音视频数据无法及时耗费,很容易就会达到内存上线。因此团队中担任音视频处理及推流开发的小伙伴要留意进行内存监控,在内存达到一个风险值的时候,及时舍弃一部分数据来维护全体的内存运用量远离临界值,防止进程被杀死。

iOS ReplayKit 与 屏幕录制

关于 Extension 内的内存操控没有自傲的团队,能够考虑将 Extension 中获取到的体系音频、视频帧经过 Local Socket 办法将数据发送至宿主 App 内,由在后台保活的宿主进行音视频处理及推流等操作。宿主保活的状况下,心跳、本地 Push、IM长连接 都能够在宿主 App 中完成, Extension 中仅保存视频数据编码一项才能,进一步压低内存开销。

iOS ReplayKit 与 屏幕录制

0x06 留意事项

  1. 尽量操控内存占用,最好永久不要碰到 50 Mb 导致 Extension 被收回。

  2. 在不同体系版别中,回调吐出的音视频帧格局有差异,留意兼容。

  3. 调用 finishBroadcastWithError: 自动结束录制时,要设置好 NSError userInfo 中的 NSLocalizedFailureReasonErrorKey,保证在体系alert中能正确的告知用户结束原因。

     - (void)networkingErrorNotificationHandler {
         NSError *error = [NSError errorWithDomain:@"replaykitDomin" code:1234 userInfo:@{NSLocalizedFailureReasonErrorKey : @"网络无法连接,请从头敞开屏幕录制"}];
         [self finishBroadcastWithError:error];
     }
    

0x07 结语

ReplayKit 面世已经多年,从开始的运用内录制到体系屏幕录制,再到 Loop Buffer 滚动剪辑,功用在不断的添加。但出于隐私维护的初衷,苹果对敞开录制行为的设置仍然繁琐,在用户交互方面必须要做好引导,降低用户学习本钱。

最后,祝咱们在完成相关功用时,不被 50 Mb 内存上线和 Extension 的调试困难绊倒,优雅的完成屏幕录制功用。

相关常识传送门:
Apple 文档:developer.apple.com/documentati…
WWDC 2021 Loop Buffer developer.apple.com/videos/play…
WWDC 2018 Screen Broadcast developer.apple.com/videos/play…
WWDC 2015 In-App Boardcast developer.apple.com/videos/play…

本文发布自网易云音乐技能团队,文章未经授权禁止任何方式的转载。咱们终年招收各类技能岗位,假如你预备换工作,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!