iOS录屏汇总

iOS11以后系统自带录屏功能,可以在系统设置中打开。而之前的系统可以通过连接Mac电脑,使用QuickTime进行录制。当然可能还有很多其它方法,但这里要总结的是App运行期间内部录屏的问题,
其实市面上很多App都已经支持了这个功能,尤其是游戏和直播几乎都支持,而且还支持服务器分发。


录屏流程

其实iOS系统已经提供了一个录屏的类,用于把视频帧数据与音频帧数据输入进去,最后合成一个视频。
这个类就是AVAssetWriter。代码大致如下:

        AVAssetWriter *assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:outputFilePath] fileType:AVFileTypeQuickTimeMovie error:nil];

    NSDictionary *videoOutputSetting = @{AVVideoCodecKey:AVVideoCodecTypeH264, AVVideoWidthKey:@(720), AVVideoHeightKey:@(1280), AVVideoCompressionPropertiesKey:@{AVVideoAverageBitRateKey:@(2000000), AVVideoProfileLevelKey:AVVideoProfileLevelH264HighAutoLevel, AVVideoMaxKeyFrameIntervalKey:@(30)}};
    AVAssetWriterInput *videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoOutputSetting];
    videoWriterInput.expectsMediaDataInRealTime = YES;
    videoWriterInput.transform = CGAffineTransformMakeScale(1, -1);


    NSDictionary * videoAdaptorSetting = [NSDictionary dictionaryWithObjectsAndKeys:  [NSNumber numberWithInt:kCVPixelFormatType_32BGRA], kCVPixelBufferPixelFormatTypeKey,
                                          @720, kCVPixelBufferWidthKey,
                                          @1280, kCVPixelBufferHeightKey,
                                          CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks),                                      kCVPixelBufferIOSurfacePropertiesKey,
                                          nil]
    AVAssetWriterInputPixelBufferAdaptor  *videoWriterPixelBuffferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] initWithAssetWriterInput:videoWriterInput sourcePixelBufferAttributes:videoAdaptorSetting];

    [assetWriter addInput:videoWriterInput];

详细内容可以参考官方例子RosyWriter,最终影响视频大小的是就分辨率和码率=【文件大小】(MB) * 1024 * 8 / 【时间】(秒)。
值得注意的是AVAssetWriter在录制视频时,并不能实时地获取视频文件的大小。我们只能通过设置的码率来估算大小。


截屏录制

顾名思义,就是通过截图获得Image,然后把Image转成CVPixelBufferRef,最终写入AVAssetWriter,生成视频,这里一般会使用一个定时器CADisplayLink来不断截图,同时也做好丢帧策略。视频一般最大帧率为30FPS,再高的话区别不大,所以定时器一般会定在1/30秒执行一次会比较合适,但是在一些低端机上,有时候会达不到30FPS,此时可以选择合适的丢帧策略

例如分机型选择不同的FPS,或者判断每帧间隔必须达到3ms等,都是不错的丢帧策略

  • renderInContext

代码大致如下:

- (UIImage *)fetchScreenshot {
    UIImage *image = nil;
    if (self.captureLayer) {
        CGSize imageSize = self.captureLayer.bounds.size;
        UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
        CGContextRef context = UIGraphicsGetCurrentContext();
        [self.captureLayer renderInContext:context];
        image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    }
    return image;
}

再将Image转成CVPixelBufferRef,代码如下:

- (CVPixelBufferRef)pixelBufferFromCGImage:(CGImageRef)image {
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
                             nil];

    CVPixelBufferRef pxbuffer = NULL;

    CGFloat frameWidth = CGImageGetWidth(image);
    CGFloat frameHeight = CGImageGetHeight(image);

    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault,frameWidth,frameHeight,kCVPixelFormatType_32ARGB,(__bridge CFDictionaryRef) options, &pxbuffer);

    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
    NSParameterAssert(pxdata != NULL);

    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(pxdata, frameWidth, frameHeight, 8,CVPixelBufferGetBytesPerRow(pxbuffer),rgbColorSpace,(CGBitmapInfo)kCGImageAlphaNoneSkipFirst);

    NSParameterAssert(context);
    CGContextConcatCTM(context, CGAffineTransformIdentity);
    CGContextDrawImage(context, CGRectMake(0, 0,frameWidth,frameHeight),  image);
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);
    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);

    return pxbuffer;
}

说明:该方法仅能录制UIKit相关的内容,无法录制OpenGL渲染的内容,另外该方法可以通过调整截图Size来优化相关性能,性能比较优秀。


  • drawViewHierarchy

代码大致如下:

- (UIImage *)snapshot:(UIView *)view
{
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, 0);
    [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

说明:该方法可以录制UIKit与OpenGL渲染的内容,截图视图Size成正比,全屏截图时性能比较差,平均耗时45ms左右。


  • createIOSurface

代码大致如下:

 @interface UIWindow (ScreenRecorder)
 + (IOSurfaceRef)createScreenIOSurface;
 + (IOSurfaceRef)createIOSurfaceFromScreen:(UIScreen *)screen;
 - (IOSurfaceRef)createIOSurface;
 - (IOSurfaceRef)createIOSurfaceWithFrame:(CGRect)frame;
@end
    CVPixelBufferRef buffer = NULL;
    IOSurfaceRef surface = [self.window createIOSurface];
    backingData = surface;

    NSDictionary *pixelBufferAttributes = @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};
    status = CVPixelBufferCreateWithIOSurface(NULL, surface, (__bridge CFDictionaryRef _Nullable)(pixelBufferAttributes), &buffer);
    if (!(status == kCVReturnSuccess && buffer)) {
        return ;
    }

说明:IOSurfaceRefUIWindow的私有API,可以直接获取CVPixelBufferRef对象,支持OpenGL与UIkit内容的渲染,性能相当优秀,但由于是私有API,无法上架。可以做为内部体验用。


系统录屏API

目前系统开放的录屏接口只有replaykit,其在iOS9推出的功能,同时又在iOS11中进行了完善,我们暂且把iOS11中完善的版本称为replayKit2,replaykit有诸多的限制:

  • 不支持AVPlayer播放的视频录制
  • 不支持模拟器
  • 无法自定义RPPreviewViewController预览视图
  • 无法修改录制视频保存路径

replayKit2做了比较大的改进,首先可以获取每帧数据,之前是做不到的。

- (void)startCaptureWithHandler:(nullable void(^)(CMSampleBufferRef sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error))captureHandler completionHandler:(nullable void(^)(NSError * _Nullable error))completionHandler;

也可以不需要RPPreviewViewController,同时性能也有较大提升。

另一个系统接口是破解AirPlay协议,然后对App进行重签名的方式分发给主播使用,由于这种方式无法上架AppStore,这里暂不研究,同时随着系统的升级,也会存在协议变化的问题。在iOS11以后,其实大家可以使用replaykit2来进行直播了。


其它录屏方式

如果需要录制的内容都是OpenGL渲染出来的,可以通过OpenGL FBO来进行录屏,其原理就是通过共享内存的方式,进行离屏渲染,来进行录屏。

- (void)createDataFBO
{
    glActiveTexture(GL_TEXTURE1);
    glGenFramebuffers(1, &movieFramebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, movieFramebuffer);

    // Code originally sourced from http://allmybrain.com/2011/12/08/rendering-to-a-texture-with-ios-5-texture-cache-api/


    CVPixelBufferPoolCreatePixelBuffer (NULL, [assetWriterPixelBufferInput pixelBufferPool], &renderTarget);

    /* AVAssetWriter will use BT.601 conversion matrix for RGB to YCbCr conversion
     * regardless of the kCVImageBufferYCbCrMatrixKey value.
     * Tagging the resulting video file as BT.601, is the best option right now.
     * Creating a proper BT.709 video is not possible at the moment.
     */
    CVBufferSetAttachment(renderTarget, kCVImageBufferColorPrimariesKey, kCVImageBufferColorPrimaries_ITU_R_709_2, kCVAttachmentMode_ShouldPropagate);
    CVBufferSetAttachment(renderTarget, kCVImageBufferYCbCrMatrixKey, kCVImageBufferYCbCrMatrix_ITU_R_601_4, kCVAttachmentMode_ShouldPropagate);
    CVBufferSetAttachment(renderTarget, kCVImageBufferTransferFunctionKey, kCVImageBufferTransferFunction_ITU_R_709_2, kCVAttachmentMode_ShouldPropagate);

    CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault, [_movieWriterContext coreVideoTextureCache], renderTarget,
                                                  NULL, // texture attributes
                                                  GL_TEXTURE_2D,
                                                  GL_RGBA, // opengl format
                                                  (int)videoSize.width,
                                                  (int)videoSize.height,
                                                  GL_BGRA, // native iOS format
                                                  GL_UNSIGNED_BYTE,
                                                  0,
                                                  &renderTexture);

    glBindTexture(CVOpenGLESTextureGetTarget(renderTexture), CVOpenGLESTextureGetName(renderTexture));
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(renderTexture), 0);
}

renderTarget就是这个共享内存为CVPixelBufferRef的对象,这种方法仅能录制OpenGL渲染的内容,对于UIKit的内存则没有办法,如果想把UIkit与OpenGL结合录制,有一种比较高效的方法,上层使用SceneKit,使用SCNRender直接把UI渲染到OpenGL的Context里面。

SceneKit类似的还有SpriteKitSpriteKit是专门2D游戏开发的框架,但SpriteKit的渲染类SKRenderer不能使用openGL渲染,只能使用苹果自己的metal,所以不适用录屏的使用。

SCNRender的Context可以是UIImage、CALayer、UIColor等内容,这样就可以做到高效录屏,SCNRender选择正交投影,使用默认Scene,在渲染时调用renderAtTime:方法就可以渲染到OpenGL里面了。


总结

录屏方案 优点 缺点
OpenGL FBO 性能优异 仅能录制OpenGL内容
renderInContext 性能可以 仅能录制UIKit内容
drawViewHierarchy 支持录制OpenGL&UIKit内容 性能差
createIOSurface 支持录制OpenGL&UIKit内容,性能优异 私有API,全屏录制
replaykit2 支持录制OpenGL&UIKit内容,性能优异 iOS11以上,全屏录制

通过OpenGL FBOSceneKit的结合,可以有效解决录屏性能问题,也可以提升开发效率。

2017-11-10 16:02266