++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
前面兩篇文章:
iOS端屏幕錄制(replaykit)調(diào)研
iOS端屏幕錄制Replaykit項目實踐
已經(jīng)對iOS端實現(xiàn)屏幕錄制的調(diào)研結(jié)果和簡單實踐進行了概述蜕提,本篇開始將分別對iOS9甜癞、iOS10糯累、iOS11、iOS12系統(tǒng)上具體實踐記錄一下,便于分享和自己查看猛蔽。
相比于安卓端蟀俊,iOS端的屏幕錄制發(fā)展太慢了,并且對開發(fā)者的需求滿足總是延遲很大殊校,就像其他功能一樣,這也許就是蘋果逐漸喪失他的競爭力的原因读存。本文將對iOS端使用replaykit在各個系統(tǒng)版本中實現(xiàn)細節(jié)進行描述为流。
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)限虫给,如果用戶拒絕了,將無法進行錄制侠碧。
- 錄制的內(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)實現(xiàn)了基本的app內(nèi)容錄制、預(yù)覽西饵、保存酝掩、分享,但是其輸出的結(jié)果其實是一個已經(jīng)將音頻眷柔、視頻編碼并交織到一起成為一個mp4文件期虾,開發(fā)者只能處理這個mp4文件,無法對原始音視頻數(shù)據(jù)進行處理驯嘱。對于有些app可能存在諸如分辨率減小镶苞、碼率減小、音頻編輯等各種需求鞠评,都需要對原始的yuv宾尚、pcm數(shù)據(jù)進行處理,或者對編碼過程進行定制化干預(yù)谢澈。
考慮到開發(fā)者這個需求,蘋果在iOS10的replaykit中開放了這部分api御板,通過extension形式將錄制進程展現(xiàn)給開發(fā)者锥忿。其實iOS9時錄制也是在一個獨立于app的進程中進行,只是未開放怠肋。iOS10提供了分發(fā)相關(guān)多個類和api敬鬓,用戶可以通過代理方法獲取到屏幕錄制的原始數(shù)據(jù),做進一步處理笙各。引入時需要通過xcode的file -> new -> target 找到兩個相關(guān)extension:
錄制
ios10的replaykit的錄制已經(jīng)跟iOS9差異很大钉答,ios10已經(jīng)支持錄制的原始音視頻數(shù)據(jù)的 【實時】獲取(iOS9只可以獲取到錄制停止后編碼的mp4)杈抢,開發(fā)者可以自己進行實時分發(fā)或者編碼后處理数尿。
主要步驟如下:
- 啟動備選界面:
iOS10中由于錄制作為一個外部的extension,可以供所有系統(tǒng)中app使用惶楼,所以不能直接啟動這個錄制的進程右蹦。需要首先啟動支持錄制的列表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)我們才能啟動錄制進程何陆。
- 反饋已完成配置
當(dāng)我們點擊了上圖sheet中我們自己制作的extension時晨汹,系統(tǒng)將會啟動我們在創(chuàng)建extension時其中一個target對應(yīng)的進程:xxxSetupUI進程,這個進程通常用于讓用戶輸入一些信息來鑒權(quán)贷盲,或者自定義其他界面淘这,在啟動錄制進程之間插入的一個交互的頁面,當(dāng)然也可以為空巩剖,但是不插入交互頁面時铝穷,我們需要在相關(guā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ù)用戶輸入情況,決定是否讓用戶使用錄制進程镊尺。
如果我們同意用戶使用錄制進程朦佩,這里我們主要需要告知調(diào)用的進程我們xxxSetupUI進程已經(jīng)完成設(shè)置,可以開始廣播了庐氮。其中viewDidLoad语稠、viewWillAppear兩個方法是我后填寫的,這里主要是需要調(diào)用[self userDidFinishSetup]; 方法來完成通知調(diào)用方弄砍。
注意:
- 必須調(diào)用[self userDidFinishSetup] 仙畦,調(diào)用進程里面的didFinishWithBroadcastController (下一步啟動錄制時用到)才能回調(diào)
- 必須在viewWillAppear中才能調(diào)用,在viewDidLoad中無法生效(都是坑啊......)
- 啟動錄制:
上一步音婶,xxxSetupUI進程通過self.extensionContext 將其extension進程中的信息反饋回來慨畸,我們的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引用下來,用于我們在合適時機使用它結(jié)束錄制碴卧。
- 接收原始音視頻數(shù)據(jù)
上一步啟動錄制成功后弱卡,我們就可以在錄制進程中接收到相關(guān)回調(diào)了,錄制進程在target創(chuàng)建時住册,模板生成了SampleHandler婶博,其中已經(jīng)復(fù)寫了相關(guā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方法,這里我們通常進行了一些初始化界弧,例如進程間通知的監(jiān)聽等凡蜻。下面的幾個方法broadcastPaused搭综、broadcastResumed、broadcastFinished表示了錄制的進程變化划栓,通常我們會在其中添加進程通知兑巾,通過源app這些變化。最后的processSampleBuffer方法就是最終采集到的音頻忠荞、視頻原始數(shù)據(jù)蒋歌。其中音頻未做混音,包括麥克音頻pcm和app音頻pcm委煤,而視頻輸出為yuv數(shù)據(jù)堂油。
注意:
- iOS10只支持app內(nèi)容錄制,所以當(dāng)app切到后臺碧绞,錄制內(nèi)容將停止府框;
- 手機鎖屏?xí)r,錄制進程將停止讥邻;
- 這幾個方法中的代碼不能阻塞(例如寫文件等慢操作)迫靖,否則導(dǎo)致錄制進程停止;
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增加了新接口苹粟,可以直接啟動想要的錄制進程踩寇,跳過中間列表sheet在點擊選擇的過程:
+ (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ù)雜:
用戶點擊進入手機設(shè)置頁面-> 控制中心-> 自定義 , 找到屏幕錄制的功能按鈕辣卒,將其添加到上方:添加成功后掷贾,我們可以在手機上滑喚出控制界面中發(fā)現(xiàn)這個啟動按鈕:
注意:
在上方彈出的列表中,需要選擇我們創(chuàng)建target對應(yīng)的app圖標(biāo)荣茫,才能使用我們的錄制進程進行采集想帅。
- 通知啟動app:
由于iOS11錄制的啟動為手動操作,并且開發(fā)者啟動錄制進程的app也無從知道是否已經(jīng)啟動啡莉,所以通常我們會在broadcastStartedWithSetupInfo中發(fā)出進程級通知港准,告知app旨剥,錄制已經(jīng)啟動。 - 結(jié)束錄制:
從iOS11的接口設(shè)計上浅缸,我們推斷結(jié)束估計也跟啟動錄制一樣轨帜,不開放給開發(fā)者,所以起初我以為只能通過用戶自己再次點擊啟動錄制按鈕衩椒,選擇停止蚌父,才能主動停止錄制,開發(fā)者無法干預(yù)這個過程毛萌。使用方法同啟動錄制類似苟弛,彈出列表中,直接點擊下面的停止阁将。
但是很明顯膏秫,這種設(shè)計對用戶體驗影響很大,如果我們的app已經(jīng)停止了對采集的數(shù)據(jù)的顯示或者分發(fā)做盅,但是由于無法干預(yù)錄制進程缤削,那個進程將持續(xù)在工作,最直觀體現(xiàn)在手機導(dǎo)航欄上方綠條(與手機通話時同樣的機制)言蛇,直到后來在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)。那么在啟動錄制進程的app中怎么使用這個 finishBroadcastWithError 方法來結(jié)束錄制呢婿斥?
由于是手動啟動錄制進程劝篷,在啟動錄制進程的app中,我們沒有相關(guān)回調(diào)能獲取到這個方法的 RPBroadcastSampleHandler實例民宿,所以無法直接啟動娇妓。只能在錄制進程中RPBroadcastSampleHandler實例自己調(diào)用,那么我們就可以通過進程通信的方法活鹰,前面已經(jīng)介紹了啟動錄制時我們先注冊進程通知哈恰,然后在收到進程通知時,我們調(diào)用 [self finishBroadcastWithError: nil]; 即可志群,這里的error入?yún)⒆疟粒覀兛梢宰远x一個字典,用于將錯誤信息展示進程結(jié)束時彈出的alert窗口中給用戶锌云。
iOS12:
iOS11的復(fù)雜操作啟動屏幕錄制荠医,不知道阻塞了多少用戶的繼續(xù)使用。進入到2018年的iOS12,蘋果終于想通了彬向,replaykit也迎來了柳暗花明兼贡,開發(fā)者企盼的api控制啟動錄制終于來了!
啟動錄制:
iOS12還是會考慮用戶的感知性娃胆,要求開發(fā)者必須通過replaykit提供的 RPSystemBroadcastPickerView 來展示啟動的view遍希,然后通過點擊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直接加載我們想要的錄制進程缕棵。
優(yōu)化:
雖然我們迎來更多自主控制權(quán)孵班,但是悲催的是這里我們還是要等待彈出界面點擊啟動,才能開始錄制招驴。如果我們這個錄制只是作為我們本身app的功能點篙程,如何繞過這個點擊操作呢? 可以考慮用一些trick方式:
- 首先我們將_broadPickerView的frame合理設(shè)置别厘,使其隱藏在某個按鈕(通常是自定義的啟動錄制)后面虱饿;
- 當(dāng)我們點擊到這個按鈕時 ,響應(yīng)鏈會將點擊也傳遞給這個_broadPickerView触趴,那么這時我們可以再把點擊傳遞給_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)我們點擊上層的按鈕時徙缴,自動點擊系統(tǒng)的_broadPickerView上面的開始錄制按鈕。
總結(jié):
本文主要論述各個iOS系統(tǒng)版本使用replaykit實現(xiàn)屏幕的技術(shù)細節(jié)嘁信,其他需要考慮的點暫不詳述于样,還包括:
- 屏幕方向變化,可以考慮使用RPVideoSampleOrientationKey 對采集的yuv數(shù)據(jù)結(jié)構(gòu)解析出來方向潘靖;
- 屏幕鎖定的通知百宇,雖然進程級通知提供了鎖屏的通知,但是appstore不允許使用秘豹,可以考慮使用appdelegate的代理方法來判斷;
- 采集到數(shù)據(jù)結(jié)構(gòu)中的yuv的緩存空間昌粤,不能占用(例如NSData的initWithBytesNoCopy方法雖然可以快速生成NSData既绕,但是將占用這個緩存)啄刹,否則將導(dǎo)致進程停止;
- 系統(tǒng)提供錄制進程的內(nèi)存空間約為50M凄贩,我們在內(nèi)存占用時需要注意超過50M, 進程將被停止誓军;