演示类应用如何降低音视频加载时延
1. 问题概述
演示类应用是用户在工作、学习中高频使用的应用,播放音视频则是用户使用演示类应用进行演示时的高频场景。由于音视频存在体积大、解码复杂等原因,演示类应用加载音视频时面临加载时延长、卡顿等问题。测试数据显示,对于一些大型高清的演示类文档,里面可能包含超过 1G 大小的大视频。而对于一些常用的演示类应用,演示时加载如此大视频的时延可能高达 10 秒,这也意味着应用在长达 10 秒内无法响应用户。一旦演示类应用在演示时出现不响应,用户可能会下意识地反复点击,导致演示过程严重混乱,这在一些重要的演示场合诸如学术报告、商业发布会下是不可接受的致命问题!
2. 优化思路
针对一些常用的演示类应用的音视频加载场景,我们使用 Hitrace 工具抓取泳道图和火焰图,并使用 SmartPerf 工具进行可视化分析。火焰图显示,演示类应用加载大型音视频耗时长的原因是多方面的,并非单一耗时点导致。我们梳理并分析后归纳出以下四大耗时点:
①演示类文档包含的音视频通常“嵌入”在文档中,而文档通常以压缩形式存储,演示时加载音视频首先需要解压缩音视频原文件,解压缩耗时较高;
②演示类文档包含的音视频体积可能相当大,演示类应用在演示时通常会将解压缩后的音视频原文件写入硬盘临时目录,写盘耗时较高;
③演示类应用通常支持丰富的演示功能(比如显示视频首帧、自定义视频播放起始时间等),这些功能普遍涉及获取音视频元数据,因此演示时应用可能会反复获取音视频元数据,这会导致较高的耗时;
④播放音视频需要进行音视频解码,这种计算密集型任务如果采用软解码会耗时较高。HarmonyOS 虽然支持硬解码,但需要额外的适配工作将演示类应用的音视频播放模块接入硬解码。
3. 优化实现
分析清楚了演示类应用加载音视频的主要耗时点,针对这些耗时点我们分别提出了对应的优化方法:
①针对耗时点①、②,即解压缩和写盘耗时高,可以将这两步放入异步线程执行;
②针对耗时点③,即反复获取音视频元数据耗时高,可以缓存音视频元数据以避免重复获取;
③针对耗时点④,即音视频软解码耗时高,可以将对应演示类应用的音视频播放模块接入 HarmonyOS 的硬解码能力。
下面具体来看这些优化点的实现。
3.1 异步解压和写盘的实现
①优化前原流程(主线程执行重任务会阻塞应用响应用户):
②优化后的流程(异步线程不会阻塞应用响应用户):
3.2 音视频元数据缓存的实现
演示类应用普遍面临反复获取音视频元数据耗时高的问题,因此我们想到缓存音视频元数据以降低耗时。然而,音视频格式的种类繁多,每种音视频格式的元数据格式和分布等又大不相同。幸运的是,HarmonyOS 提供了一套强大的音视频编解码服务即 AVCodec Kit,能够使得应用处理音视频时无需关心具体的音视频格式。应用调用 AVCodec Kit 提供的统一的 API 即可完成对各种不同格式的音视频的处理,这也方便了我们在不修改应用侧代码的前提下实现音视频元数据缓存机制。
HarmonyOS 的 AVCodec Kit 在运行时的核心数据结构是 OH_AVSource。应用使用 AVCodec Kit 时通常会先调用 OH_AVSource_CreateWithFD 来创建 OH_AVSource,然后调用 OH_AVSource_GetSourceFormat 或 OH_AVSource_GetTrackFormat 创建 OH_AVFormat。其中,OH_AVSource_CreateWithFD 会读取并解析文件级别的元数据,OH_AVSource_GetTrackFormat 会读取并解析轨道级别的元数据。然而,部分音视频由于格式的缘故(比如 mp4、mov 等),仅读取文件头并不能获取完整的元数据,还需要解一定数量的帧来获取完整元数据,导致较高的耗时。之后,应用便可以调用一些其他接口从 OH_AVFormat 中获取视频的元信息。最后,应用调用 OH_AVSource_Destroy 以销毁 OH_AVSource,调用 OH_AVFormat_Destroy 以销毁 OH_AVFormat。以下是对应的伪代码:
SomeType GetVideoSomeProperty(int fd, ...)
{
OH_AVSource *source = OH_AVSource_CreateWithFD(fd, 0, fileSize);
if (source == nullptr) {
return SomeErrorCode;
}
OH_AVFormat *sourceFormat = OH_AVSource_GetSourceFormat(source);
if (sourceFormat == nullptr) {
return SomeErrorCode;
}
int32_t trackIndex = 0; // 以 0 号轨为例
OH_AVFormat *trackFormat = OH_AVSource_GetTrackFormat(source, trackIndex);
if (trackFormat == nullptr) {
return SomeErrorCode;
}
/*
* 这里调用 API 从 sourceFormat 中获取一些信息
*/
OH_AVFormat_Destroy(sourceFormat);
OH_AVFormat_Destroy(trackFormat);
OH_AVSource_Destroy(source);
return SomeResult;
}
①优化前原流程:
②增加缓存后的流程:
音视频元数据缓存模块主要根据音视频文件的 mtime(最近修改时间)判断对应文件是否被修改。若文件已被修改则缓存失效,走原流程;否则缓存命中,直接拷贝对应数据结构,跳过复杂的解析流程。
③音视频元数据缓存的主要技术难点和解决方案:
实现音视频元数据缓存的方法就是拷贝整个 OH_AVSource 结构体。然而,OH_AVSource 是超大结构体,内部包含很多子结构体、指针、buffer 等等,有些 void* 指针还需要仔细推敲不同情况下分别指向哪种结构体。此处简要总结了实现拷贝的过程中遇到的难点及其解决方案:
(1)const 指针:通常指向 static const 全局变量,直接拷贝;
(2)指针指向子结构体:实现并调用对应子结构体的拷贝函数;
(3)字符串:使用 strdup 函数拷贝;
(4)数组:创建数组,逐个元素拷贝;
(5)Dictionary、Buffer、Packet 等底层数据结构:调用已有的对应 copy、ref、clone 函数拷贝;
(6)void* 指针:最为复杂,需要走读代码理解其指向。通常 void* 指针意味着多种后端,比如不同的视频格式(mp4、avi 等)各自会有专属的数据结构和专属的处理函数,此时就需要对每种后端对应的数据结构分别实现拷贝,并将 void* 指针的拷贝分发至对应后端。目前已在四个地方发现三类后端:Format 后端、Codec 后端、URL 后端。目前音视频元数据缓存已支持全部的 Codec 后端、仅支持 File 这一种 URL 后端、支持多种 Format 后端(mp4、mov、avi、flv、rm、mv 等)但尚未全部支持。
④音视频元数据缓存的效果:
3.3 音视频硬解码的实现
HarmonyOS 音视频硬解码的架构如下图:
以下是 HarmonyOS 音视频硬解码相关函数的解释:
①创建解码器实例 OH_VideoDecoder_CreateByName();
②调用 OH_VideoDecoder_RegisterCallback() 设置回调函数;
OH_AVCodecOnError 解码器运行错误;OH_AVCodecOnStreamChanged 码流信息变化,如码流宽、高变化;OH_AVCodecOnNeedInputBuffer 运行过程中需要新的输入数据,即解码器已准备好,可以输入数据;OH_AVCodecOnNewOutputBuffer 运行过程中产生了新的输出数据,即解码完成;
③调用 OH_VideoDecoder_Configure() 配置解码器;
④调用 OH_VideoDecoder_SetSurface 设置 surface;
⑤调用 OH_VideoDecoder_Prepare() 解码器就绪;
⑥调用 OH_VideoDecoder_Start() 启动解码器;
⑦调用 OH_VideoDecoder_PushInputBuffer() 写入解码码流;⑧调用 OH_VideoDecoder_RenderOutputBuffer() 或 OH_VideoDecoder_RenderOutputBufferAtTime() 显示并释放解码帧,或调用 OH_VideoDecoder_FreeOutputBuffer() 释放解码帧;
⑨调用 OH_VideoDecoder_Destroy() 销毁解码器实例,释放资源。
4. 总结
演示类应用中的音视频加载是一个典型的高负载但要求低时延的场景,既有IO密集型瓶颈,也有计算密集型瓶颈,并且涉及了多种经典的优化手段,包括异步并行、缓存数据、硬件加速等等。音视频加载场景并不局限于演示类应用,在多媒体技术早已渗透进每一个角落的今天,许多应用均可能涉及这一场景。大体积、超大体积音视频加载时延长的问题,具有一定的普遍性。因此,本文提出的优化手段具有一定的推广价值,值得更多应用尝试。
更多推荐
所有评论(0)