1. 程式人生 > >iOS端使用replaykit錄製螢幕的技術細節

iOS端使用replaykit錄製螢幕的技術細節

前端兩篇文章:
iOS端螢幕錄製(replaykit)調研
iOS端螢幕錄製Replaykit專案實踐
已經對iOS端實現螢幕錄製的調研結果和簡單實踐進行了概述,本篇開始將分別對iOS9,iOS10,iOS11,iOS12系統上具體實踐記錄一下,便於分享和自己檢視。

相比於安卓端,iOS裝置的端的螢幕錄製發展太慢了,並且對開發者的需求滿足總是延遲很大,就像其他功能一樣,這也許就是蘋果逐漸喪失他的競爭力的原因。本文將對的的iOS端使用replaykit在各個系統版本中實現細節進行描述。


iOS9:

對於iOS9的replaykit介紹功能參考可以官方WWDC視訊:支援錄製音訊,視訊,還可以增加語音旁白評論等其他額外的定製化東西對於錄製的內容

,使用者可以回訪,剪輯或者通過社交媒體軟體分享出去。

ReplayKit記錄正在執行的應用程式 的音訊視覺狀語從句:效果它還允許您使用它來新增語音評論,因此可以他們使他們的錄音更個性化或只是為了提供額外的主頁背景。 它允許您的使用者播放,擦洗和修剪他們的錄音,並將名單最終他們的錄音分享到他們最喜歡的社交網路狀語從句:視訊網站目的地。

啟動錄製使用介面:

    [[RPScreenRecorder sharedRecorder] startRecordingWithMicrophoneEnabled:YES handler:^(NSError * _Nullable error) {
        ;
    }];

注意:

  • 使用[RPScreenRecorder sharedRecorder]啟動錄製,會首先請求使用者同意使用攝像頭和麥克風,主要考慮使用者的隱私和許可權,如果使用者拒絕了,將無法進行錄製。
  • 錄製的內容不會包含系統的使用者介面中,比如上方導航欄;
  • 錄製的內容會經過音視訊編碼,而不是原始的YUV或PCM資料;
  • 錄製的內容無法直接檢視,必須通過RPPreviewViewController才能檢視預覽,或者分享,或者儲存到本地相簿中。而這個RPPreviewViewController在停止錄製的介面回撥中才能獲取,也就是說,只有停止錄製之後才能通過RPPreviewViewController操作錄製的音視訊。
[[RPScreenRecorder sharedRecorder] stopRecordingWithHandler:^(RPPreviewViewController *previewViewController, NSError *  error){
        
        [self presentViewController:previewViewController animated:YES completion:^{
            ;
        }];
    }];

預覽的VC展示出來如下圖:圖中圈中位置分別提供了預覽,儲存到相簿,分享三個入口。

 


iOS10:

···
iOS9已經實現了基本的app內容錄製,預覽,儲存,分享,但是其輸出的結果其實是一個已經將音訊,視訊編碼並交織到一起成為一個mp4檔案,開發者只能處理這個mp4檔案,無法對原始音視訊資料進行處理。對於有些應用可能存在諸如解析度減小,位元速率減小,音訊編輯等各種需求,都需要對原始的YUV,PCM資料進行處理,或者對編碼過程進行定製化干預。
考慮到開發者這個需求,蘋果在iOS10的replaykit中開放了這部分API,通過擴充套件形式將錄製程序展現給開發者。其實iOS9時錄製也是在一個獨立於應用程式的程序中進行,只是未開放.iOS10提供了分發相關多個類和api,使用者可以通過代理方法獲取到螢幕錄製的原始資料,做進一步處理。引入時需要通過xcode的檔案 - > new - > target找到兩個相關擴充套件:

 

錄製

ios10的replaykit的錄製已經跟iOS9差異很大,ios10已經支援錄製的原始音視訊資料的【實時】獲取(iOS9只可以獲取到錄製停止後編碼的MP4),開發者可以自己進行實時分發或者編碼後處理。
主要步驟如下:

  1. 備選啟動
    介面:iOS10中由於錄製作為一個外部的擴充套件,可以供所有系統中的應用程式使用,所以不能直接啟動這個錄製的程序需要首先啟動支援錄製的列表,通過下面介面
[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {
        
        self.broadcastAVC = broadcastActivityViewController;
        self.broadcastAVC.delegate = self;
        [self presentViewController:self.broadcastAVC animated:YES completion:nil];
    }];

這裡我們設定代理,通過代理方法的回撥我們才能啟動錄製程序。

 


  1. 反饋已完成配置
    當我們點選了上圖產品產品片中我們自己製作的擴充套件時,系統將會啟動我們在建立擴充套件時其中一個目標對應的程序:xxxSetupUI程序,這個程序通常用於讓使用者輸入一些資訊來鑑權,或者自定義其他介面,在啟動錄製程序之間插入的一個互動的頁面,當然也可以為空,但是不插入互動頁面時,我們需要在相關程序中反饋資訊:
#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的目標中,是這個目標建立時自動生成的模板VC,我們可以在這裡新增自定義方法來建立一個VC,新增檢視,用於展示資訊,或者使用者鑑權,然後根據使用者輸入情況,決定是否讓使用者使用錄製程序。
如果我們同意使用者使用錄製程序,這裡我們主要需要告知呼叫的程序我們xxxSetupUI程序已經完成設定,可以開始廣播了。其中的viewDidLoad中,viewWillAppear中中兩個方法是我後填寫的,這裡主要是需要呼叫[self userDidFinishSetup]; 方法來完成通知呼叫方。

注意:

  • 必須呼叫[self userDidFinishSetup],呼叫程序裡面的didFinishWithBroadcastController(下一步啟動錄製時用到)才能回撥
  • 必須在viewWillAppear中中中才能呼叫,在viewDidLoad中中中無法生效(都是坑啊......)

  1. 啟動錄製:
    上一步,xxxSetupUI程序通過self.extensionContext將其擴充套件程序中的資訊反饋回來,我們的RPBroadcastActivityViewController的代理方法將會回撥:
- (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) {

    }];
}

回撥中我們需要首先將表介面解除。然後通過回調回來的broadcastController,呼叫介面啟動錄製,這裡需要將broadcastController引用下來,用於我們在合適時機使用它結束錄製。


  1. 接收原始音視訊資料
    上一步啟動錄製成功後,我們就可以在錄製程序中接收到相關回調了,錄製程序在目標建立時,模板生成了SampleHandler,其中已經複寫了相關錄製進行的方法
@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;
    }
}

首先會回撥到broadcastStartedWithSetupInfo方法,這裡我們通常進行了一些初始化,例如程序間通知的監聽等。下面的幾個方法broadcastPaused,broadcastResumed,broadcastFinished表示了錄製的程序變化,通常我們會在其中新增程序通知,通過源應用這些變化。最後的processSampleBuffer方法就是最終採集到的音訊,視訊原始資料。其中音訊未做混音,包括麥克音訊PCM和應用音訊PCM,而視訊輸出為YUV資料。

注意:

  • iOS10只支援的應用程式內容錄製,所以當應用切到後臺,錄製內容將停止;
  • 手機鎖屏時,錄製程序將停止;
  • 這幾個方法中的程式碼不能阻塞(例如寫檔案等慢操作),否則導致錄製程序停止;

iOS11:

到了iOS11時代,蘋果終於開放了對錄製內容的升級,從iOS10的應用內升級到整個系統級別的錄製。但是對於隱私方面的考慮,蘋果還是增加了很多使用者使用門檻.iOS11中如果只是錄製的應用程式內的內容,直接使用iOS10的方法即可,但是如果錄製系統內容,則變化較多:

  1. 啟動錄製:
  • 對於錄製應用內容,iOS11增加了新介面,可以直接啟動想要的錄製程序,跳過中間列表片在點選選擇的過程:
+ (void)loadBroadcastActivityViewControllerWithPreferredExtension:(NSString * _Nullable)preferredExtension handler:(nonnull void(^)(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error))handler API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos);
  • 對於錄製系統內容,iOS11不允許開發直接呼叫api來啟動系統界別的錄製,必須是使用者通過手動啟動。啟動方法很複雜:
    使用者點選進入手機設定頁面 - >控制中心 - >自定義,找到螢幕錄製的功能按鈕,將其新增到上方:新增成功後,我們可以在手機上滑喚出控制介面中發現這個啟動按鈕

     

     

注意:

在上方彈出的列表中,需要選擇我們建立目標對應的應用程式圖示,才能使用我們的錄製程序進行採集。

  1. 通知啟動應用程式:
    由於iOS11錄製的啟動為手動操作,並且開發者啟動錄製程序的應用也無從知道是否已經啟動,所以通常我們會在broadcastStartedWithSetupInfo中發出程序級通知,告知程式,錄製已經啟動。
  2. 結束錄製:
    從iOS11的介面設計上,我們推斷結束估計也跟啟動錄製一樣,不開放給開發者,所以起初我以為只能通過使用者自己再次點選啟動錄製按鈕,選擇停止,才能主動停止錄製,開發者無法干預這個過程,使用方法同啟動錄製類似,彈出列表中,直接點選下面的停止。
    但是很明顯,這種設計對使用者體驗影響很大,如果我們的應用程式已經停止了對採集的資料的顯示或者分發,但是由於無法干預錄製程序,那個程序將持續在工作,最直觀體現在手機導航欄上方綠條(與手機通話時同樣的機制),直到後來在RPBroadcastSampleHandler的方法裡面發現了這個:

/*! @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等方法後面,被我誤以為是一個錄製狀態的回撥。那麼在啟動錄製程序的應用程式中怎麼使用這個finishBroadcastWithError方法來結束錄製呢?
由於是手動啟動錄製程序,在啟動錄製程序的應用程式中,我們沒有相關回調能獲取到這個方法的RPBroadcastSampleHandler例項,所以無法直接啟動。只能在錄製程序中RPBroadcastSampleHandler例項自己呼叫,那麼我們就可以通過程序通訊的方法,前面已經介紹了啟動錄製時我們先註冊程序通知,然後在收到程序通知時,我們呼叫[self finishBroadcastWithError:nil]; 即可,這裡的錯誤入參,我們可以自定義一個字典,用於將錯誤資訊展示程序結束時彈出的警告視窗中給使用者


iOS12:

iOS11的複雜操作啟動螢幕錄製,不知道阻塞了多少使用者的繼續使用。進入到2018年的iOS12,蘋果終於想通了,replaykit也迎來了柳暗花明,開發者企盼的API控制啟動錄製終於來了!
啟動錄製:
iOS12還是會考慮使用者的感知性,要求開發者必須通過replaykit提供的RPSystemBroadcastPickerView來展示啟動的檢視,然後通過點選檢視上面的按鈕才能啟動:


#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直接載入我們想要的錄製程序。

優化:

雖然我們迎來更多自主控制權,但是悲催的是這裡我們還是要等待彈出介面點選啟動,才能開始錄製。如果我們這個錄製只是作為我們本身的應用程式的功能點,如何繞過這個點選操作呢?可以考慮用一些伎倆方式:

  1. 首先我們將_broadPickerView的幀合理設定,使其隱藏在某個按鈕(通常是自定義的啟動錄製)後面;
  2. 當我們點選到這個按鈕時,響應鏈會將點選也傳遞給這個_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]])
            {
                [(UIButton*)view sendActionsForControlEvents:UIControlEventTouchDown];
            }
        }
    }
    else
    {
#endif
   // 其他邏輯程式碼
#ifdef IPHONE_OS_VERSION_iOS12
}
#endif

注意:

sendActionsForControlEvents:UIControlEventTouchDown傳遞的引數必須是UIControlEventTouchDown,我之前傳的是upinside事件,一直失敗,直到嘗試了UIControlEventAllTouchEvents,發現可以成功,才發覺事件不對,逐個嘗試其他事件後,才定位到是UIControlEventTouchDown。

  1. 當我們點選上層的按鈕時,自動點選系統的_broadPickerView上面的開始錄製按鈕。

總結:

本文主要論述各個的的iOS系統版本使用replaykit實現螢幕的技術細節,其他需要考慮的點暫不詳述,還包括:

  1. 螢幕方向變化,可以考慮使用RPVideoSampleOrientationKey對採集的YUV資料結構解析出來方向;
  2. 螢幕鎖定的通知,雖然程序級通知提供了鎖屏的通知,但是蘋果商店的不允許使用,可以考慮使用的的appdelegate的代理方法來判斷;
  3. 採集到資料結構中的YUV的快取空間,不能佔用(例如NSData的的的initWithBytesNoCopy方法雖然可以快速生成的NSData的,但是將佔用這個快取),否則將導致程序停止;
  4. 系統提供錄製程序的記憶體空間約



作者:杭研融合通訊的iOS
連結:HTTPS://www.jianshu.com/p/401b5b632d5b
來源:書繁簡
繁簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處