Osheep

时光不回头,当下最重要。

iOS-你不知道的系统录屏框架-ReplayKit

前段时间公司项目有个要求,让找一个第三方的录屏框架,实现画板绘画过程的录制,github找了好久,但没有让人十分满意又符合项目要求的,所以就研究了下系统提供的ReplayKit,这里整理下,发出来,用到的书友们可以借鉴学习。

  • The ReplayKit framework provides the ability to record video and audio within an app. Users can then share the resulting recording with other users through social media, and you can build app extensions for live broadcasting your content to sharing services. ReplayKit is not compatible with AVPlayer content.

  • 简单说,RP – ReplayKit能够用来在你的app中录制视频和音频,可以通过社交媒体分享你录制的作品。

  • 但它目前还有如下一些不足:

    1. 不支持AVPlayer播放的视频录制
    2. 不支持模拟器
    3. 无法自定义RPPreviewViewController预览视图
    4. 无法修改录制视频保存路径
    5. 只支持 iOS9.0+ 版本
  • 几张效果图

    1. 录屏权限提醒
      《iOS-你不知道的系统录屏框架-ReplayKit》

      1.png
    2. 开始录制
      《iOS-你不知道的系统录屏框架-ReplayKit》

      2.png
    3. 录制完成,预览 – 这个vc按钮样式什么的没法定制很不好,除非找到私有api
      《iOS-你不知道的系统录屏框架-ReplayKit》

      3.png
    4. 录制的视频保存在系统相册,然后读取
      《iOS-你不知道的系统录屏框架-ReplayKit》

      4.png

1. ReplayKit相关的class和protocol

* classes:

RPPreviewViewController – 录制完成预览vc

The RPPreviewViewController class displays a user interface that allows users to preview and edit a screen recording created with ReplayKit. The preview view controller is passed into the completion handler for stopRecordingWithHandler: upon a successful recording.

RPScreenRecorder – 真正录屏的类

Use the RPScreenRecorder class to implement the ability to record audio and video of your app.

* protocols:

RPPreviewViewControllerDelegate – 录制完成预览vc的代理回调协议 – 录制完成预览页面

Implement the RPPreviewViewControllerDelegate protocol to respond to changes to a screen recording user interface, represented by a RPPreviewViewController object.

RPScreenRecorderDelegate – 录屏功能回调协议 – 录屏开始 / 结束 / 出错接受通知

Implement the RPScreenRecorderDelegate protocol to receive notifications from an RPScreenRecorder object. The delegate is called when recording stops or there is a change in recording availability.

  • ReplayKit毕竟是比较新的一个框架,所以它能实现的功能也比较简单单一,方法和协议也比较少。

2.实现思路

  • 主要难点是录制完成后,保存到系统相册,怎么区分并且分类你录制的视频,比如录屏1,2,3
  • 如何播放录制的视频,注意系统相册的video是不支持将url传递来达到播放的,所以必须找到你录制的视频,把它拷贝到你的沙盒目录下,顺便进行格式转换、压缩等,以便以后读取,分类。
《iOS-你不知道的系统录屏框架-ReplayKit》

简单实现流程

3.具体实现过程 – 关键代码

1. 录制前的检测: 设备是否是真机、iOS版本是否>9.0、录屏硬件是否可用
    // 真机模拟器检测
    struct Platform {
        static let isSimulator: Bool = {
            var isSim = false
            #if arch(i386) || arch(x86_64)
                isSim = true
            #endif
            return isSim
        }()
    }

    // Alert提示框
    private func showUnsuportAlert(message: String, showVC: UIViewController){
        let ok = UIAlertAction(title: "好的", style: .Default) { (action) in
        }
        let alert = UIAlertController(title: nil, message: message, preferredStyle: .Alert)
        alert.addAction(ok)
        showVC .presentViewController(alert, animated: true, completion: nil)
    }

    // 是真机还是模拟器
    func isPlatformSuport(showVC: UIViewController) -> Bool{
        if Platform.isSimulator {
            showUnsuportAlert("录屏功能只支持真机设备", showVC: showVC)
            return false
        }
        return true
    }

    // 系统版本是9.0
    func isSystemVersionSuport(showVC: UIViewController) -> Bool{
        if NSString(string: UIDevice.currentDevice().systemVersion).floatValue < 9.0 {
            showUnsuportAlert("录屏功能要求系统版本9.0以上", showVC: showVC)
            return false
        }
        return true
    }

    // 检查是否可用
    func isRecorderAvailable(showVC: UIViewController) -> Bool {
        if !RPScreenRecorder.sharedRecorder().available{
            showUnsuportAlert("录屏功能不可用", showVC: showVC)
            return false
        }
        return true
    }
2. 录制功能类: 开始、结束
    // 开始
    func startCapture(){
        print("录屏初始化...")
        let recorder = RPScreenRecorder.sharedRecorder()
        recorder.delegate = self
        // 关键方法
        recorder.startRecordingWithMicrophoneEnabled(false) { (error) in
            print("录屏开始")
            self.delegate?.didStartRecord()
            if let error = error {
                self.delegate?.didStopWithError(error)
            }
        }
    }

    // 完成
    func stopCapture(){
        let recorder = RPScreenRecorder.sharedRecorder()
        // 关键方法
        recorder.stopRecordingWithHandler { (previewController, error) in
            if let error = error {
                self.delegate?.didStopWithError(error)
            } else if let preview = previewController{
                print("录屏完成")
                preview.previewControllerDelegate = self
                self.delegate?.didFinishRecord(preview)
                print("显示预览页面...")
            }
        }
    }

    // 自定义录屏manager协议   
    protocol ScreenCaptureManagerDelegate: class{
        func didStartRecord() // 正在录制
        func didFinishRecord(preview: UIViewController) // 完成录制
        func didStopWithError(error: NSError) //发生错误停止录制
        func savingRecord() // 保存
        func discardingRecord() // 取消保存
    }
3. 代理方法: RPPreviewViewControllerDelegate, RPScreenRecorderDelegate
extension ScreenCaptureManager: RPScreenRecorderDelegate{
    // 录制出错而停止
    func screenRecorder(screenRecorder: RPScreenRecorder, didStopRecordingWithError error: NSError, previewViewController: RPPreviewViewController?) {
        delegate?.didStopWithError(error)
    }
}

extension ScreenCaptureManager: RPPreviewViewControllerDelegate{
    // 取消回调
    func previewControllerDidFinish(previewController: RPPreviewViewController) {
        print("previewControllerDidFinish")
        dispatch_async(dispatch_get_main_queue()) {
            self.delegate?.discardingRecord() // 取消保存
        }
    }

    // 保存-分享等的回调
    func previewController(previewController: RPPreviewViewController, didFinishWithActivityTypes activityTypes: Set<String>) {
        if activityTypes.contains("com.apple.UIKit.activity.SaveToCameraRoll") {
            dispatch_sync(dispatch_get_main_queue(), {
                self.delegate?.savingRecord() // 正在保存
            })
        }
    }
}
4. 录屏完成后的视频处理逻辑类:CaptureVideoManager

1. import AssetsLibrary
2. 你可以在将录制的视频转存到沙盒目录后,删除系统相册里的视频,以减小空间,但每次会提示用户是否删除,用户体验很不好,后来没找到解决方法就直接不删除了

    // 导出视频
    private func outputVideo(url: NSURL, board: Blackboard, model: CaptureVideo, success: (() -> Void)?){
        let asset = AVAsset(URL: url)
        let fmanager = NSFileManager.defaultManager()
        let path = blackboardPath(board.id) + "/\(BlackboardFileName.video.rawValue)"
        if !fmanager.fileExistsAtPath(path) {
            guard model.URL != nil else{
                return
            }

            let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality)
            session?.outputFileType = AVFileTypeQuickTimeMovie //MOV
            session?.outputURL = NSURL(fileURLWithPath: path)
            session?.exportAsynchronouslyWithCompletionHandler({
                ()in
                PrintUtil.Log("导出视频成功", args: nil)

                dispatch_async(dispatch_get_main_queue(), {
                    if success != nil{
                        success!()
                    }
                })
            })
        }
    }

    // 视频操作 - model转换
    private func operatingVideoAsset(board: Blackboard, asset: ALAsset, success: (() -> Void)?){
        let name = asset.defaultRepresentation().filename()
        let url = asset.defaultRepresentation().url()
        let model = CaptureVideo(URL: url, videoName: name, board: nil) //录制视频model类
        // 保存预览图
        let img = UIImage(CGImage: asset.aspectRatioThumbnail().takeUnretainedValue())
        let path = blackboardPath(board.id) + "/\(BlackboardFileName.thumbnail.rawValue)"
        if NSFileManager.defaultManager().createFileAtPath(path, contents: UIImagePNGRepresentation(img), attributes: nil){

            PrintUtil.Log("保存视频预览图成功", args: [path])
            // 导出视频
            outputVideo(url, board: board, model: model, success: success)
        }
    }

    // 提取刚才录制的视频 - NSEnumerationOptions.Reverse 倒序遍历,时间
    func saveVideo(board: Blackboard, success: (() -> Void)?){
        assets.enumerateGroupsWithTypes(ALAssetsGroupSavedPhotos, usingBlock: {
            (group, stop) in
            if group != nil{
                PrintUtil.Log("group", args: [group])
                // 过滤
                group.setAssetsFilter(ALAssetsFilter.allVideos())
                group.enumerateAssetsWithOptions(NSEnumerationOptions.Reverse, usingBlock:             { (asset, index, stop2) in
                    if asset != nil{
                        let name = asset.defaultRepresentation().filename()
                        // 特征点,一般为app boundle id
                        if name.containsString("com.founder.OrangeClass"){ 
                            // 是否是要的视频
                            self.operatingVideoAsset(board, asset: asset, success: success)
                            PrintUtil.Log("asset", args: [asset,index])
                            // 停止遍历 - 倒序遍历,第一个就是刚才录制的
                            stop2.memory = true
                        }
                    }
                })
            }

        }) { (error) in
            PrintUtil.Log("error", args: [error])
        }
    }

这里差不多了,可能没有讲太清楚,希望用到的童鞋可以参考下,共同学习探讨。

  • 稍后整理下代码,上传到github

有空接着写完吧 懒懒~

点赞