在iOS上构建自己的视频音序器时要避免的陷阱

Rosberry 的iOS开发人员Anton Kormakov

嗨! 我叫Anton,我是Rosberry的iOS开发人员。

不久前,我碰巧从事名为Hype Type的项目,不得不解决一些有关使用视频,文本和动画的有趣任务。 在本文中,我想告诉您一些陷阱,以及如何避免它们在iOS上构建实时视频定序器。


Hype Type使用户可以录制一组短片和/或多张图片,总运行时间为15秒,向制作的剪辑中添加文本,并向其中应用一个可用的动画。

在这种情况下,处理视频的关键方面是,用户应该可以选择彼此无关地管理视频剪辑:更改播放速度,反向,翻转和(可能在以后的版本中)随时随地交换剪辑。

您可以问:“为什么不使用AVMutableComposition ?” 在大多数情况下,您是对的-显然这是一个相对方便的视频音序器。 但是,遗憾的是,它有很多限制,所以我们无法使用它。 首先,不可能在旅途中更改和添加轨道-要获得更改后的视频流,必须重新创建AVPlayerItem并重新初始化AVPlayer 。 同样,使用AVMutableComposition中的图像并不是完美无缺的-将静态图像添加到时间线中,必须使用AVVideoCompositionCoreAnimationTool ,这肯定会增加大量开销,并且会大大降低渲染速度。

简短的网络搜索并未发现其他或多或少合适的解决方案来应对这一任务,因此我们决定开发自己的视频序列发生器。

首先,介绍一下项目中渲染管道的结构。 我必须立即说,我不会详细介绍希望您有所了解,否则这篇文章会引起骚动。 如果您是新手,我建议您应该更加注意著名的框架GPUImage (Obj-C,Swift),这是在OpenGLES上使用清晰插图进行处理的一个很好的起点。

负责通过计时器在屏幕上呈现录制的视频的视图( CADisplayLink )向定序器请求帧。 由于该应用主要用于视频,因此使用YCbCr色彩空间并将每个帧作为CVPixelBufferRef发送会更合乎逻辑。 在获取每个帧时,将创建亮度和色度纹理,并将其发送到着色器程序。 一个人在输出上获得RGB图像,并呈现给用户。 在这种情况下, 刷新循环如下所示:

这里几乎所有内容都是使用包装器构建的(用于CVPixelBufferRefCVOpenGLESTexture等)—这允许将基本的底层逻辑带到单独的层,并大大简化了使用OpenGL的基本步骤。 当然,它有一些缺点(主要是-性能略有下降,灵活性较低),但是,它们并不是那么关键。 值得澄清:self.context —是EAGLContext的简单包装,使CVOpenGLESTextureCache和多线程OpenGL调用的工作更加轻松。 self.source —一个定序器,用于确定应从哪个轨道将哪个帧分配给视图。

现在有几句话关于我们如何组织要渲染的帧的获取。 由于定序器将同时处理视频和图像,因此用通用协议覆盖它是合乎逻辑的。 在这种情况下,音序器的任务是控制播放头,并根据其位置从相关音轨中释放新帧。

实现MovieSourceProtocol的对象处理如何获取帧的逻辑。 这种方案使该系统具有通用性和可扩展性,因为图像和视频处理的唯一区别将是获取帧的方法。

因此,我们的VideoSequencer变得非常简单,主要的问题仍然是识别当前轨道,并使所有轨道具有相同的帧频。

这里的VideoSequencerTrack是实现MovieSourceProtocol的对象的包装,该对象包含不同的元数据。

现在,让我们开始获取帧,并仔细观察一种情况-图像表示。 一个人可以从相机上获取它—在这种情况下,我们可以立即以YCbCr格式获取CVPixelBufferRef,然后仅复制它就足够了(为什么这很重要,我将在后面解释),并根据请求将其返回。 或从图片库中获取图片-在这种情况下,您将不得不跳过一些挂钩并将其手动转换为所需的格式。 从RGB转换为YCbCr可以在GPU中进行处理,但是,现代设备的CPU可以非常快地完成此任务,尤其要记住,应用程序在使用图像之前还会对其进行裁剪和压缩。 其余的很简单。 唯一要做的就是在给定的时间内给出相同的帧。

现在,让我们添加一些视频。 我们决定为其使用AVPlayer-主要是因为它具有易于使用的API来获取帧并处理声音。 通常来说,这听起来很简单,但是有些时候人们需要更加注意。

让我们从一些显而易见的东西开始:

必须创建一个AVURLAsset ,加载有关轨道的信息,创建AVPlayerItem ,等待已准备好播放的通知,并使用适合渲染的参数创建AVPlayerItemVideoOutput-到目前为止已经非常简单了。

但是,这里还有第一个问题-seekToTime的运行速度不够快,并且在循环时会出现明显的延迟。 如果未设置leranceBeforeleranceAfter参数,则除了已经提到的延迟将与定位误差相辅相成的事实外,几乎无法更改任何内容。 这是系统限制,无法完全解决,但我们可以绕开它。 为了实现这一目标,只需准备两个AVPlayerItem并一个一个地使用它们-当其中一个停止播放时,另一个开始,而第一个倒退到开头,因此循环播放。

第二个问题同样令人不愉快,但可以解决-AVFoundation无法正确(无缝且平稳)地支持所有类型文件的回放和反向速度更改,并且如果情况是当一个人从摄像机录制视频时,该格式可以受到控制,在用户从照片库下载视频的情况下,我们无法做到这一点。 让用户等待视频转换实际上是一个不好的选择,而且,这不一定意味着他们会使用这些设置。 因此,决定在后台执行此操作,并用转换后的视频替换原始视频,而似乎没有这样做。

MovieProcessor是一种服务,该服务从阅读器获取帧和音频样本,并将其返回给编写器。 (实际上,它也可以使用GPU处理从阅读器接收到的帧,但仅当整个项目被渲染以将现成的视频与动画帧一起放置时才使用它)。

如果每个剪辑都根据需要准备好进行播放,则延迟会非常明显。 也不能准备所有剪辑以进行播放(由于iOS限制,即同时工作的h264解码器的数量)。 当然,有一种摆脱这种情况的方法,这很简单-我们可以准备几首将在接下来播放的曲目,“清除”那些短期内不应该使用的曲目。

通过这种简单的方法,我们设法获得了连续播放和循环播放。 没错,擦洗肯定会导致一定程度的滞后,但并不是那么关键。

最后,我想介绍一些您可能会遇到的解决类似问题的陷阱。

首先-如果您使用从设备相机收到的像素缓冲区 ,则应尽快释放它们,或者如果以后要使用它们则应复制它们。 否则,视频流将被冻结-我曾经在文档中未找到此约束的描述,但似乎系统会跟踪它提供的像素缓冲区 ,而当旧的缓冲区处于缓冲区时不会提供新的像素缓冲区 。记忆。

第二点-使用OpenGL时的多线程。 OpenGL本身并不是多线程的大朋友,但是可以使用同一个EAGLSharegroup中的不同EAGLContext来传递它 —它将使用户在屏幕上实际看到的内容的渲染逻辑与其他背景处理(例如视频处理,渲染等)相分离,并且以一种简单,快速的方式进行。