探索纯前端实现实时的视频帧预览

栏目: 后端 · 前端 · 发布时间: 6年前

内容简介:这篇文章主要记录了我探索纯前端实现实时的视频帧预览的过程,并且总结我是如何利用 WebAssembly,将 FFmpeg 的视频处理能力带到 Web 平台中的。 文章中至少给出了以下问题的答案:探索意味着前方都是未知的事物,希望这篇文章能够带着读者,一起回顾我探索的过程,同时学习遇到的种种未知的事物。现在开始吧。在一些视频点播网站的视频播放界面,用户将鼠标移动到进度条上时,会弹出一个浮窗并展示了一张图片,意在告诉用户这张图片是鼠标所在位置的时间点对应的视频画面。而且目前的实现,用户体验是足够好的,预览图出现
探索纯前端实现实时的视频帧预览

这篇文章主要记录了我探索纯前端实现实时的视频帧预览的过程,并且总结我是如何利用 WebAssembly,将 FFmpeg 的视频处理能力带到 Web 平台中的。 文章中至少给出了以下问题的答案:

  • 如何使用 FFmepg 生成视频缩略拼图?包含 FFmpeg 是什么?
  • 如何使用 WebAssembly 移植 C 程序到浏览器中?
  • 如何在浏览器中解析 mp4 文件以获取其中某一帧的字节数据?
  • 如何发送 HTTP 范围请求?

文章的大纲:

  • 什么是视频帧预览?
  • 常见的实现方式
  • 进阶的实现
  • 最终的实现
  • 总结

探索意味着前方都是未知的事物,希望这篇文章能够带着读者,一起回顾我探索的过程,同时学习遇到的种种未知的事物。现在开始吧。

什么是视频帧预览?

在一些视频点播网站的视频播放界面,用户将鼠标移动到进度条上时,会弹出一个浮窗并展示了一张图片,意在告诉用户这张图片是鼠标所在位置的时间点对应的视频画面。而且目前的实现,用户体验是足够好的,预览图出现的速度非常快,而且不同时间范围展示的也是不同的画面,达到模拟实时预览的效果,如图:

探索纯前端实现实时的视频帧预览

这样的视频画面预览功能,我把它称为视频帧预览。而我要探索的,就是如何通过前端技术来实现视频帧预览中的每一个环节,并且实现真正的实时预览。在探索之前,先来了解一下目前常见的实现方式。

常见的实现方式

通过翻看各大视频网站,发现弹窗中的画面一般是一张背景图片,打开背景图片的链接看到的是一张视频缩略拼图。打开 Chrome 浏览器 DevTools 的 Elements 面板,可以看到:

探索纯前端实现实时的视频帧预览

将链接打开是这样一幅图:

探索纯前端实现实时的视频帧预览

可以看出这张图是由视频中不同画面的缩略图拼接而成的,我将它称为视频缩略拼图。那么,这样的拼图又是如何生成的?其中一个方法是使用 FFmpeg。

FFmpeg 是一个非常强大的音视频处理工具,它的官网是这么介绍的:

探索纯前端实现实时的视频帧预览

注:一个用于录制,转换,流式传输音视频的完整的跨平台解决方案。

我写了一个 C 应用程序,实现了如何使用 FFmpeg 生成视频缩略拼图。它接收一个视频文件路径作为参数,获取到参数后,使用 FFmpeg 的方法读取视频文件并经过一系列步骤(解复用 -> 帧解码 -> 帧转码… )处理之后,会在当前目录生成一张拼图。 总结了一下程序逻辑执行的步骤:

  1. 初始化输入:这一步主要做了一些初始化的工作,比如获取入参、读取视频文件、初始化必要的对象并分配内存等;
  2. 初始化解码器:获取适合视频文件的解码器并打开它;
  3. 按指定的间隔时间读取视频帧数据:根据入参指定的间隔时间,从视频文件中读取帧数据;
  4. 按指定的列数排列数据:根据入参指定的拼图每行包含的图片数,排列解码后的帧数据;
  5. 生成拼图文件:将排列好的拼图的字节序列写入图片文件中。

以上是生成视频缩略拼图程序逻辑执行的步骤。因为这部分与这篇文章的主题无关,所以就不贴代码了。感兴趣的同学可以前往 GitHub - VVangChen/video-thumbnail-sprite 查看完整源码,也可以下载可执行文件在本地运行。

进阶的实现方式

上面讲到了如何使用 FFmpeg 生成视频缩略拼图,接下来向探索的目标再进一步。在常见的实现方式中,最重要的一环就是生成视频缩略拼图,那么能不能将这最重要的一环在浏览器中实现呢?答案是肯定的,并且应该能联想到最近比较火热的 WebAssembly,因为它就是为此而生。

WebAssembly,是一门被设计成可以运行在浏览器中的编译目标语言,意在通过移植将原生应用的能力带到浏览器中。如果想要了解更多,可以浏览它的官网WebAssembly 或者前往WebAssembly | MDN 进行学习。接下来要讲的,是如何将上面实现的,生成视频缩略拼图的 C 程序移植到 Chrome 中。

简单来说,“单纯的移植”只需以下两步:

  1. 使用 emconfigure 和 emmake 配置并编译 FFmpeg;
  2. 使用 emcc 编译上面的 C 程序。

其中 emconfigure、emmake 和 emcc 都是 Emscripten 的 emsdk 提供的工具,通过 emsdk 可以非常简单地将 C/C++ 程序移植到浏览器中。使用 emcc 能够将 C 程序编译成 wasm 模块,同时还会生成一个 JS 文件,它暴露了一系列 工具 方法,使得 JS 能够访问 C 模块导出的方法,访问 C 模块的内存。emsdk 的安装方法参考Download and install。 安装好之后我们开始移植我们的 C 程序:

  1. 先进入事先下载好的 FFmpeg 目录,运行以下命令配置编译程序:
emconfigure ./configure --prefix=/usr/local/ffmpeg-web --cc="emcc" --enable-cross-compile --target-os=none --arch=x86_64 --cpu=generic \
  --disable-ffplay --disable-ffprobe --disable-asm --disable-doc --disable-devices --disable-pthreads --disable-w32threads --disable-network \
  --disable-hwaccels --disable-parsers --disable-bsfs --disable-debug --disable-protocols --disable-indevs --disable-outdevs --enable-protocol=file
复制代码
emmake make && sudo make install
emcc -o web_api.html web_api.c preview.c \
-s ASSERTIONS=1 -s VERBOSE=1 \
-s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=67108864 \
-s WASM=1 \
-s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
`pkg-config --libs --cflags libavutil libavformat libavcodec libswscale`
复制代码

我们可以看到运行命令生成了 wasm 和 js 文件,这样我们就算完成了移植。

但是 “单纯的移植” 后的应用是不能直接运行的,因为在浏览器中程序不能直接操作用户的本地文件。所以需要稍微改造一下之前的 C 程序,以及增加 Web 端的代码,移植后的应用逻辑大概是这样的:

  1. 获取用户上传的视频数据;
  2. 将视频数据传给 C 模块;
  3. C 模块获取到视频后,生成视频缩略图;
  4. 将视频缩略图返回给 Web 程序;
  5. Web 端获取到视频缩略图,通过 Canvas 将其绘制出来。

接下来简单分析一下移植后的 Web 应用。因为这一节不是今天的主题,就不贴完整的代码了,感兴趣的同学可以前往 GitHub - VVangChen/video-thumbnail-sprite 查看完整源码及示例。

移植后的 Web 应用,与之前的 C 程序最大的不同在于视频数据获取的方式。之前的 C 程序可以直接加载本地文件,而现在需要在 Web 端将用户上传的视频缓存到内存中,再通过调用 C 模块暴露的方法,将内存地址传递给 C 模块。C 模块在获取到内存地址后,从内存之中读取视频文件的数据,然后进行之后的处理。除了视频数据获取的方式不同,C 模块也不再需要生成图片文件,而是将排列好的 RGB 数据通过内存返回给 Web 端。 主要看下 Web 端与 C 模块交互的部分,关键代码如下:

function generateSprite(data, cols = 5, interval = 10) {
  // 获取 c 模块暴露的 getSpriteImage 方法
  const getSpriteImage = Module.cwrap('getSpriteImage', 'number',
                  ['number', 'number', 'number', 'number']);
  const uint8Data = new Uint8Array(data.buffer)
  // 分配内存
  const offset = Module._malloc(uint8Data.length)
  // 将数据写入内存
  Module.HEAPU8.set(uint8Data, offset)
  // 调用 getSpriteImage,得到生成的拼图地址
  const ptr = getSpriteImage(offset, uint8Data.length, cols, interval)

  // 从内存中取出拼图的内存地址
  const spriteData = Module.HEAPU32[ptr / 4]
  ...
  ...
  ,,,
  // 获取拼图数据
  const spriteRawData = Module.HEAPU8.slice(spriteData, spriteData + size)

  // 释放内存
  Module._free(offset)
  Module._free(ptr)
  Module._free(spriteData)

  return ...
}
复制代码

另外,如果 Web 端想要调用 C 模块的方法,需要在 C 代码中使用宏标记想要暴露给 Web 端的方法,如下所示:

EMSCRIPTEN_KEEPALIVE // 用来标记想要暴露给 Web 端的方法
SpriteImage *getSpriteImage(uint8_t *buffer, const int buff_size, int cols, int interval);
复制代码

这样就可以在 JS 中直接调用 C 模块的 getSpriteImage 方法,等待 C 模块生成视频缩略拼图后返回给 Web 端,然后在 Canvas 画布中将其绘制并展示。可以前往 GitHub - VVangChen/video-thumbnail-sprite 查看完整源码及示例。

最终的实现

在上一节中,完成了在浏览器中独立地实现完整的视频帧预览功能。那么离探索的目标只差一步,就是真正实时地生成视频预览图。 开头讲过,真正实时有两个条件,一是不预先准备好图片,而是在鼠标移到进度条上时再去生成;二是每个时间点的预览图都是不同的,就是说展示的图片一定是那一秒的视频画面。 其中第一个条件只是时间上的延迟,所以只要在鼠标移到进度条上时再触发生成拼图的动作就行;而第二个条件,只要缩短拼图中缩略图的采样频率到1秒1次就行。 现有的方案都是基于拼图来实现的,但是事实上,现在的需求并不需要预先生成所有画面的缩略图,只需要生成那一秒的就行。考虑到已经能够生成所有画面的缩略图,那么只生成一张肯定是可以实现的。 另外,既然现在只需要生成一张缩略图,而不是所有视频画面的拼图,那么是不是只需要获取这一张缩略图的数据就行?答案也是肯定的。所以如何获取某个时间点的视频缩略图数据,是这次探索成败的关键。 先来看下最终实现的程序,执行逻辑是怎样的:

  1. 获取鼠标指针所选时间点对应的视频画面的帧数据;
  2. 将帧数据传给 C 模块;
  3. C 模块使用 FFmpeg 解码帧数据并转成 RGB 数据;
  4. 将生成的 RGB 数据传回给 Web 端;
  5. 在 Canvas 画布上绘制 RGB 数据。

其中 2 - 5 与上一节实现的方法相同,就不再赘述,查看完整源码请前往 : github.com/VVangChen/v… 。 剩下的内容中,主要讲下如何实现第 1 步,获取鼠标指针所选时间点的帧数据。它可以被拆解为两个步骤:

  1. 因为视频画面的帧数据属于视频文件的一部分,它在视频文件中应该是一段连续的字节数据序列,所以在第一步,需要 计算出帧数据在视频文件中的偏移量,以及帧数据的长度
  2. 第二步,需要发起一个请求, 获取视频文件 [偏移量, 偏移量 + 帧数据长度] 范围内的数据

在这里,只考虑目前比较流行的 mp4 格式的视频文件。所以可以将第一步转换成: 如何在 mp4 格式的视频文件中,计算出某个时间点对应的帧数据的偏移量及其大小? 这涉及到对 mp4 文件结构的解析。mp4 文件是由一个个连续的被称为 ‘box’ 的结构单元构成的,每一个 ‘box’ 由 header 和 data 组成,header 至少包含大小和类型,data 可以是 ‘box’ 自身的数据,也可以是一个或多个 ‘box’。不同的 ‘box’ 有不同的作用,对于计算帧数据的偏移量,主要需要用到以下几个 ‘box’:

  • moov:保存着视频编解码需要的数据
  • mdhd:保存着视频相关的元数据
  • stts:用于查询 sample 的时间表示
  • stss:用于查询文件所有关键帧的索引
  • stsc:用于查询 sample 所属块的索引和 sample 在块中的索引
  • stco:用于查询 sample 所在 chunk 的偏移量
  • stsz:用于查询 sample 的大小
  • mdat:存放着音视频码流数据

通过这些 ‘box’,按照一定的算法就可以得到帧数据的偏移量和大小:

  1. 首先需要获得 mp4 文件根结构,moov 的位置可能在文件的开头或者结尾,知道了它的位置之后就可以获得 moov 的数据;
  2. 解析 moov,获得上面提到的所有 box 数据并解析;
  3. 获取帧在码流中的时间表示;
  4. 通过时间计算帧在字节序列中的索引;
  5. 通过索引获得帧所属块在字节序列中的索引和它在块中的索引;
  6. 计算帧所属块在字节序列的偏移量;
  7. 通过它在块中的索引,计算它在块中的偏移量;
  8. 通过它在块中的偏移量和帧所属块在字节序列中的偏移量得到。

实现了如何计算帧数据在 mp4 文件中的偏移量,以及帧数据的长度。接下来进行第二步,获取视频文件 [偏移量, 偏移量 + 帧数据长度] 范围内的数据。它可以被转换成下面这个问题: 如何获取 URL 资源的某部分数据? 它可以通过 HTTP 的范围请求来实现。如果资源服务器支持,只需要在 HTTP 请求中指定一个 Range 请求头,它的值是想要获取的资源数据的范围,看下示例:

function fetchRangeData(url, offset, size) {
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest()
    xhr.onload = (e) => {
      resolve(xhr.response)
    }
    xhr.open('GET', url)
    // 设置 Range 请求头
    xhr.setRequestHeader('Range', `bytes=${offset}-${offset + size - 1}`)
    xhr.responseType = 'arraybuffer'
    xhr.send()
  })
}
复制代码

通过调用 fetchRangeData 函数,传入资源的 URL,想要请求的字节偏移量和字节大小,就可以获得你想要的字节序列。

至此,已经实现了获取某个时间点的视频帧数据,但这并不意味着一定能够生成用户想要的预览图。即使从获取到的部分帧数据大小也可以发现,它们非常小,有的才几十字节,显然不够描述一幅图片。如果把这些帧数据直接传给 FFmpeg,也无法成功被解码。这又是为什么呢? 这是因为在 H.264 编码中,帧主要分为三种类型: 1. I 帧:独立解码帧,又称关键帧(Intra frame),表示解码它不依赖其他帧 2. P 帧:前向预测帧,表示解码它需要参照帧序列中的上一帧 3. B 帧:双向预测帧,表示解码它需要参照帧序列中的上一帧和后一帧 显而易见,P 帧和 B 帧相对于 I 帧,会小很多。这也是为什么一些帧只需要几十个字节。 从上面帧类型的描述可以得知,在解码时帧与帧之间的依赖(参照)关系,如果不是 I 帧,就无法被独立解码。要解码非 I 帧,就需要获取到它参照的所有帧。在 H.264 编码的码流中,帧序列中的帧是以参照关系排列的,参照关系也决定了帧解码的顺序,因为被参照帧的解码顺序一定在参照帧的前面。 因为只有 I 帧能够独立解码,所以它在一组参照关系中一定是被排在最前面。如果想要解码非 I 帧,只需要获取到所选帧到它所在参照组中最前面的 I 帧之间的所有帧。一般将可以独立解码的参照组序列称为一组帧(GOP),它一般是两个 I 帧之间的一段帧序列。如示例图所示:

探索纯前端实现实时的视频帧预览

查看获取帧序列的代码请前往: github.com/VVangChen/v… 获取到鼠标指针所选时间点的帧数据后,将其传给 C 模块,生成 RGB 数据后返回给 Web 端,然后在 Canvas 画布上绘制并展示,用户就可以看到所选时间点的视频画面了。 至此,就实现了使用纯前端技术实现实时的视频帧预览。

总结

感谢能够耐心看完的读者。肯定有人会问了,做这件事的意义在哪里?我能回答是既然是探索,前方肯定也应该是未知的,路的尽头在走到之前谁也不清楚是什么,何况探索的脚步并未停止。 目前实现的程序还存在很多问题,比如:

  • 每次生成预览图,所有帧数据都需要重新获取;
  • 获取的帧数据只被利用于预览功能,浏览器播放视频时又会重新获取这些数据;
  • 编译生成的 wasm 文件体积过大;
  • 没有利用多线程以防止阻塞主进程;
  • 存在内存泄露;

接下来会着手解决这些问题,并继续探索如何将其应用于生产环境,使其更具实际使用的价值。所以探索的脚步并未停止,敬请期待,共勉。

如果文章有错误或待商榷的地方,欢迎指出或讨论,感谢!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Dive Into Python 3

Dive Into Python 3

Mark Pilgrim / Apress / 2009-11-6 / USD 44.99

Mark Pilgrim's Dive Into Python 3 is a hands-on guide to Python 3 (the latest version of the Python language) and its differences from Python 2. As in the original book, Dive Into Python, each chapter......一起来看看 《Dive Into Python 3》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具