美文网首页iOS相关视频开发
数数GPUImage里那些未知的坑(一)

数数GPUImage里那些未知的坑(一)

作者: CoderHenry | 来源:发表于2018-04-19 17:11 被阅读151次

           记录一些实际开发中,比较难碰到与遇见的坑。大多是较深的操作,一般项目都碰不上,只有对自定义视频定制性较高的项目才会碰上。其他的像首帧黑帧,常见崩溃之类问题的社区有大把现成解决方案,不提也罢。本文着重于记录,并提供个人的一些解决问题思路,不喜勿喷~ 写得不好,还请多多包涵~

    1、GPUImageVideoCamera 的回调

        videoCamera提供了willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer的代理方法,并在videoProcessQueue串行队列里回调上层做视频帧处理。值得注意的是

    (1)、出现willOutputSampleBuffer回调的帧数与实际设置的帧数相差太远

    captureOuput的回调是在cameraProcessingQueue里,并用信号量进行控制,先看源码:

    (GPUImaeVideoCamera源码)

    看代码可知,如果上层处理视频帧时间过久,就会产生丢帧。如果上层处理时间每次都超时,会导致每两帧都会掉一帧,卡顿就会很明显。具体解决方案就要根据产品的需求和上层处理时长来定了,我们假设第二帧回调到时,信号量仍未释放,则丢弃了该帧,但很快信号量释放了,在信号量释放到第三帧到达之前,时间是浪费的,所以可以尝试调大相机的帧数,或者优化上层处理时间。要想完美控制帧数,只能在录制时调节recordWriter的帧数来实现。

    (2)、willOutputSampleBuffer:回调的buffer与你想像中一样吗?

         通过GPU...Camera拿到的buffer,我们用GPUImageView来显示,一般我们设置outputImageOrientation = UIDeviceOrientationPortrait,因为我们看到的是竖的视频帧,我们就以为得到的是竖的视频帧,但实际上这是个坑。

         系统默认采集方向为向左转90度,GPU...Camera是通过改变输出方向来调整,也就是你此时采集到的帧其实是翻转的,只不过GPU..Camera通过outputImageOrientation,在输出的时候又帮你做相应的翻转。所以此时,如果你拿看到的是竖的视频帧做处理,就会出问题了。坑就坑在你看到的和你想的完全是两回事。

        解决方法:使用AVCaptureConnection自己控制视频帧的方向,由于GPU...Camera的videoOutput只有子类有权限使用,那就继承它,

    记得把outputImageOrientation 设为UIDeviceOrientationUnknown; 翻转摄像头时,记得做相应切换操作,要不会恢复左翻转90度。

    ps:仅对sampleBuffer有特殊要求时才需要这么做

    2、非主线程执行同步主队列操作导致死锁

        我们先来看一段代码,谈论下他的可行性

       咋一看,老铁,没毛病。非主队列同步操作主队列没问题。博主在之前也是这么认为的。那我们把这段代码放到GPUVideoImageCamera的回调中试试:

    你会发现,主线程卡死了!在非主队列,同步主队列,这有毛病吗?明面没毛病。那为什么主线程卡死,而且还不崩?只有一种情况:互等 -- 我等你,你等我(思路)。GPU...Camera的回调的串行队列(videoProcessQueue)在等主队列同步完成后回调,那主队列在等谁呢?我们来看看堆栈:

    (主队列同步串行队列videoProcessQueue)

    主队列同步等待串行队列videoProcessQueue!这就形成了互等,主队列同步等待串行队列,串行队列同步等待主队列,死锁。故意在串行队列sleep(1)也就是为了卡这个bug(实际上操作都可能卡死主线程,操作时间越长越容易出现)。看来,在非主队列同步调用主队列也并不是一个绝对安全的做法。此处,也咨询过其他iOS开发者,同步操作前有没办法判断是不是在等待另一个队列,得到的答案都是NO,若有有缘人知道安全的做法,还请留言赐教!

    解决方案:此处不宜通过同步主队列的方法来处理事件,一定要同步处理的话,可以自己创建一个队列,或者用GPU...Camera的videoProcessQueue.

    3、队列的同步异步操作判断

    GPUImage提供了自建队列同步的操作方法

    同步操作队列:

    runSynchronouslyOnVideoProcessingQueue(void (^block)(void))

    runSynchronouslyOnContextQueue(GPUImageContext *context, void (^block)(void))

    我们再来看看实现:

    runSynchronouslyOnContextQueue实现方法类似,不再贴图。重点就在这句:dispatch_get_specific([GPUImageContext contextKey]);--  dispatch_get_specific与dispatch_set_specific,先通过dispatch_set_specific给一个队列做一个标识,然后在当前队列dispatch_get_specific当前队列是不是该标识的队列,不明白的自己科普。

    我们看看[GPUImageContext contextKey],该标识是openGLESContextQueueKey.

    通过[GPUImageContext contextKey]拿到的key都是一样的,这会有什么后果呢?就是在videoProcessingQueue或与contextQueue的队列上通过dispatch_get_specific([GPUImageContext contextKey]),他一定会返回YES。这就会出问题了:

    假设我在contextQueue里希望同步到一个里videoProcessingQueue操作,我们调用runSynchronouslyOnVideoProcessingQueue,但其实由于dispatch_get_specific([GPUImageContext contextKey])一定返回YES,block里的操作会直接进行,你所希望的操作将会在contextQueue里进行,而不会在videoProcessingQueue中进行。因为没法同步videoProcessingQueue,如果此时你有希望在videoProcessingQueue同步的操作,就会出现偏差了。GPU的异步队列大同小异,就不再重复举例。

            解决方案:(二选一)

            1、改写源码 

            2、同等对待这两个queue,把他们当同个queue(实际上GPUImage在同步上就是这么对待的)。

    4、recordWriter 与 imageMovie配合的坑

    大部分人都是直接Ctrl + c , Ctrl + V,加上,项目用得少,也基本不会有问题。但当你随意调换顺序或是自定义的时候,就可能会出现你摸不着头脑的坑。先简单说下imageMovie的创建方式(算有点小坑):

    1、- (id)initWithAsset:(AVAsset *)asset;(有图像和声音回调,但不会播放声音,得自己添加播放,但可以直接与recordWriter配合)

    2、- (id)initWithPlayerItem:(AVPlayerItem *)playerItem;(自己创建AVPlayer去解析和控制item,可以做到和播放器一样,播放也有声音。但只有图像回调,没有声音回调,因为系统只提供了AVPlayerItemVideoOutput。如果和recordWriter配合,制作完成后得自己添加音轨)

    3、- (id)initWithURL:(NSURL *)url;本质就是initWithAsset,有兴趣自己看底层

    下面说说坑(出现概率低于1%。):

    (一)、野指针,出现如图

    (野指针一) (野指针二)

                bug排查:synchronizedMovieWriter或movie已经被销毁,很绕,这也是GPUImage出问题难排查的原因,全是block,你调我,我调他,他还调你,重点你还不知道他什么时候会调,不想看的(看了还得研究源码)直接看解决方案。只分析野指针一,二的原理差不多。

            首先:movieWriter通过 

            videoQueue = dispatch_queue_create("com.sunsetlakesoftware.GPUImage.videoReadingQueue", GPUImageDefaultQueueAttribute());

            和块:videoInputReadyCallback()进行下一帧的读取操作movie里的readNextVideoFrameFromOutput,而终止录制用的是runSynchronouslyOnContextQueue;也就是存在掉用finish后继续读取下一帧的可能,

           当movie里的synchronizedMovieWriter赋值videoInputReadyCallback,同时读取下一帧,此时发现isKeepLooping为NO,在block里调用本身的endProcessing(见下图),此时来到崩溃的地方,由于block并没有强引用,而此时如果synchronizedMovieWriter已经置空(上层销毁),就会产生野指针。野指针二出现的情况就是movie先置空,writer后置空

                解决方案:      

          调了这两句后,上层保证你的recordMovie,movieWriter不会马上致空(其实大概也就0.01秒的时间)即可

       (二)、上层渲染没问题,record完成却全部变黑帧

            出现这情况,逻辑上是先判断是否有videoTrack,没videoTrack肯定是业务逻辑出问题,上层排查。有videoTrack,渲染没问题,却全部黑帧:时间戳出问题(思路),定位

            [assetWriter startSessionAtSourceTime:frameTime];

            正常这里应该是0,看看frameTime是否异常,或者是这里根本没有走就直接进行write了。

            再定位

            [assetWriterPixelBufferInput appendPixelBuffer:pixel_buffer         withPresentationTime:frameTime],打印frameTime一步步排查

            可能出现此异常的操作:先processing,再startRecording

            [self.recordMovie startProcessing];

            [self.movieWriter startRecording];

            出现概率低,需要天时地利人和,但他就是存在。

            解决方案:把这两句代码顺序调过来就可以了

            PS:此处出现状况较多,仅提供排查思路

      (三)、录制完成后,通过主线程回调上层,通过视频地址获取的视频马上播放后,发现无法播放,隔一段时间又能播放了(出现概率极低,当处理较大的视频是概率稍大)

            bug排查:估计还是这两货出问题(不确定)

                [self.recordMovie startProcessing];

                [self.movieWriter startRecording];

            由于马上回调上层,可能movieWriter还在操作该路径,导致马上播放失败

            解决方案:

            延时回调主线程(0.1秒);

    (四)、设置_movie.audioEncodingTarget = _writer后,出现

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[AVAssetWriterInput appendSampleBuffer:] Cannot append sample buffer: Input buffer must be in an uncompressed format when outputSettings is not nil'

    设置outputSetting支持PCM,两个方法,一个改源码,不想破坏源码的写子类,重写GPUImageMovie里的createAssetReader:

    原创文章,转载请注明出处


    相关文章

      网友评论

      本文标题:数数GPUImage里那些未知的坑(一)

      本文链接:https://www.haomeiwen.com/subject/wffwkftx.html