目錄
- 不同系統版本的演進
這裏只以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幀的推流,而產生白屏的尷尬情況, 這個也是實現秒開的辦法之一。