++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

前面兩篇文章:
iOS端屏幕錄制(replaykit)調(diào)研
iOS端屏幕錄制Replaykit項目實(shí)踐
已經(jīng)對iOS端實(shí)現(xiàn)屏幕錄制的調(diào)研結(jié)果和簡單實(shí)踐進(jìn)行了概述,本篇開始將分別對iOS9、iOS10、iOS11、iOS12系統(tǒng)上具體實(shí)踐記錄一下,便于分享和自己查看。
相比于安卓端,iOS端的屏幕錄制發(fā)展太慢了,并且對開發(fā)者的需求滿足總是延遲很大,就像其他功能一樣,這也許就是蘋果逐漸喪失他的競爭力的原因。本文將對iOS端使用replaykit在各個系統(tǒng)版本中實(shí)現(xiàn)細(xì)節(jié)進(jìn)行描述。
iOS9:
對于iOS9的replaykit功能介紹可以參考官方wwdc視頻:支持錄制音頻、視頻,還可以增加語音旁白評論等其他額外的定制化東西。對于錄制的內(nèi)容,用戶可以回訪、剪輯或者通過社交媒體軟件分享出去。
ReplayKit records the audio and visuals of your running application. It also allows you to use this to add voice commentary and so they can make their recordings more personal or just to provide additional context. It allows your users to play back, scrub and trim their recordings and finally share their recordings to their favorite social networks and video destination sites.
啟動錄制使用接口:
[[RPScreenRecorder sharedRecorder] startRecordingWithMicrophoneEnabled:YES handler:^(NSError * _Nullable error) {
;
}];
注意:
- 使用 [RPScreenRecorder sharedRecorder] 啟動錄制,會首先請求用戶同意使用攝像頭和麥克風(fēng),主要考慮用戶的隱私和權(quán)限,如果用戶拒絕了,將無法進(jìn)行錄制。
- 錄制的內(nèi)容不會包含系統(tǒng)的UI,比如上方導(dǎo)航欄;
- 錄制的內(nèi)容會經(jīng)過音視頻編碼,而不是原始的yuv或pcm數(shù)據(jù);
- 錄制的內(nèi)容無法直接查看,必須通過RPPreviewViewController才能查看預(yù)覽,或者分享,或者保存到本地相冊中。而這個RPPreviewViewController在停止錄制的接口回調(diào)中才能獲取,也就是說,只有停止錄制之后才能通過RPPreviewViewController操作錄制的音視頻。
[[RPScreenRecorder sharedRecorder] stopRecordingWithHandler:^(RPPreviewViewController *previewViewController, NSError * error){
[self presentViewController:previewViewController animated:YES completion:^{
;
}];
}];
預(yù)覽的vc展示出來如下圖:圖中圈中位置分別提供了預(yù)覽、保存到相冊、分享三個入口。

iOS10:
···
iOS9已經(jīng)實(shí)現(xiàn)了基本的app內(nèi)容錄制、預(yù)覽、保存、分享,但是其輸出的結(jié)果其實(shí)是一個已經(jīng)將音頻、視頻編碼并交織到一起成為一個mp4文件,開發(fā)者只能處理這個mp4文件,無法對原始音視頻數(shù)據(jù)進(jìn)行處理。對于有些app可能存在諸如分辨率減小、碼率減小、音頻編輯等各種需求,都需要對原始的yuv、pcm數(shù)據(jù)進(jìn)行處理,或者對編碼過程進(jìn)行定制化干預(yù)。
考慮到開發(fā)者這個需求,蘋果在iOS10的replaykit中開放了這部分api,通過extension形式將錄制進(jìn)程展現(xiàn)給開發(fā)者。其實(shí)iOS9時錄制也是在一個獨(dú)立于app的進(jìn)程中進(jìn)行,只是未開放。iOS10提供了分發(fā)相關(guān)多個類和api,用戶可以通過代理方法獲取到屏幕錄制的原始數(shù)據(jù),做進(jìn)一步處理。引入時需要通過xcode的file -> new -> target 找到兩個相關(guān)extension:

錄制
ios10的replaykit的錄制已經(jīng)跟iOS9差異很大,ios10已經(jīng)支持錄制的原始音視頻數(shù)據(jù)的 【實(shí)時】獲?。╥OS9只可以獲取到錄制停止后編碼的mp4),開發(fā)者可以自己進(jìn)行實(shí)時分發(fā)或者編碼后處理。
主要步驟如下:
- 啟動備選界面:
iOS10中由于錄制作為一個外部的extension,可以供所有系統(tǒng)中app使用,所以不能直接啟動這個錄制的進(jìn)程。需要首先啟動支持錄制的列表sheet,通過下面接口
[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {
self.broadcastAVC = broadcastActivityViewController;
self.broadcastAVC.delegate = self;
[self presentViewController:self.broadcastAVC animated:YES completion:nil];
}];
這里我們設(shè)置代理,通過代理方法的回調(diào)我們才能啟動錄制進(jìn)程。

- 反饋已完成配置
當(dāng)我們點(diǎn)擊了上圖sheet中我們自己制作的extension時,系統(tǒng)將會啟動我們在創(chuàng)建extension時其中一個target對應(yīng)的進(jìn)程:xxxSetupUI進(jìn)程,這個進(jìn)程通常用于讓用戶輸入一些信息來鑒權(quán),或者自定義其他界面,在啟動錄制進(jìn)程之間插入的一個交互的頁面,當(dāng)然也可以為空,但是不插入交互頁面時,我們需要在相關(guān)進(jìn)程中反饋信息:
#import "BroadcastSetupViewController.h"
@implementation BroadcastSetupViewController
- (void)userDidFinishSetup {
NSURL *broadcastURL = [NSURL URLWithString:@"http://apple.com/broadcast/streamID"];
NSDictionary *setupInfo = @{ @"broadcastName" : @"example" };
// Tell ReplayKit that the extension is finished setting up and can begin broadcasting
[self.extensionContext completeRequestWithBroadcastURL:broadcastURL setupInfo:setupInfo];
}
- (void)userDidCancelSetup {
[self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"YourAppDomain" code:-1 userInfo:nil]];
}
- (void)viewDidLoad
{
}
- (void)viewWillAppear:(BOOL)animated
{
[self userDidFinishSetup];
}
這里的BroadcastSetupViewController就在xxxSetupUI的target中,是這個target建立時自動生成的模板vc,我們可以在這里添加自定義方法來建立一個vc,添加view,用于展示信息,或者用戶鑒權(quán),然后根據(jù)用戶輸入情況,決定是否讓用戶使用錄制進(jìn)程。
如果我們同意用戶使用錄制進(jìn)程,這里我們主要需要告知調(diào)用的進(jìn)程我們xxxSetupUI進(jìn)程已經(jīng)完成設(shè)置,可以開始廣播了。其中viewDidLoad、viewWillAppear兩個方法是我后填寫的,這里主要是需要調(diào)用[self userDidFinishSetup]; 方法來完成通知調(diào)用方。
注意:
- 必須調(diào)用[self userDidFinishSetup] ,調(diào)用進(jìn)程里面的didFinishWithBroadcastController (下一步啟動錄制時用到)才能回調(diào)
- 必須在viewWillAppear中才能調(diào)用,在viewDidLoad中無法生效(都是坑啊......)
- 啟動錄制:
上一步,xxxSetupUI進(jìn)程通過self.extensionContext 將其extension進(jìn)程中的信息反饋回來,我們的RPBroadcastActivityViewController的代理方法將會回調(diào):
- (void)broadcastActivityViewController:(RPBroadcastActivityViewController *)broadcastActivityViewController didFinishWithBroadcastController:(RPBroadcastController *)broadcastController error:(NSError *)error
{
dispatch_async(dispatch_get_main_queue(), ^{
[broadcastActivityViewController dismissViewControllerAnimated:YES completion:nil];
});
self.broadcastController = broadcastController;
[broadcastController startBroadcastWithHandler:^(NSError * _Nullable error) {
}];
}
回調(diào)中我們需要首先將sheet界面dismiss。 然后通過回調(diào)回來的broadcastController,調(diào)用接口啟動錄制,這里需要將broadcastController引用下來,用于我們在合適時機(jī)使用它結(jié)束錄制。
- 接收原始音視頻數(shù)據(jù)
上一步啟動錄制成功后,我們就可以在錄制進(jìn)程中接收到相關(guān)回調(diào)了,錄制進(jìn)程在target創(chuàng)建時,模板生成了SampleHandler,其中已經(jīng)復(fù)寫了相關(guān)錄制進(jìn)行的方法:
@implementation SampleHandler
- (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.
}
- (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;
}
}
首先會回調(diào)到broadcastStartedWithSetupInfo方法,這里我們通常進(jìn)行了一些初始化,例如進(jìn)程間通知的監(jiān)聽等。下面的幾個方法broadcastPaused、broadcastResumed、broadcastFinished表示了錄制的進(jìn)程變化,通常我們會在其中添加進(jìn)程通知,通過源app這些變化。最后的processSampleBuffer方法就是最終采集到的音頻、視頻原始數(shù)據(jù)。其中音頻未做混音,包括麥克音頻pcm和app音頻pcm,而視頻輸出為yuv數(shù)據(jù)。
注意:
- iOS10只支持app內(nèi)容錄制,所以當(dāng)app切到后臺,錄制內(nèi)容將停止;
- 手機(jī)鎖屏?xí)r,錄制進(jìn)程將停止;
- 這幾個方法中的代碼不能阻塞(例如寫文件等慢操作),否則導(dǎo)致錄制進(jìn)程停止;
iOS11:
到了iOS11時代,蘋果終于開放了對錄制內(nèi)容的升級,從iOS10的app內(nèi)升級到整個系統(tǒng)級別的錄制。但是對于隱私方面的考慮,蘋果還是增加了很多用戶使用門檻。iOS11中如果只是錄制app內(nèi)的內(nèi)容,直接使用iOS10的方法即可,但是如果錄制系統(tǒng)內(nèi)容,則變化較多:
- 啟動錄制:
- 對于錄制app內(nèi)容,iOS11增加了新接口,可以直接啟動想要的錄制進(jìn)程,跳過中間列表sheet在點(diǎn)擊選擇的過程:
+ (void)loadBroadcastActivityViewControllerWithPreferredExtension:(NSString * _Nullable)preferredExtension handler:(nonnull void(^)(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error))handler API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos);
-
對于錄制系統(tǒng)內(nèi)容,iOS11不允許開發(fā)直接調(diào)用api來啟動系統(tǒng)界別的錄制,必須是用戶通過手動啟動。啟動方法很復(fù)雜:
用戶點(diǎn)擊進(jìn)入手機(jī)設(shè)置頁面-> 控制中心-> 自定義 , 找到屏幕錄制的功能按鈕,將其添加到上方:添加成功后,我們可以在手機(jī)上滑喚出控制界面中發(fā)現(xiàn)這個啟動按鈕:
注意:
在上方彈出的列表中,需要選擇我們創(chuàng)建target對應(yīng)的app圖標(biāo),才能使用我們的錄制進(jìn)程進(jìn)行采集。
- 通知啟動app:
由于iOS11錄制的啟動為手動操作,并且開發(fā)者啟動錄制進(jìn)程的app也無從知道是否已經(jīng)啟動,所以通常我們會在broadcastStartedWithSetupInfo中發(fā)出進(jìn)程級通知,告知app,錄制已經(jīng)啟動。 - 結(jié)束錄制:
從iOS11的接口設(shè)計上,我們推斷結(jié)束估計也跟啟動錄制一樣,不開放給開發(fā)者,所以起初我以為只能通過用戶自己再次點(diǎn)擊啟動錄制按鈕,選擇停止,才能主動停止錄制,開發(fā)者無法干預(yù)這個過程。使用方法同啟動錄制類似,彈出列表中,直接點(diǎn)擊下面的停止。
但是很明顯,這種設(shè)計對用戶體驗影響很大,如果我們的app已經(jīng)停止了對采集的數(shù)據(jù)的顯示或者分發(fā),但是由于無法干預(yù)錄制進(jìn)程,那個進(jìn)程將持續(xù)在工作,最直觀體現(xiàn)在手機(jī)導(dǎo)航欄上方綠條(與手機(jī)通話時同樣的機(jī)制),直到后來在RPBroadcastSampleHandler的方法里面發(fā)現(xiàn)了這個:
/*! @abstract Method that should be called when broadcasting can not proceed due to an error. Calling this method will stop the broadcast and deliver the error back to the broadcasting app through RPBroadcastController's delegate.
@param error NSError object that will be passed back to the broadcasting app through RPBroadcastControllerDelegate's broadcastController:didFinishWithError: method.
*/
- (void)finishBroadcastWithError:(NSError *)error;
這個方法就藏在上面列出的broadcastStartedWithSetupInfo、broadcastPaused、broadcastResumed、broadcastFinished等方法后面,被我誤以為是一個錄制狀態(tài)的回調(diào)。那么在啟動錄制進(jìn)程的app中怎么使用這個 finishBroadcastWithError 方法來結(jié)束錄制呢?
由于是手動啟動錄制進(jìn)程,在啟動錄制進(jìn)程的app中,我們沒有相關(guān)回調(diào)能獲取到這個方法的 RPBroadcastSampleHandler實(shí)例,所以無法直接啟動。只能在錄制進(jìn)程中RPBroadcastSampleHandler實(shí)例自己調(diào)用,那么我們就可以通過進(jìn)程通信的方法,前面已經(jīng)介紹了啟動錄制時我們先注冊進(jìn)程通知,然后在收到進(jìn)程通知時,我們調(diào)用 [self finishBroadcastWithError: nil]; 即可,這里的error入?yún)?,我們可以自定義一個字典,用于將錯誤信息展示進(jìn)程結(jié)束時彈出的alert窗口中給用戶。
iOS12:
iOS11的復(fù)雜操作啟動屏幕錄制,不知道阻塞了多少用戶的繼續(xù)使用。進(jìn)入到2018年的iOS12,蘋果終于想通了,replaykit也迎來了柳暗花明,開發(fā)者企盼的api控制啟動錄制終于來了!
啟動錄制:
iOS12還是會考慮用戶的感知性,要求開發(fā)者必須通過replaykit提供的 RPSystemBroadcastPickerView 來展示啟動的view,然后通過點(diǎn)擊view上面的按鈕才能啟動:
#ifdef IPHONE_OS_VERSION_iOS12
_broadPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(20, 5, 20, 20)];
_broadPickerView.preferredExtension = @"com.cmcc.xiaoximeeting.ScreenRecordUpload";
[self addSubview:_broadPickerView];
#endif
如上面代碼,可以通過屬性preferredExtension直接加載我們想要的錄制進(jìn)程。
優(yōu)化:
雖然我們迎來更多自主控制權(quán),但是悲催的是這里我們還是要等待彈出界面點(diǎn)擊啟動,才能開始錄制。如果我們這個錄制只是作為我們本身app的功能點(diǎn),如何繞過這個點(diǎn)擊操作呢? 可以考慮用一些trick方式:
- 首先我們將_broadPickerView的frame合理設(shè)置,使其隱藏在某個按鈕(通常是自定義的啟動錄制)后面;
- 當(dāng)我們點(diǎn)擊到這個按鈕時 ,響應(yīng)鏈會將點(diǎn)擊也傳遞給這個_broadPickerView,那么這時我們可以再把點(diǎn)擊傳遞給_broadPickerView上面的開始按鈕:
- (void)clickedOnStartRecordButton:(UIButton *)sender
{
#ifdef IPHONE_OS_VERSION_iOS12
if (sender.tag == TAG_SHARESCREEN)
{
for (UIView *view in _broadPickerView.subviews)
{
if ([view isKindOfClass:[UIButton class]])
{
// 注意ios12 時 對應(yīng)UIControlEventTouchDown,不知從哪個版本開始已經(jīng)變成UIControlEventTouchUpInside,所以為了兼容性還是使用UIControlEventAllTouchEvents
//[(UIButton*)view sendActionsForControlEvents:UIControlEventTouchDown];
//[(UIButton*)view sendActionsForControlEvents:UIControlEventTouchUpInside];
[(UIButton*)view sendActionsForControlEvents:UIControlEventAllTouchEvents];
}
}
}
else
{
#endif
// 其他邏輯代碼
#ifdef IPHONE_OS_VERSION_iOS12
}
#endif
注意:
sendActionsForControlEvents:UIControlEventTouchDown傳遞的參數(shù)必須是UIControlEventTouchDown,我之前傳的是upinside事件,一直失敗,直到嘗試了UIControlEventAllTouchEvents,發(fā)現(xiàn)可以成功,才發(fā)覺事件不對,逐個嘗試其他事件后,才定位到是UIControlEventTouchDown
【 ---注意---: ios12 時 對應(yīng)UIControlEventTouchDown,不知從哪個版本開始已經(jīng)變成UIControlEventTouchUpInside,所以為了兼容性還是使用UIControlEventAllTouchEvents
】。
- 當(dāng)我們點(diǎn)擊上層的按鈕時,自動點(diǎn)擊系統(tǒng)的_broadPickerView上面的開始錄制按鈕。
總結(jié):
本文主要論述各個iOS系統(tǒng)版本使用replaykit實(shí)現(xiàn)屏幕的技術(shù)細(xì)節(jié),其他需要考慮的點(diǎn)暫不詳述,還包括:
- 屏幕方向變化,可以考慮使用RPVideoSampleOrientationKey 對采集的yuv數(shù)據(jù)結(jié)構(gòu)解析出來方向;
- 屏幕鎖定的通知,雖然進(jìn)程級通知提供了鎖屏的通知,但是appstore不允許使用,可以考慮使用appdelegate的代理方法來判斷;
- 采集到數(shù)據(jù)結(jié)構(gòu)中的yuv的緩存空間,不能占用(例如NSData的initWithBytesNoCopy方法雖然可以快速生成NSData,但是將占用這個緩存),否則將導(dǎo)致進(jìn)程停止;
- 系統(tǒng)提供錄制進(jìn)程的內(nèi)存空間約為50M,我們在內(nèi)存占用時需要注意超過50M, 進(jìn)程將被停止;

