本文作者:clc
0x00 引言
在客户端开发的生涯里,有时会遇到这样一些场景,须要对用户在利用内的操作做进行屏幕录制,甚至是零碎层级的跨利用屏幕录制来实现某种非凡需要,例如在线监考、利用问题反馈、游戏直播等。
苹果提供了 ReplayKit Framework 来满足这些需要,目前云音乐 LOOK 直播客户端内就是采纳这个零碎框架来实现跨利用录屏直播的。
0x01 ReplayKit 简史
ReplayKit 的故事要从 iOS 9 说起。
iOS 9 提供了 ReplayKit Extension 进行利用内的录制以及利用声音采集。次要波及两个类:一个是 RPScreenRecorder,作为录制 Task 的管理者;另一个是 RPPreviewViewController,录制状态的视觉反馈。利用内间接调用 ReplayKit API 来管制开始与进行,在 Extension 中将捕捉的音频 / 视频流推向服务器,这就是利用内录制(In-App Boardcast)。
WWDC 课程:Going Social with ReplayKit and Game Center
在 iOS 11 中,ReplayKit 提供了更弱小的能力:将零碎作为一个整体进行直播。用户在控制中心内开启屏幕录制后,ReplayKit2 Extension 能够获取到整个零碎级的屏幕画面、以及设施所产生的所有音频,实现跨利用录屏 (iOS System Boardcast),同时 ReplayKit 也提供了麦克风采集。这种零碎级的直播在利用间切换时也不会进行。(留神揭示你的用户爱护好本人的隐衷!)。
音视频数据仍然是在 Extension 内间接获取并上传至服务器,文章的前面将重点聊一下这块内容在 LOOK 直播中的实际。
WWDC 课程:Live Screen Broadcast with ReplayKit
在 iOS 15 之后,ReplayKit 提供了 Loop Buffer 性能,依据 WWDC 的形容,在利用内开启 Loop Buffer 后 ReplayKit 会创立一个最长 15 秒的 Buffer 并开始继续录制,利用能够随时调用 API 将这一部分导出(对直播利用而言,这能够用来随时截获精彩霎时,很酷)。这一部分不须要创立 Extension,间接在利用内实现。
WWDC 课程:Discover rolling clips with ReplayKit
0x02 零碎级录制的流程简述
- 用户在 App 内做好前置筹备(例如:开播)。
- 用户从控制中心启动 ReplayKit。
- ReplayKit Extension 开始承受录屏视频流、App 音频流,同时开始向服务器推流。
- 用户被动从控制中心敞开录制,流程完结。
0x03 创立并接入 ReplayKit Extension
上面咱们在 Xcode 14.1 中演示一下如何接入 ReplayKit。
首先,在 Xcode 中新建一个 Target,抉择 Broadcast Upload Extension。
因为零碎录制不须要 UI Extension,所以勾销勾选 Include UI Extension 这一默认选项。
生成的文件很简略,只有一对 SampleHandler.h 和 SampleHandler.m。
在 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,长按控制中心里的屏幕录制开关(如果没有,则须要在“设置”=>“控制中心”中手动增加。
而后选中对应利用,就能够开始了!
0x04 LOOK 直播内的实际
1.Extension 中的性能集成
在 Extension 中,除了音视频解决和推流性能外,其余性能应该尽量少,来保障内存在一个稳固值,咱们集成的几个次要的能力是:
- 网络申请能力,次要负责直播心跳保活,保障在宿主 App 被杀死后,直播仍然能失常进行。以及一系列须要依据接口进行的判断和校验。
- IM 长连贯能力,确保风控能够及时通过 IM 音讯来中断有危险的直播内容,在一些场景下接管房间用户的弹幕与送礼音讯。
- 本地 Push(可选开关,由宿主 App 管制),作为主播与观众进行弹幕 / 礼物互动的次要媒介,也是风控正告等告诉的能力的提醒伎俩。在灵动岛推出后,能够抉择将这部分能力向灵动岛迁徙。
- Local Socket 加上 AppGroup 两者单干实现了宿主 App 和 Extension 间的数据同步,在下一章节会对数据通信开展解说。
通过测试与线上验证,目前这些性能的总内存占用大概在 20Mb 左右,占 Extension 内存下限大概一半,但不可避免的是在小局部状况下零碎会将 Extension 线程阻塞,产生音视频帧内存挤压超过阈值,导致 Extension 被杀死。
2. 宿主 App 与 Extension 间的通信
App 间的过程间通信形式次要有两种,一种是通过创立 Local Socket 互发数据。
另一种是通过 AppGroup 进行资源共享(简略的说,通过 AppGroup,宿主利用和 Extension 能够拜访到同一份 UserDefault)。
在技术计划选型时,咱们思考过独自应用 Local Socket 或是独自应用 AppGroup 来实现通信,但发现两者都有弊病:
- 仅应用 Local Socket 通信时,思考到宿主和 Extension 各自的重开场景以及局部数据须要长久化,数据同步会较为简单。
- 仅应用 AppGroup 时,单方须要通过轮询来进行数据同步,蕴含文件读写操作,有效率问题。
于是最终抉择两者并用,一方改写 UserDefault 数据后,通过 Local Socket 告诉另一方,进行同步。
3. 疏导用户关上录制
ReplayKit2 的开启流程比拟繁琐,对用户不敌对:回顾上文,开启屏幕录制须要用户中断在利用中的操作流程,到控制中心长按“屏幕录制”按钮,选中你的利用,点击开始;如果用户还没有向控制中心增加“屏幕录制”按钮,则须要疏导用户到“设置”中增加。
LOOK 直播在设计开播流程时,首先想到的是搁置一个疏导视频进行疏导:通过 Local Socket 轮询 Extension 状态,如果还没有开启,则搁置一块播放区域,循环播放开启疏导视频。这样尽管和用户讲明确了如何开启,但还是无奈防止简单的流程,咱们有没有方法在流程上简化用户操作呢?
答案是有,在 iOS 13 开始,Replaykit2 提供了 RPSystemBroadcastPickerView 零碎控件,通过点击控件,用户能够间接唤起本应由长按“屏幕录制”唤起的零碎界面,并只蕴含你指定的选项了
这样,整个流程就变成线性的了,不须要用户再来到你的开播流程去操作系统控制中心。
那么问题完结了吗?还没有,RPSystemBroadcastPickerView 是一个零碎控件,出于隐衷爱护的前提,零碎并不想让这个控件能够被随便的批改款式。在不批改款式的状况下,它长这样:
遗憾的是,这个款式和 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 的点击行为包装进去了,能够解决成主动唤起或是由自定义控件唤起了。
让咱们来看一下实现的成果
4. 隐衷爱护
因为 Replaykit 是零碎级的录制,用户在进行直播时所有的操作都会被观众看到,如果主播操作不当,一些比拟隐衷的信息(例如短信验证码、通讯录、聊天记录和相册)就会被透露进来,这是主播和平台方都不心愿产生的。
在 LOOK 直播内,咱们提供了“隐衷模式”这一性能。在隐衷模式下,零碎提供的视频帧将被舍弃,推流组件从一张本地图片中取帧,并继续向服务器推送,这样观众端就看不到主播的隐衷内容操作了。
隐衷模式只针对画面,音频方面,由主播本人管制是否静音(局部主播须要在隐衷模式下放弃观众互动,防止直播间人数散失)。
咱们无奈辨认利用外的用户操作和界面停留,只能文字揭示用户留神。而在利用内,咱们能够人工划分出哪些界面是波及用户隐衷的,例如直播间背景抉择页,须要在利用内拜访零碎相册。
所以咱们设计了两种触发形式,从利用的角度来看,分为被动触发和被动触发。被动触发指的是主播在利用内进入蕴含隐衷信息的界面时,利用被动进入隐衷模式,退出界面时敞开隐衷模式。被动触发则是由主播操作直播间内的“开启隐衷模式”按钮来开启和敞开隐衷模式。
0x05 艰难与挑战
正如前文所说,iOS Extension 中有 50Mb 的最大内存阈值,如果超过了,将会被零碎发出。如果因为频繁达到内存阈值而导致 Extension 被零碎强制敞开回收就得失相当了,所以对于 50 Mb 的边界状况就必须小心应答。
开发过程中,因为模拟器中没有控制中心,咱们只能用真机设备开发调试。因为 Xcode 的断点优化并不好,在开发过程中常常会遇到断点导致过程阻塞,引发内存超过阈值的状况,排查一些偶现的问题非常苦楚,所以须要做好 Debug 日志打印,确保在内存超限的状况下也能有足够的日志来剖析问题。
内存限度对音视频解决也是一个挑战。如果网络不佳,推流阻塞,这时对音视频帧的生产速度远不迭零碎吐帧的生产速度,编码后的音视频数据无奈及时耗费,很容易就会达到内存上线。因而团队中负责音视频解决及推流开发的小伙伴要留神进行内存监控,在内存达到一个危险值的时候,及时舍弃一部分数据来爱护整体的内存使用量远离临界值,防止过程被杀死。
对于 Extension 内的内存管制没有自信的团队,能够思考将 Extension 中获取到的零碎音频、视频帧通过 Local Socket 形式将数据发送至宿主 App 内,由在后盾保活的宿主进行音视频解决及推流等操作。宿主保活的状况下,心跳、本地 Push、IM 长连贯 都能够在宿主 App 中实现,Extension 中仅保留视频数据编码一项能力,进一步压低内存开销。
0x06 注意事项
- 尽量管制内存占用,最好永远不要碰到 50 Mb 导致 Extension 被回收。
- 在不同零碎版本中,回调吐出的音视频帧格局有差别,留神兼容。
- 调用 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 文档:https://developer.apple.com/documentation/replaykit
WWDC 2021 Loop Buffer https://developer.apple.com/videos/play/wwdc2021/10101/
WWDC 2018 Screen Broadcast https://developer.apple.com/videos/play/wwdc2018/601
WWDC 2015 In-App Boardcast https://developer.apple.com/videos/play/wwdc2015/605
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!