头图

一、背景&现状

创作者服务平台作为得物为社区创作者提供的PC端视频发布入口,地位非常重要。且随着功能的升级迭代,用户群体也越来越多。但我们偶尔会收到如下反馈:

  • 视频损坏,无法播放
  • 视频模糊
  • 曝光度问题
  • 黑屏,只有声音,没有画面

黑屏.jpeg
黑屏,无法播放

低清晰度.jpeg
低清晰度

曝光异常.jpeg
曝光异常

黑屏只有声音.jpeg
黑屏,只有声音

视频的损坏不仅影响用户体验,还可能导致忠诚用户的流失。用户在浏览时看到错误反馈或者无法播放的视频,容易产生挫败感。

第二,流量的上涨导致此类case越来越多,据统计,自2024年4月份开始,通过创作者平台发布的视频可分发视频量较之前上涨多倍。

为提升视频发布的质量和用户体验,视频发布前的检测能力需尽快落地。

二、业界的做法

在视频内容平台兴盛的今天,视频上传和检测方面已经有了一些有力措施,以确保用户上传的视频质量,最大程度地减少损坏视频对用户体验和平台形象的影响,比如服务端检测,创作者在上传完视频后,会立即触发服务端检测功能,经过等待后会反馈给用户信息,不强卡发布。

这一方案是可行的,但也存在些弊端:

  • 需要视频上传完成才能拿到完整的文件流进行检测,通常在PC上传的视频文件很大,要等到传完作者才能知道自己的视频有问题,然后再上传,再检测
  • 帖子发布时间拉长,增加创作者的等待,从心智上会影响创作者的体验
  • 带宽成本

但这一方案有个优秀的点,如对涉黄、涉恐等元素的视频,能同时被检测到并且禁止分发。

三、得物音视频团队的方案

目前得物音视频团队在上传前的预检测这种场景下已有了一套较为完善的方案,那就是使用C+ffmpeg编写好检测代码后再通过Emscripten工具将其打包成WebAssembly的二进制文件使代码运行在web端。

音视频团队的方案.jpeg

目前这个方案的核心检测能力已应用在得物App发布工具场景使用,其能通过ffmpeg解析视频的元数据,获取其基本信息,如视频尺寸,码率等,能查找目标视频下的视频流,对音频和视频的AVPacket进行验证,检测文件是否损坏,时间轴是否存在异常等等。正常的流程如下:

正常的流程如下.jpeg

经过此过程的检测,我们可以排除绝大多数文件格式存在问题的视频文件,下面列出一些常见的文件结构存在问题或者格式不合规的视频:

1.文件的moov不存在

https://videocdn.poizon.com/creator/og/2386746172_96421a9fd86...

moov不存在.jpeg

2.视频帧的NAL结构异常

https://videocdn.poizon.com/creator/og/2376721105_6dc8f934807...

NAL结构异常.jpeg

3.没有视频轨道

https://videocdn.poizon.com/app/sns-og/2024/video/1753839080_...

没有视频轨道.jpeg

除了可以检测视频文件是否存在问题之外,我们还可以通过预检测获取大量的视频相关的信息:

1.视频的基本信息

  • 宽高、帧率(是否是动态帧率)、码率
  • 旋转角度

2.色域信息

  • 是否是HDR、DP3色域
  • 是8bit/10bit/12bit/16bit

3.视频编码附加信息

  • 当前的视频是否是从其他平台上搬运而来的?
  • 当前的视频是否使用其他的剪辑工具导出的?
  • 我们可以通过识别视频中的metadata中的信息来分析当前的视频来自哪些平台的:

来自抖音.jpeg
来自抖音

来自微信.jpeg
来自微信

来自快手.jpeg
来自快手

综上所述,我们在上传前预检测阶段,可以得到视频的很多信息 + 检测视频是否存在结构问题和格式问题。

四、我们需要解决的问题

虽然整体链路方案非常完善,但是SDK实际落地到web端运行还存在一些问题。

内存泄漏

我们先做个压力测试,在不刷新页面的情况,看下网页端内存如何变化:

1.准备32个50MB以内的小视频,1个800MB的大视频,内存采样为每3s采样一次

2.通过一个个上传,内存占用一直在上升,当传入800MB视频进行检测时,内存占用直接飙升至3G,未被正确释放

旧版SDK内存占用情况.jpeg
旧版SDK压力测试内存占用情况

此时控制台报错,内存溢出,页面卡死,用户必须刷新页面才可继续操作。

内存溢出报错.jpeg
控制台内存溢出报错

array异常占用.jpeg
ArrayBuffer异常占用

大视频无法检测

传入1.9GB的视频文件,控制台直接报错,无法申请1.9GB的内存。

大文件传入报错.jpeg
大文件传入报错

检测速度慢

一步步通过裁剪,缩小视频大小,800MB时视频传入检测成功,耗时94630ms。

五、内存优化方案

针对以上问题,我们一个个来看:

内存溢出:先了解下现有代码中内存是如何被分配和销毁的,下面是部分核心代码。

IO核心代码.jpeg
IO核心代码

大致的流程如下:

大致的流程如下.jpeg
通过流程图可见内存已经被正确的申请和释放了,但是实际表现确是申请的内存一直在被占用,所以可能不是代码逻辑问题,在翻阅了wasm官方设计文档时发现一个issue,其中提到了wasm内存设计方案存在几个问题(该issue还处于open状态):

wasm内存设计问题.jpeg
wasm内存设计问题

所以正是由于第2、3点导致了wasm占用的内存只能被扩大,而无法通过释放被缩小,一旦过多的使用malloc内存占用达到wasm的最大内存限制后,申请必将失败,后续的链路也就无法继续了。那么排除其他因素,通过代码验证一下吧,我们只需要将extract_video_data函数修改一下,直接释放掉接收的内存指针指向的内存块:

修改代码片段.jpeg
修改代码片段

依旧还是之前的物料进行压力测试,可见内存整体增长情况态势与之前别无二致,所以基本可以确认,内存一直占用的问题是wasm底层Memory的设计导致的,所以malloc函数我们肯定是用不了了。

验证内存采集.jpeg
验证内存采集

大视频无法检测:通过分析"IO核心代码"一图可以看到,视频文件是转成ArrayBuffer后通过forEach把一个个的字节塞入了提前申请好的内存中来实现数据传递的,这种方案存在两个问题:

  1. 效率低,不考虑文件转ArrayBuffer的时间,光遍历动不动就上百兆量级的buffer需要的时间都是巨量的,经过测试800MB文件想要全部转化为Uint8ClampedArray,然后写入到wasm内存中耗时大约在14秒左右,这就是导致检测速度慢的一个原因。
  2. ArrayBuffer的size是存在最大限制的,以chrome为例,这个限制是2GB,导致在js侧检测视频理论大小限制为了2GB,wasm侧最大可申请内存也存在限制,大约在1.6GB左右。在使用malloc申请到内存空间后又将其传入avio_alloc_context,avio_alloc_context内部将再次申请buffer_size大小的内存空间进行数据缓存, 从而导致实际检测视频最大为800MB。

所以ArrayBuffer这种方案是不能使用的,天然存在限制。那么到此问题原因都找到了,归根结底都出在文件的IO上,那么是否可以换一种思路,如果不需要任何的数据格式的转换,使wasm环境下的ffmpeg直接读取到文件这样肯定是最省时且高效的。那么ffmpeg能直接读取文件吗,答案是肯定的。

答案是肯定的.jpeg

avformat_open_input可以传入文件路径并能将AVFormatContext自动分配并写入ps。意思是只要能拿到目标文件的文件路径,就能直接调用avformat_open_input读取文件到IOContext,省去了将数据手动塞入IOContext的逻辑,也就意味着能绕过多余的内存的申请和释放,且不再需要进行数据格式的转换,可是怎样才能拿到目标文件路径呢?

因为SDK的运行环境是在web端,web端想要访问本地文件只能通过file类型的input拿到文件对象,那传入blobUrl行不行呢,经过测试,传入blobUrl会报Permission denied 无权限的错误,因为wasm的文件系统有自己的特殊实现,其并不能与JavaScript 直接进行交互,读取文件需要通过nodefs.js,idbfs.js,workerfs.js,proxyfs.js这几个js分别构造的虚拟文件系统才行,其分别是:

  • NODEFS,在node环境中使用的文件系统
  • IDBFS,在web浏览器中可使用的文件系统,强依赖IndexedDB
  • WORKERFS,工作在web浏览器,且只能运行在webWorker中的文件系统
  • PROXYFS,允许通过代理的方式访问本地文件系统或远程文件系统,主要用于将文件操作传递到JavaScript中的其他实现。这种机制使得WebAssembly模块能够与JavaScript代码进行交互,进而访问不同的文件系统或数据源

由此看来想要提供给avformat_open_input目标文件的路径,我们还需将目标文件挂载到一个虚拟文件系统中。那么该如何选择呢?

因为我们的SDK是需要运行在web浏览器中,那么NODEEFS首先就被排除掉了,其次视频的读取检测属于计算密集型任务,是需要运行在webWorker中的,所以WORKERFS与我们的使用场景更加契合,他提供对webWorker中的file和Blob对象的只读访问,而无需将整个数据复制到内存中,非常适合对大型文件的读取,也满足了我们对于快速读取和内存占用少的要求,简直完美。

那么说干就干,webWorker + WORKERFS的方案需要对打包命令和代码进行改造。首先启用WORKERFS需要在wasm的打包命令中添加-l workerfs.js参数,并且导出运行时函数WORKERFS,完整命令如下:

emcc -O3 \
-I ${FFMPEG_DIST_DIR}/include \
-L ${FFMPEG_DIST_DIR}/lib -l avcodec -l avformat -l swresample -l avutil -l workerfs.js\
-I ${CJSON_SOURCE_DIR} \
-s EXPORTED_FUNCTIONS="['_get_video_info', '_extract_video_data']" \
-s WASM=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s EXPORTED_RUNTIME_METHODS=[\"wasmMemory\", \"FS\", \"WORKERFS\", \"ccall\"]"
-fsanitize=address \
-o ${workspaceFolder}/sociality/main.js ${workspaceFolder}/sociality/main.c \
${CJSON_SOURCE_DIR}/cJSON.c ${CJSON_SOURCE_DIR}/cJSON_Utils.c

如果导出成功就能在wasm模块中看到WORKERFS的实例:

worker实例.jpeg

导出成功后想要使用的话只需要在webworker中创建任意文件夹,将目标文件通过mount方法挂载到该文件夹上就行,直接上代码:

webwork使用.jpeg
WORKERFS在Webworker中的使用

然后修改C语言侧extract_video_data方法:

修改c语言侧.jpeg

文件就能正确地被读取和处理了。可见这个方案非常的简洁且省去了巨量的IO操作,效率提升了,但是内存占用问题还存不存在呢,再次跑个压力测试试一试:

老SDK占用情况.jpeg
对比老SDK内存占用情况

用同样的视频物料进行压力测试,得出的内存占用情况如图,可见优化后内存使用在压力测试后一直维持在900MB左右,且继续传入大视频文件不会继续上涨,判断为正常内存占用(绿色线条),检测速度也做了一个粗略的统计,与旧版SDK对比,性能方面,以800MB文件为例,检测时长分别为20s和95s,性能预计提升约78%;2GB视频文件检测时长为61s,对于更大的视频也能轻松应对。至此所有的问题都已解决。目前该功能已上线:得物创作者平台。

六、总结

通过WebAssembly技术的引入与整合,我们在视频损坏检测上迎来了新的机遇。在逐步解决技术挑战的过程中,完善了我们的视频上传流程,并提升了用户体验。展望未来,我们希望继续优化这些功能,确保用户能够在平台上无障碍地上传和分享他们的创作,进一步提升社区的活跃度和用户粘性。

文 / 佳庆&ALBERT

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。


得物技术
854 声望1.5k 粉丝