背景
2013 年美团外卖成立,至今一直迅猛发展。随着外卖业务量级与日俱增,单一的文字和图片已无法满足商家的需求,商家迫切需要更丰富的商品描述手段吸引用户,增加流量,进而提高下单转化率和下单量。商品视频的引入,在一定程度上可以提升商品信息描述丰富度,以更加直观的方式为商家引流,增加收益。为此,商家端引入了视频功能,进行了一系列视频功能开发,核心功能包含视频处理(混音,滤镜,加水印,动画等)、视频拍摄、合成等,最终效果图如下所示:
自视频功能上线后,每周视频样本量及使用视频的商家量大幅增加,视频录制成功率达 99.533%,视频处理成功率 98.818%,音频处理成功率 99.959%,Crash 率稳定在 0.1‰,稳定性高且可用性强。目前,视频功能已在蜜蜂 App、闪购业务和商家业务上使用。
对于视频链路的开发,我们经历了方案选型、架构设计及优化、业务实践、功能测试、监控运维、更新维护等各个环节,核心环节如下图所示。在开发过程中,遇到了各种技术问题和挑战,下文会针对遇到的问题、挑战,及其解决方案进行重点阐述。
方案选型
在方案选型时,重点对核心流程和视频格式进行选型。我们以功能覆盖度、稳定性及效率、可定制性、成本及开源性做为核心指标,从而衡量方案的高可用性和可行性。
1. 核心流程选型
视频开发涉及的核心流程包括播放、录制、合成、裁剪、后期处理(编解码、滤镜、混音、动画、水印)等。结合商家端业务场景,我们有针对性的进行方案调研。重点调研了业界现有方案,如阿里的云视频点播方案、腾讯云视频点播方案、大众点评 App 的 UGC 方案,及其它的一些第三方开源方案等,并进行了整体匹配度的对比,如下图所示:
阿里和腾讯的云视频点播方案比较成熟,集成度高,且能力丰富,稳定性及效率也很高。但两者成本较高,需要收费,且 SDK 大小均在 15M 以上,对于我们的业务场景来说有些过于臃肿,定制性较弱,无法迅速的支持我们做定制性扩展。
当时的大众点评 App UGC 方案,基础能力是满足的,但因业务场景差异:
比如外卖的视频拍摄功能要求在竖屏下保证 16:9 的视频宽高比,这就需要对原有的采集区域进行截取,视频段落的裁剪支持不够等,业务场景的差异导致了实现方案存在巨大的差异,故放弃了大众点评 App UGC 方案。其他的一些开源方案(比如Grafika等),也无法满足要求,这里不再一一赘述。
通过技术调研和分析,吸取各开源项目的优点,并参考大众点评 App UGC、Google CTS 方案,对核心流程做了最终的方案选型,打造一个适合我们业务场景的方案,如下表所示:
2. 视频格式选型
采用 H.264 的视频协议:H.264 的标准成熟稳定,普及率高。其最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264 的压缩比是 MPEG-2 的 2 倍以上,是 MPEG-4 的 1.5~2 倍。
采用 AAC 的音频协议:AAC 是一种专为声音数据设计的文件压缩格式。它采用了全新的算法进行编码,是新一代的音频有损压缩技术,具有更加高效,更具有“性价比”的特点。
整体架构
我们整体的架构设计,用以满足业务扩展和平台化需要,可复用、可扩展,且可快速接入。架构采用分层设计,基础能力和组件进行下沉,业务和视频能力做分离,最大化降低业务方的接入成本,三方业务只需要接入视频基础 SDK,直接使用相关能力组件或者工具即可。
整体架构分为四层,分别为平台层、核心能力层、基础组件层、业务层。
平台层:依赖系统提供的平台能力,比如 Camera、OpenGL、MediaCodec 和 MediaMuxer 等,也包括引入的平台能力,比如 ijkplayer 播放器、mp4parser。
核心能力层:该层提供了视频服务的核心能力,包括音视频编解码、音视频的转码引擎、滤镜渲染能力等。
基础能力层:暴露了基础组件和能力,提供了播放、裁剪、录屏等基础组件和对应的基础工具类,并提供了可定制的播放面板,可定制的缓存接口等。
业务层:包括段落拍摄、自由拍摄、视频空间、拍摄模版预览及加载等。
我们的视频能力层对业务层是透明的,业务层与能力层隔离,并对业务层提供了部分定制化的接口支持,这样的设计降低了业务方的接入成本,并方便业务方的扩展,比如支持蜜蜂 App 的播放面板定制,还支持缓存策略、编解码策略的可定制。整体设计如下图所示:
实践经验
在视频开发实践中,因业务场景的复杂性,我们遇到了多种问题和挑战。下面以核心功能为基点,围绕各功能遇到的问题做详细介绍。
视频播放
播放器是视频播放基础。针对播放器,我们进行了一系列的方案调研和选择。在此环节,遇到的挑战如下:
1. 兼容性问题
2. 缓存问题
针对兼容性问题,Android 有原生的 MediaPlayer,但其版本兼容问题偏多且支持格式有限,而我们需要支持播放本地视频,本地视频格式又无法控制,故该方案被舍弃。ijkplayer 基于 FFmpeg,与 MediaPlayer 相比,优点比较突出:具备跨平台能力,支持 Android 与 iOS;提供了类似 MediaPlayer 的 API,可兼容不同版本;可实现软硬解码自由切换,拥有 FFmpeg 的能力,支持多种流媒体协议。基于上述原因,我们最终决定选用 ijkplayer。
但紧接着又发现 ijkplayer 本身不支持边缓存边播放,频繁的加载视频导致耗费大量的流量,且在弱网或者 3G 网络下很容易导致播放卡顿,所以这里就衍生出了缓存的问题。
针对缓存问题,引入AndroidVideoCache的技术方案,利用本地的代理去请求数据,先本地保存文件缓存,客户端通过 Socket 读取本地的文件缓存进行视频播放,这样就做到了边播放边缓存的策略,流程如下图:
此外,我们还对 AndroidVideoCache 做了一些技术改造:
优化缓存策略。针对缓存策略的单一性,支持有限的最大文件数和文件大小问题,调整为由业务方可以动态定制缓存策略;
解决内存泄露隐患。对其页面退出时请求不关闭会导致的内存泄露,为其添加了完整的生命周期监控,解决了内存泄露问题。
视频录制
在视频拍摄的时候,最为常用的方式是采用 MediaRecorder+Camera 技术,采集摄像头可见区域。但因我们的业务场景要求视频采集的时候,只录制采集区域的部分区域且比例保持宽高比 16:9,在保证预览图像不拉伸的情况下,只能对完整的采集区域做裁剪,这无形增加了开发难度和挑战。通过大量的资料分析,重点调研了有两种方案:
Camera+AudioRecord+MediaCodec+Surface
MediaRecorder+MediaCodec
方案 1 需要 Camera 采集 YUV 帧,进行截取采集,最后再将 YUV 帧和 PCM 帧进行编码生成 mp4 文件,虽然其效率高,但存在不可把控的风险。
方案 2 综合评估后是改造风险最小的。综合成本和风险考量,我们保守的采用了方案 2,该方案是对裁剪区域进行坐标换算(如果用前置摄像头拍摄录制视频,会出现预览画面和录制的视频是镜像的问题,需要处理)。当录制完视频后,生成了 mp4 文件,用 MediaCodec 对其编码,在编码阶段再利用 OpenGL 做内容区域的裁剪来实现。但该方案又引发了如下挑战。
(1)对焦问题
因我们对采集区域做了裁剪,引发了点触对焦问题。比如用户点击了相机预览画面,正常情况下会触发相机的对焦动作,但是用户的点击区域只是预览画面的部分区域,这就导致了相机的对焦区域错乱,不能正常进行对焦。后期经过问题排查,对点触区域再次进行相应的坐标变换,最终得到正确的对焦区域。
(2)兼容适配
我们的视频录制利用 MediaRecorder,在获取配置信息时,由于 Android 碎片化问题,不同的设备支持的配置信息不同,所以就会出现设备适配问题。
视频合成
我们的视频拍摄有段落拍摄这种场景,商家可根据事先下载的模板进行分段拍摄,最后会对每一段的视频做拼接,拼接成一个完整的 mp4 文件。mp4 由若干个 Box 组成,所有数据都封装在 Box 中,且 Box 可再包含 Box 的被称为 Container Box。mp4 中 Track 表示一个视频或音频序列,是 Sample 的集合,而 Sample 又可分为 Video Smaple 和 Audio Sample。Video Smaple 代表一帧或一组连续视频帧,Audio Sample 即为一段连续的压缩音频数据。(详见mp4文件结构。)
基于上面的业务场景需要,视频合成的基础能力我们采用 mp4parser 技术实现(也可用 FFmpeg 等其他手段)。mp4parser 在拼接视频时,先将视频的音轨和视频轨进行分离,然后进行视频和音频轨的追加,最终将合成后的视频轨和音频轨放入容器里(这里的容器就是 mp4 的 Box)。采用 mp4parser 技术简单高效,API 设计简洁清晰,满足需求。
但我们发现某些被编码或处理过的 mp4 文件可能会存在特殊的 Box,并且 mp4parser 是不支持的。经过源码分析和原因推导,发现当遇到这种特殊格式的 Box 时,会申请分配一个比较大的空间用来存放数据,很容易造成 OOM(内存溢出),见下图所示。于是,我们对这种拼接场景下做了有效规避,仅在段落拍摄下使用 mp4parser 的拼接功能,保证处理过的文件不会包含这种特殊的 Box。
视频裁剪
我们刚开始采用 mp4parser 技术完成视频裁剪,在实践中发现其精度误差存在很大的问题,甚至会影响正常的业务需求。比如禁止裁剪出 3s 以下的视频,但是由于 mp4parser 产生的精度误差,导致 4-5s 的视频很容易裁剪出少于 3s 的视频。究其原因,mp4parser 只能在关键帧(又称 I 帧,在视频编码中是一种自带全部信息的独立帧)进行切割,这样就可能存在一些问题。比如在视频截取的起始时间位置并不是关键帧,会造成误差,无法保证精度而且是秒级误差。以下为 mp4parser 裁剪的关键代码:
为了解决精度问题,我们废弃了 mp4parser,采用 MediaCodec 的方案,虽然该方案会增加复杂度,但是误差精度大大降低。
方案具体实施如下:先获得目标时间的上一帧信息,对视频解码,然后根据起始时间和截取时长进行切割,最后将裁剪后的音视频信息进行压缩编码,再封装进 mp4 容器中,这样我们的裁剪精度从秒级误差降低到微秒级误差,大大提高了容错率。
视频处理
视频处理是整个视频能力最核心的部分,会涉及硬编解码(遵循 OpenMAX 框架)、OpenGL、音频处理等相关能力。
下图是视频处理的核心流程,会先将音视频做分离,并行处理音视频的编解码,并加入特效处理,最后合成进一个 mp4 文件中。
在实践过程中,我们遇到了一些需要特别注意的问题,比如开发时遇到的坑,严重的兼容性问题(包括硬件兼容性和系统版本兼容性问题)等。下面重点讲几个有代表性的问题。
1. 偶数宽高的编解码器
视频经过编码后输出特定宽高的视频文件时出现了如下错误,信息里仅提示了 Colorformat 错误,具体如下:
查阅大量资料,也没能解释清楚这个异常的存在。基于日志错误信息,并通过系统源码定位,也只是发现是了和设置的参数不兼容导致的。经过反复的试错,最后确认是部分编解码器只支持偶数的视频宽高,所以我们对视频的宽高做了偶数限制。引起该问题的核心代码如下:
2. 颜色格式
我们在处理视频帧的时候,一开始获得的是从 Camera 读取到的基本的 YUV 格式数据,如果给编码器设置 YUV 帧格式,需要考虑 YUV 的颜色格式。这是因为 YUV 根据其采样比例,UV 分量的排列顺序有很多种不同的颜色格式,Android 也支持不同的 YUV 格式,如果颜色格式不对,会导致花屏等问题。
3. 16 位对齐
这也是硬编码中老生常谈的问题了,因为 H264 编码需要 16*16 的编码块大小。如果一开始设置输出的视频宽高没有进行 16 字节对齐,在某些设备(华为,三星等)就会出现绿边,或者花屏。
4. 二次渲染
4.1 视频旋转
在最后的视频处理阶段,用户可以实时的看到加滤镜后的视频效果。这就需要对原始的视频帧进行二次处理,然后在播放器的 Surface 上渲染。首先我们需要 OpenGL 的渲染环境(通过 OpenGL 的固有流程创建),渲染环境完成后就可以对视频的帧数据进行二次处理了。通过 SurfaceTexture 的 updateTexImage 接口,可将视频流中最新的帧数据更新到对应的 GL 纹理,再操作 GL 纹理进行滤镜、动画等处理。在处理视频帧数据的时候,首先遇到的是角度问题。在正常播放下(不利用 OpenGL 处理情况下)通过设置 TextureView 的角度(和视频的角度做转换)就可以解决,但是加了滤镜后这一方案就失效了。原因是视频的原始数据经过纹理处理再渲染到 Surface 上,单纯设置 TextureView 的角度就失效了,解决方案就是对 OpenGL 传入的纹理坐标做相应的旋转(依据视频的本身的角度)。
4.2 渲染停滞
视频在二次渲染后会出现偶现的画面停滞现象,主要是 SurfaceTexture 的 OnFrameAvailableListener 不返回数据了。该问题的根本原因是 GPU 的渲染和视频帧的读取不同步,进而导致 SurfaceTexture 的底层核心 BufferQueue 读取 Buffer 出了问题。下面我们通过 BufferQueue 的机制和核心源码深入研究下:
首先从二次渲染的工作流程入手。从图像流(来自 Camera 预览、视频解码、GL 绘制场景等)中获得帧数据,此时 OnFrameAvailableListener 会回调。再调用 updateTexImage(),会根据内容流中最近的图像更新 SurfaceTexture 对应的 GL 纹理对象。我们再对纹理对象做处理,比如添加滤镜等效果。SurfaceTexture 底层核心管理者是 BufferQueue,本身基于生产者消费者模式。
BufferQueue 管理的 Buffer 状态分为:FREE、DEQUEUED、QUEUED、ACQUIRED、SHARED。当 Producer 需要填充数据时,需要先 Dequeue 一个 Free 状态的 Buffer,此时 Buffer 的状态为 DEQUEUED,成功后持有者为 Producer。随后 Producer 填充数据完毕后,进行 Queue 操作,Buffer 状态流转为 QUEUED,且 Owner 变为 BufferQueue,同时会回调 BufferQueue 持有的 ConsumerListener 的 onFrameAvailable,进而通知 Consumer 可对数据进行二次处理了。Consumer 先通过 Acquire 操作,获取处于 QUEUED 状态的 Buffer,此时 Owner 为 Consumer。当 Consumer 消费完 Buffer 后,会执行 Release,该 Buffer 会流转回 BufferQueue 以便重用。BufferQueue 核心数据为 GraphicBuffer,而 GraphicBuffer 会根据场景、申请的内存大小、申请方式等的不同而有所不同。
SurfaceTexture 的核心流程如下图:
通过上图可知,我们的 Producer 是 Video,填充视频帧后,再对纹理进行特效处理(滤镜等),最后再渲染出来。前面我们分析了 BufferQueue 的工作流程,但是在 Producer 要填充数据、执行 dequeueBuffer 操作时,如果有 Buffer 已经 QUEUED,且申请的 dequeuedCount 大于 mMaxDequeuedBufferCount,就不会再继续申请 Free Buffer 了,Producer 就无法 DequeueBuffer,也就导致 onFrameAvailable 无法最终调用,核心源码如下:
5. 码流适配
视频的监控体系发现,Android 9.0 的系统出现大量的编解码失败问题,错误信息都是相同的。在 MediaCodec 的 Configure 时候出异常了,主要原因是我们强制使用了 CQ 码流,Android 9.0 以前并无问题,但 9.0 及以后对 CQ 码流增加了新的校验机制而我们没有适配。核心流程代码如下:
关于码流还有个问题,就是如果通过系统的接口 isBitrateModeSupported(int mode),判断是否支持该码流可能会出现误判,究其原因是 framework 层写死了该返回值,而并没有从硬件层或从 media_codecs.xml 去获取该值。关于码流各硬件厂商支持的差异性,可能谷歌也认为码流的兼容性太碎片化,不建议用非默认的码流。
6. 音频处理
音频处理还括对音频的混音、消声等操作。在混音操作的时候,还要注意音频文件的单声道转换等问题。
其实视频问题总结起来,大部分是都会牵扯到编解码(尤其是使用硬编码),需要大量的适配工作(以上也只是部分问题,碎片化还是很严峻的),所以就需要兜底容错方案,比如加入软编。
线上监控
视频功能引入了埋点、日志、链路监控等技术手段进行线上的监控,我们可以针对监控结果进行降级或维护更新。埋点更多的是产品维度的数据收集,日志是辅助定位问题的,而链路监控则可以做到监控预警。
我们加了拍摄流程、音视频处理、视频上传流程的全链路监控,整个链路如果任何一个节点出问题都认为是整个链路的失败,若失败次数超过阈值就会通过大象或邮件进行报警,我们在适配 Andorid 9.0 码流问题时,最早发现也是由于链路监控的预警。所有全链路的成功率目标值均为 98%,若成功率低于 92%的目标阈值就会触发报警,我们会根据报警的信息和日志定位分析,该异常的影响范围,再根据影响范围确定是否热修复或者降级。
我们以拍摄流程为例,来看看链路各核心节点的监控,如下图:
容灾降级
视频功能目前只支持粗粒度的降级策略。我们在视频入口处做了开关控制,关掉后所有的视频功能都无法使用。我们通过线上监控到视频的稳定性和成功率在特定机型无法保证,导致影响用户正常的使用商家端 App,可以支持针对特定设备做降级。后续我们可以做更细粒度的降级策略,比如根据 P0 级功能做降级,或者编解码策略的降级等。
维护更新
视频功能上线后,经历了几个稳定的版本,保持着较高的成功率。但近期收到了 Sniffer(美团内部监控系统)的邮件报警,发现视频处理链路的失败次数明显增多,通过 Sniffer 收集的信息发现大部分都是 Android 9.0 的问题(也就是上面讲的 Android 9.0 码流适配的问题),我们在商家端 5.2 版本进行了修复。该问题解决后,我们的视频处理链路成功率也恢复到了 98%以上。
总结和规划
视频功能上线后,稳定性、内存、CPU 等一些相关指标数据比较理想。我们建设的监控体系,覆盖了视频核心业务,一些异常报警让我们能够及时发现问题并迅速对异常进行维护更新。但视频技术栈远比本文介绍的要庞大,怎么提高秒播率,怎么提高编解码效率,还有硬编解码过程中可能造成的花屏、绿边等问题都是挑战,需要更深入的研究解决。
未来我们会继续致力于提高视频处理的兼容性和效率,优化现有流程,我们会对音频和视频处理合并处理,也会引入软编和自定义编解码算法。
美团外卖大前端团队将来也会继续致力于提高用户的体验,将在实践过程中遇到的问题进行总结,继续和大家分享。
敬请关注。如果你也对视频技术感兴趣,欢迎加入我们。
参考资料
作者介绍:
金辉、李琼,美团外卖商家终端研发工程师。
本文转载自公众号美团技术团队(ID:meituantech)。
原文链接:
评论 1 条评论