iOS中使用ReplayKit扩展进行屏幕录制的注意事项(丢帧,补帧,及帧率控制)

背景

ReplayKit是苹果提供的一个框架,主要用于便捷地将屏幕录制功能集成到应用中。当然,如果只需在应用内实现录屏,我们也可以借助AVFoundation框架中的AVAssetWriter。ReplayKit的独特之处在于其能够捕获不仅仅是应用内的内容,即便是在应用处于后台状态时,也能记录整个手机屏幕的显示画面,拓展了其应用场景。

但ReplayKit有一个十分严格的内存空间限制,最高为50M,一旦超过了这个限制扩展进程将会自动退出。所以我们需要十分注意扩展进程中内存的使用,接下来,我们将详细介绍在使用ReplayKit扩展时需要注意的事项,来避免使用内存过高导致进程崩溃。

帧率控制

ReplayKit 的录制画面的帧率通常受到设备性能和配置的影响,因此在不同的机型上可能有所差异,在较新且性能较好的设备上,帧率可能达到 30 帧每秒(FPS)或更高。而标准帧率25帧每秒,已经完全可以满足我们对画面流程度的要求。所以我们需要对录制的帧率进行进一步的控制,来减轻下面各个环节的压力。

1.为了达到次目的我们首先要记录上一帧的时间,在CoreMedia框架中时间采用CMTime来进行表示。

@interface SampleHandler()

///上一帧时间
@property (nonatomic, assign) CMTime lastTime;

@end

2.计算当前帧与上一帧的时间差值,如果小于1/25秒,则直接舍弃该帧(这里只考虑视频数据)。

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:{
            // Handle video sample buffer
            result = [self hadleVideoSampleBuffer:sampleBuffer];
        }break;
        case RPSampleBufferTypeAudioApp:{
            // Handle audio sample buffer for app audio
        }break;
        case RPSampleBufferTypeAudioMic:{
            // Handle audio sample buffer for mic audio
    
        }break;
            
        default:
            break;
    }
}

//MARK: Handle video sample buffer
- (int)hadleVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    //添加帧率控控制
    CMTime time = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer);
    CMTime resultTime = CMTimeSubtract(time, self.lastTime);
    CMTime targetTime = CMTimeMake(1, 25);
    if (CMTimeCompare(resultTime, targetTime) == -1) {//如果接收帧与上一帧时间间隔小于1/25秒则直接舍弃
        return -1;
    }
    self.lastTime = time;
    ///继续处理...
    return 0;
}

丢帧

因为最大内存的缘故,丢帧这一步显得尤为重要,因为视频帧的数据很大使用iPhone11录制的一个视频帧数据大概在2M,高端机型会更大。同时我们还要预留出其它数据,以及数据处理时所需要的内存空间。所以我们需要设置自己的丢帧队列,保证队列内所有的数据加上正在处理及其它数据所需的内存空间小于50M。而丢帧的原则应该是是保留最新帧丢弃最老的一帧。

1.创建丢帧队列,对于队列我们需要保证线程安全,因此对队列的操作需要加锁,加锁的方式有很多这里我采用了信号量的方式。

#import 
#import 

NS_ASSUME_NONNULL_BEGIN

@interface LMSampleVideoQueue : NSObject

@property (nonatomic, strong) NSMutableArray * arrayM;


///是否为空
- (BOOL)isEmpty;
///大小
- (int)size;
///队首元素
- (CMSampleBufferRef)peek;
///入队
- (void)enqueue:(CMSampleBufferRef)sampleBuffer;
///出队 注意:出队的时候必须释放出队对象
- (CMSampleBufferRef)dequeue;

@end

NS_ASSUME_NONNULL_END



#import "LMSampleVideoQueue.h"

@interface LMSampleVideoQueue ()
{
    dispatch_semaphore_t _semphore;
}

@end

@implementation LMSampleVideoQueue

- (id)init{
    if (self = [super init]) {
        _arrayM = [NSMutableArray array];
        _semphore = dispatch_semaphore_create(1);
    }
    return self;
}


///是否为空
- (BOOL)isEmpty{
    dispatch_semaphore_wait(_semphore, DISPATCH_TIME_FOREVER);
    BOOL empty = _arrayM.count == 0;
    dispatch_semaphore_signal(_semphore);
    return empty;
}
///大小
- (int)size{
    dispatch_semaphore_wait(_semphore, DISPATCH_TIME_FOREVER);
    int size = (int)_arrayM.count;
    dispatch_semaphore_signal(_semphore);
    return size;
}
///队首元素
- (CMSampleBufferRef)peek{
    dispatch_semaphore_wait(_semphore, DISPATCH_TIME_FOREVER);
    CMSampleBufferRef sampleBuffer = (__bridge CMSampleBufferRef _Nonnull)([_arrayM lastObject]);
    dispatch_semaphore_signal(_semphore);
    return sampleBuffer;
}
///入队
- (void)enqueue:(CMSampleBufferRef)sampleBuffer{
    dispatch_semaphore_wait(_semphore, DISPATCH_TIME_FOREVER);
    [_arrayM addObject:(__bridge id _Nonnull)(sampleBuffer)];
    dispatch_semaphore_signal(_semphore);
}
///出队
- (CMSampleBufferRef)dequeue{
    if ([self isEmpty]){
        return nil;
    }
    dispatch_semaphore_wait(_semphore, DISPATCH_TIME_FOREVER);
    CMSampleBufferRef sampleBuffer = (__bridge CMSampleBufferRef _Nonnull)([_arrayM firstObject]);
    CFRetain(sampleBuffer);
    [_arrayM removeObjectAtIndex:0];
    dispatch_semaphore_signal(_semphore);
    return sampleBuffer;
}

@end

2.使用队列进行丢帧处理,本例中队列中最大视频帧个数为5。

在SampleHandler中创建视频处理队列;

@interface SampleHandler()
///视频帧处理队列
@property (nonatomic, strong) LMSampleVideoQueue * sampleVideoQueue;

@end

在接收视频帧的方法中进行进队和丢帧的操作。添加丢帧逻辑,获取到的视频帧数据直接进入队列,判断当前队列是否大于限制,如果大于则直接出队丢弃。

//MARK: Handle video sample buffer
- (int)hadleVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    //添加帧率控控制
    ….
    //添加丢帧逻辑
    [self.sampleVideoQueue enqueue:sampleBuffer];
    if (self.sampleVideoQueue.size > 5) {
        CMSampleBufferRef sampleBuffer = [self.sampleVideoQueue dequeue];
        //这里因为 出队的时候 将sampleBuffer进行了CFRetain操作,所以如果该帧丢弃的时候需要释放
        CFRelease(sampleBuffer);
    }
    //继续处理 数据
    .....
    return 0;
}

补帧

有部分版本和机型会有一个特殊的现象,当手机中的画面没有任何变化时,replayKit的视频捕捉回调不会被调用,也就是不会有任何视频帧数据传递给你。

这个时候接收端没有接收到数据可能会认为数据传输已经中断,从而做一些断开连接或结束直播等操作。为了类似的情况出现,我们需要进行补帧检测。

开启定时器检查最后一帧与当前时间的时间差,如果超过0.5秒则认为需要补帧。

//MARK: 检查是否需要发送补帧的消息
- (void)checkNeedAddSampleBuffer{
    CFTimeInterval time = CACurrentMediaTime();
    if (time - self.lastVideoTimeInterval > 0.5) {
       //需要补帧
    }
}

结语

当然,在开发过程中除了上述提到的内容,还有许多其他细节工作需要特别注意。希望我之前分享的信息能够帮助解决大家在使用 ReplayKit 时可能遇到的问题。同时,也希望大家积极分享自己在工作和学习中积累的宝贵经验,以促进共同学习和进步。

你可能感兴趣的:(ios,ReplayKit)