會議投屏直播:投屏數據流獲取方案

- 不同系統版本的演進

這裏只以ios爲例,以後有時間會分別寫一下其它平臺上的實現方式。
蘋果在這方面一直處於消極緩慢的演進狀態,總是令開發者有點巧婦難爲的感覺。即使是給出的實現方案,也因爲各種不穩定,在實際調用過程中不能穩定執行。後面也會一一說明。

ios中比較正規的實現方式是用Extension來實現的,從ios9開始提供很有限的實現。在ios9之前也有通過破解協議來實現的,本文是以Extension擴展方式實現,如何添加擴展我就不一一說明了。

1. RPScreenRecorder

RPScreenRecorder是蘋果最早提出的屏幕錄製方案,直接上部分代碼:


/*
 *  直接觸發錄屏,只能錄取應該應用中的內容,應用進入後臺或退出後,自動關閉。
 */
 #pragma mark- start record
-(IBAction)onStart:(id)sender{
    if([[RPScreenRecorder sharedRecorder] isAvailable] == YES){
        [[RPScreenRecorder sharedRecorder] startRecordingWithMicrophoneEnabled:YES handler:^(NSError * _Nullable error) {
            
        }];
    }
}

#pragma mark- stop record
- (IBAction)onStop:(id)sender{
    if([[RPScreenRecorder sharedRecorder] isAvailable] == YES){
        [[RPScreenRecorder sharedRecorder]stopRecordingWithHandler:^(RPPreviewViewController * _Nullable previewViewController, NSError * _Nullable error) {
            [self presentViewController:previewViewController animated:YES completion:^{
                
            }];
        }];
    }
}

上面這種方式當點擊完成後,會模態打開系統提供的預覽界面。在界面中可以選擇保存共享等方式。顯示然這種方式是不支持流式實時返回的,做直播顯然不太合適,主要用於後期生成完整文件再上傳的方式。

於是,在 ios11有時候,蘋果又在這個基礎上提供了一定的實時流返回的能力,但其實這個時候,已經可以不用這個方式實現了,這時大概提供一下調用方法:


// 開始錄製
    if (@available(iOS 11.0, *)) {
        [[RPScreenRecorder sharedRecorder] startCaptureWithHandler:^(CMSampleBufferRef  _Nonnull sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error) {
            NSLog(@"--- [數據]  %@", sampleBuffer);
        } completionHandler:^(NSError * _Nullable error) {
            NSLog(@"Recording started error %@",error);
        }];
    }


// 停止錄製
    if (@available(iOS 11.0, *)) {
        [[RPScreenRecorder sharedRecorder] stopCaptureWithHandler:^(NSError * _Nullable error) {
            
        }];
    }
    

這時已經解決了實時流數據的返回問題,但始終無法錄製應用外的內容。其實在實現這個需求,已經有了另外的實現方式。

2. Extension 實現實時流數據的返回

Extension添加過程不上圖了,選擇target -> add -> 選擇 Broadcast Upload Extension ->Next ->點選 include UI Extension選項 -> 完成添加

這個時候,系統多出了兩個文件夾,一個用於實現錄屏前的ui展示(ios 12及之後,基於新的啓動方式,已經不再回調)。通常可以在這個界面做一些設置或作爲過度性的界面展示。


@implementation BroadcastSetupViewController

// Call this method when the user has finished interacting with the view controller and a broadcast stream can start
- (void)userDidFinishSetup {
    NSURL *broadcastURL = [NSURL URLWithString:@"http://apple.com/broadcast/streamID"];
    NSDictionary *setupInfo = @{ @"broadcastName" : @"example" };
    [self.extensionContext completeRequestWithBroadcastURL:broadcastURL setupInfo:setupInfo];
}

- (void)userDidCancelSetup {
    [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"YourAppDomain" code:-1 userInfo:nil]];
}

@end

從註釋上已經可以理解,當選擇確定,調用 ** userDidFinishSetup ** ,當選擇取消時,調用 ** userDidCancelSetup ** 進行錄屏前的終止。

當完成上面的流程後,系統進入到錄屏並返回實時流的回高階段。回調代碼如下:


#import "SampleHandler.h"

@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;
    }
}

@end

如何觸Extension調用?

在ios10,系統提供了第一種方式:用戶主動選擇Extension

//打開系統選擇框,在擴展開表中選擇擴展
[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {
        [self presentViewController:broadcastActivityViewController animated:YES completion:nil];
    }];

在 ios11的時候,系統又提供了另外一種更直接的觸發方式:

[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithPreferredExtension:@"com.ReplayKitDemo.TestSetupUI" handler:^(RPBroadcastActivityViewController * _Nullable controller, NSError * _Nullable error) {
        controller.delegate = self;
        [self presentViewController:controller animated:YES completion:nil];
    }];

代碼中 “com.ReplayKitDemo.TestSetupUI”對應於SetupUI中的BoundID。
後來,蘋果覺得上面的兩種方式還是不夠直接,後來又有了第三種,在ios12開始提出的方式:

if (@available(iOS 12.0, *)) {
        RPSystemBroadcastPickerView *pickView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
        pickView.preferredExtension = @"com.awesome.SRUtil.SRExtUpload";
        [self.view addSubview:pickView];
    }

RPSystemBroadcastPickerView是一個系統提供的控件,提供直接調用功
能。這種方式調用之下,不再調用 SetupUI進行顯示,而是直接調用SampleHandler 所在擴展項目。“com.awesome.SRUtil.SRExtUpload”爲SampleHandler項目中的BoundID。但到現時ios13將要推出之時,這種調用方式還不是太穩定。

- 注意事項

1. Extension 50M 內存限制

在項目實踐中,針對這 50M的內存限制,直接促使我在推流方案上做出了多種方式的嘗試。包括從Tcp向多端同時推流,到UDP組播發送,幾乎整個過程都圍繞內存限制而展開。

因爲TPLine的硬性指標要求,推流不能經過服務器中轉,而實現 PTP的直接送達。所以早期用Tcp實現同時多端推送過程中,修改了一開始的方案,爲解決回調數據堆積速度快於Tcp多端推流的問題,果斷放棄了發送數據隊列的慣用實現方式。這個在後面的博文中會一一展開說明。

後面改用UDP+組播的方式發送時,的的確確解決了多端併發推流的問題,但要配合UDP實現多種差錯控制的方案。這個在後面的博文中會一一展開說明。

受限於推流不能經過服務器中轉,實踐證明,用於局域網內的會議投屏系統,採用UDP+組播的方式纔是最佳實踐方式。

2. Extension 邏輯處理的時間限制

在進行擴展的debug過程中,一開始會習慣性以下斷點的方式進行數據的調試。但很快你會發現,擴展很快就結束並退出了。

系統在提供擴展支持的時候,同時也限制了他的最大運行時間,以保持系統的流暢。包括編碼在內的所有邏輯都在這個最大運行時間限制下運行。但實際應用說明,進行編碼操作時間上還是十分足夠的。

所以在進行邏輯設計時,在“processSampleBuffer”方法回調後的處理要做好多線程的處理。

3. 同步線程隊列執行編碼

//
//  SampleHandler.m
//  TPUpload
//
//  Created by adam on 2019/5/22.
//  Copyright © 2019 lcs. All rights reserved.
//

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:
            [[H264Encoder shareInstance] startH264EncodeWithSampleBuffer:sampleBuffer andReturnData:^(NSData *data) {
                [[UDPServerMain shareInstance] pushVideo:data];
            }];
            break;
        case RPSampleBufferTypeAudioApp:
            break;
        case RPSampleBufferTypeAudioMic:
            break;
        default:
            break;
    }
}

//
//  H264Encoder.m
//  Awesome
//
//  Created by adam on 2019/1/8.
//  Copyright © 2019年 common. All rights reserved.
//


#pragma mark- startH264EncodeWithSampleBuffer
-(void)startH264EncodeWithSampleBuffer:(CMSampleBufferRef)sampleBuffer andReturnData:(ReturnDataBlock)block{
    self.returnDataBlock = block;
    dispatch_sync(m_EncodeQueue, ^{
        [self encode:sampleBuffer];
    });
}


以上面代碼爲例,H264Encoder進行硬編碼,而在 “startH264EncodeWithSampleBuffer”方法中,進行編碼操作時,不能使用dispatch_async異步的方式進行調用。

4. RPSystemBroadcastPickerView

  • 不穩定
    發展到iOS 12後,蘋果終於開放了RPSystemBroadcastPickerView給開發及用戶,終於可以實現在應用內啓動系統的錄屏功能,但在實踐中發現,其呼出的pickerView,在指定preferredExtension的情況下,會出對應該的Extension不顯示的情況。
  • 外觀無法自定義
    RPSystemBroadcastPickerView的外觀不能訂製外觀,只能默認顯示系統的樣式,在界面高度定製的情況下很難實現和諧共存。
    於是一般使用隱性調用的方式,先把RPSystemBroadcastPickerView添加到頁面,再進行隱藏。通過其它的按鈕對RPSystemBroadcastPickerView實現觸發,參考代碼如下:
for (UIView *item in self.pickView.subviews) {
        if ([item isKindOfClass:UIButton.class] == YES) {
            actionButton = (UIButton*)item;
        }
    }
dispatch_async(dispatch_get_main_queue(), ^{
            [self->actionButton sendActionsForControlEvents:UIControlEventTouchDown];
 });

- 開發過程中的經驗

1. 如何調試Extension

簡單來說就是,在調試Extension的過程中,要先安裝運行主項目,再選擇SampleHandler項目target進行調度運行,在打開的應用列表中選擇你的項目。

在代碼修改過之後,也最好重複上面的步驟,確保運行的是最新的代碼。

2. 如何處理奇怪的現象

很多奇怪現象都來源於幾個主要因素:

  • 長期不關機
    這個恐怕是很多開發人員的習慣了
  • 長期進行調試
    可以通過重啓手機,重啓電腦等方式進行
  • 系統不穩定
    這個有可能是xcode的問題,也有可能是api的問題,這些情況比較麻煩。

3.SampleHandler在不同界面中,返回的流情況不盡相同。

  • ReplayKit通過監聽屏幕是否有變化對SampleHandler中的processSampleBuffer方法進行回調。在測試中發現,不同手機,不同應用顯示在界面上時processSampleBuffer的回調情況都會不盡相同。
  • 在ipad上運行,因爲上面紅色的title一直有漸變的變化,所以正常情況下,在回調方法中一直都有流回調過來。
  • 但在手機端運行時,在非桌面(打開一個應用)的情況下,應用沒當前界面沒有操作和變化或用戶沒有發生觸摸操作,流的回調就會停止。所以,很多人就以爲投屏數據是很少的,很多人就直接把每個幀直接轉成圖片傳到播放端,其實這是一個誤區。有效地利用IBP幀可以很有效的減少網絡流量和畫面的流暢度。
  • 所以在推流服務端中,最好能緩存最後一個IBP幀組和sps, pps數據,以解決有新的接入端接入時,不會因爲IBP幀的推流,而產生白屏的尷尬情況, 這個也是實現秒開的辦法之一。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章