【iOS】JPMovieWriter 基于GPUImageMovieWriter改良的录制器

4,115 阅读4分钟

前言

最近想做个简单的带滤镜功能的录制器,于是乎就想到GPUImage这个远古且强大的滤镜库,其中GPUImageGPUImageMovieWriter可以实现录制功能,用起来比较简单,对于我这种OpenGL小白十分友好。

然而,开发过程中,发现这个录制器的暂停功能是有问题的。

这种情况,最好是换一个库,毕竟GPUImage已经很久没有更新了,以后问题估计会越来越多,但是此时录制相关的逻辑都写得差不多了,这沉没成本有点大。。。

最终决定,专门针对这个录制器进行修复吧。

最终效果

Demo地址:JPMovieWriter_Demo,可暂停、续录、定时、带滤镜美颜的录制器,另附GIF制作。

暂停问题

GPUImageMovieWriter目前存在的问题是:暂停又重录会黑屏或者有画面没声音

经过调试发现:

  1. 多次暂停恢复后,GPUImageVideoCamera- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection这个方法会采集不了音频。

  2. 经百度谷歌发现这篇文章也是发现这个问题,但是注释了文章提到的那段代码后,虽然音频能继续采集,但是多次暂停恢复,偏差会越来越大,最终导致音画不同步(有时候声音卡了,有时候是画面卡了,最后连声音也没了)。

// 文中提及注释的地方
if (offsetTime.value > 0) {
     CFRelease(audioBuffer);
     audioBuffer = [self adjustTime:audioBuffer by:offsetTime];
     CFRetain(audioBuffer);
}

解决方案

GPUImageMovieWriter内部是通过AVAssetWriter实现的录制功能,但AVAssetWriter原本就没有暂停的功能,而GPUImageMovieWriter是通过一些延时算法来实现,然而并不靠谱,多次暂停后偏差会越来越离谱。

继续百度谷歌还有ChatGPT的询问后,最好办法就是每次暂停后,换一个新的AVAssetWriter重新录制,这是最保险的解决方案。

由于GPUImageMovieWriter除了能录制,内部还有对视频帧进行滤镜渲染,为了不影响这部分的逻辑,我的做法就是完全拷贝一个新的GPUImageMovieWriter文件,然后只针对AVAssetWriter的部分进行改造:移除【暂停】这个功能,只能【完成】录制,不同的是,【完成】录制后,可以【再次】录制

这就是JPMovieWriter的由来,一个可以重复录制,并且带滤镜渲染功能的录制器。

新的特性

  • 可重复、重新录制:每次录制都是一个全新的AVAssetWriter,因此每次”暂停“都是新的一个视频,也可以清空已录制部分重新开始录制,且在内部实现,无需重新设置滤镜渲染链(重新设置页面会明显卡顿)。
  • 可设置最大录制时长:录制过程中会不断监听录制的时间,达到最大值会自动停止录制。

JPMovieWriter可以完全代替GPUImageMovieWriter,因为除了AVAssetWriter的部分,其余地方几乎没有改动,并且也做了大量的代码结构性优化,更加面向对象化,方便阅读和调试。

注意点

虽然说能重录,但每次录制都是新的一个视频,所以最终结束录制时,会返回一个存放在Temp文件夹的视频URL数组,鉴于以便应付不同的需求,内部并没有将多段视频合成一个视频的实现,数据将原汁原味保留(合成的逻辑Demo中有实现)。

遵守JPMovieWriterDelegate协议,监听录制过程,录制结束后合成并导出:

// MARK: - <JPMovieWriterDelegate>
extension RecordViewController: JPMovieWriterDelegate {
    // AVAssetWriter重置错误的回调(例如目标路径已存在文件)
    func movieWriter(_ writer: JPMovieWriter, resetFailed recordedURLs: [URL], error: Error) {
        JPProgressHUD.showError(withStatus: (error as NSError).localizedDescription, userInteractionEnabled: true)
    }
    
    // 录制进度的回调
    func movieWriter(_ writer: JPMovieWriter, recording recordDuration: TimeInterval, totalDuration: TimeInterval) {
        let progress = recordDuration / totalDuration
        controlBtn.setProgress(progress, animated: false)
    }
    
    // 录制即将结束的回调
    func movieWriter(_ writer: JPMovieWriter, recordWillDone error: Error?) {
        JPProgressHUD.show(withStatus: error == nil ? "正在结束录制" : "录制发生错误")
        controlBar.isEnabled = false
    }
    
    // 录制已经结束的回调
    func movieWriter(_ writer: JPMovieWriter, recordDone recordedURLs: [URL], error: Error?) {
        if let error = error as? NSError {
            // 录制发送错误
            return
        }
        
        // 录制完成,将视频碎片合成并导出
        VideoTool.mergeVideos(recordedURLs, videoSize: UIConfig.videoSize, contentMode: .scaleAspectFit, maxDuration: CMTime(value: Int64(RecordConfig.videoMaxDuration), timescale: 1)) { mergedFilePath in
            // mergedFilePath:合成好的路径 -> 缓存
        } faild: { kError in
            // 合成或导出失败
        }
    }
}

Demo介绍

JPMovieWriter_Demo:可暂停、续录、定时、带滤镜美颜的录制器,另附GIF制作。没什么技术含量,不过可以玩一下。

Feature:
    ✅ 可暂停、定时、续录;
    ✅ 多种滤镜选择;
    ✅ 基础美颜功能;
    ✅ 支持相册视频的导入和拼接;
    ✅ GIF制作;

TODO:
    🔘 更多的滤镜和特效;
    🔘 更加定制化的GIF制作;
    🔘 视频编辑。

美颜滤镜

GIF制作

生成图:

其他

最后

原本想做一个GIF制作(用于微信斗图)、同时也能随时随地记录心情的App,但是最近有更重要的事情要先去做,还有很多页面和功能都没写完,这个项目只能搁置了。