文章的大纲:html
探索意味着前方都是未知的事物,但愿这篇文章可以带着读者,一块儿回顾我探索的过程,同时学习遇到的种种未知的事物。如今开始吧。前端
在一些视频点播网站的视频播放界面,用户将鼠标移动到进度条上时,会弹出一个浮窗并展现了一张图片,意在告诉用户这张图片是鼠标所在位置的时间点对应的视频画面。并且目前的实现,用户体验是足够好的,预览图出现的速度很是快,并且不一样时间范围展现的也是不一样的画面,达到模拟实时预览的效果,如图: git
经过翻看各大视频网站,发现弹窗中的画面通常是一张背景图片,打开背景图片的连接看到的是一张视频缩略拼图。打开 Chrome 浏览器 DevTools 的 Elements 面板,能够看到: github
FFmpeg 是一个很是强大的音视频处理工具,它的官网是这么介绍的: web
我写了一个 C 应用程序,实现了如何使用 FFmpeg 生成视频缩略拼图。它接收一个视频文件路径做为参数,获取到参数后,使用 FFmpeg 的方法读取视频文件并通过一系列步骤(解复用 -> 帧解码 -> 帧转码… )处理以后,会在当前目录生成一张拼图。 总结了一下程序逻辑执行的步骤:算法
以上是生成视频缩略拼图程序逻辑执行的步骤。由于这部分与这篇文章的主题无关,因此就不贴代码了。感兴趣的同窗能够前往 GitHub - VVangChen/video-thumbnail-sprite 查看完整源码,也能够下载可执行文件在本地运行。api
上面讲到了如何使用 FFmpeg 生成视频缩略拼图,接下来向探索的目标再进一步。在常见的实现方式中,最重要的一环就是生成视频缩略拼图,那么能不能将这最重要的一环在浏览器中实现呢?答案是确定的,而且应该能联想到最近比较火热的 WebAssembly,由于它就是为此而生。浏览器
WebAssembly,是一门被设计成能够运行在浏览器中的编译目标语言,意在经过移植将原生应用的能力带到浏览器中。若是想要了解更多,能够浏览它的官网WebAssembly 或者前往 WebAssembly | MDN 进行学习。接下来要讲的,是如何将上面实现的,生成视频缩略拼图的 C 程序移植到 Chrome 中。缓存
简单来讲,“单纯的移植”只需如下两步:bash
其中 emconfigure、emmake 和 emcc 都是 Emscripten 的 emsdk 提供的工具,经过 emsdk 能够很是简单地将 C/C++ 程序移植到浏览器中。使用 emcc 可以将 C 程序编译成 wasm 模块,同时还会生成一个 JS 文件,它暴露了一系列工具方法,使得 JS 可以访问 C 模块导出的方法,访问 C 模块的内存。emsdk 的安装方法参考 Download and install。 安装好以后咱们开始移植咱们的 C 程序:
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 端的代码,移植后的应用逻辑大概是这样的:
接下来简单分析一下移植后的 Web 应用。由于这一节不是今天的主题,就不贴完整的代码了,感兴趣的同窗能够前往GitHub - VVangChen/video-thumbnail-sprite查看完整源码及示例。
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次就行。 现有的方案都是基于拼图来实现的,可是事实上,如今的需求并不须要预先生成全部画面的缩略图,只须要生成那一秒的就行。考虑到已经可以生成全部画面的缩略图,那么只生成一张确定是能够实现的。 另外,既然如今只须要生成一张缩略图,而不是全部视频画面的拼图,那么是否是只须要获取这一张缩略图的数据就行?答案也是确定的。因此如何获取某个时间点的视频缩略图数据,是此次探索成败的关键。 先来看下最终实现的程序,执行逻辑是怎样的:
其中 2 - 5 与上一节实现的方法相同,就再也不赘述,查看完整源码请前往 :github.com/VVangChen/v… 。 剩下的内容中,主要讲下如何实现第 1 步,获取鼠标指针所选时间点的帧数据。它能够被拆解为两个步骤:
在这里,只考虑目前比较流行的 mp4 格式的视频文件。因此能够将第一步转换成:如何在 mp4 格式的视频文件中,计算出某个时间点对应的帧数据的偏移量及其大小? 这涉及到对 mp4 文件结构的解析。mp4 文件是由一个个连续的被称为 ‘box’ 的结构单元构成的,每个 ‘box’ 由 header 和 data 组成,header 至少包含大小和类型,data 能够是 ‘box’ 自身的数据,也能够是一个或多个 ‘box’。不一样的 ‘box’ 有不一样的做用,对于计算帧数据的偏移量,主要须要用到如下几个 ‘box’:
经过这些 ‘box’,按照必定的算法就能够获得帧数据的偏移量和大小:
实现了如何计算帧数据在 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 画布上绘制并展现,用户就能够看到所选时间点的视频画面了。 至此,就实现了使用纯前端技术实现实时的视频帧预览。
感谢可以耐心看完的读者。确定有人会问了,作这件事的意义在哪里?我能回答是既然是探索,前方确定也应该是未知的,路的尽头在走到以前谁也不清楚是什么,况且探索的脚步并未中止。 目前实现的程序还存在不少问题,好比:
接下来会着手解决这些问题,并继续探索如何将其应用于生产环境,使其更具实际使用的价值。因此探索的脚步并未中止,敬请期待,共勉。
若是文章有错误或待商榷的地方,欢迎指出或讨论,感谢!