目录
- 不同系统版本的演进
这里只以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帧的推流,而产生白屏的尴尬情况, 这个也是实现秒开的办法之一。