夏日花火,谈谈移动端背景视频实现

8,068 阅读7分钟

八月,依然需要空调续命,这个季节二次元大概率在放烟火,给我映像最深的应该是《声之形》中硝子坠落时的烟火片段,绚丽而安静。

先来看看上面的图片,这是使用 ffmpeg 截取视频片段转成的 gif,用 gif 来实现背景视频也是一种选择,一张图片就能解决的事,为什么还要研究背景视频呢?

为什么不使用 GIF

先来看一组数据:

源视频分辨率为 1920×1080,25fps 时长为 6s,体积是 1.4M,转成同分辨率同帧率的 gif 图片,体积居然要 26M !!

而且由于 gif 格式只支持 256 色,mp4 转 gif 画面的分辨率虽然不变但画质有很大损失,上图能看到明显的像素块效果。

所以日常视频转 gif 时都会进行一定的压缩处理,页首 gif 经过 640×360 12fps 的压缩处理过的体积是 2.3M,还是大于视频的体积。

# mp4 转 gif
ffmpeg -y -i demo.mp4 -s 640x360 -r 12 demo_mini.gif

相较于视频 gif 有两个比较明显的缺点:

  • 同等视觉效果的 gif 图片体积远大于视频文件
  • gif 受限于 256色无法保证视觉效果的细节

所以说时代变了,gif 这一页可以揭过了,看看视频~

video 元素的背景实现

我们先来看看使用视频来实现背景的关键要素:

  1. 自动播放
  2. 去除掉 video 的播放控件
  3. video 元素需要放置于底层

好像没什么问题,可以开整了:

<section>
    <video
        loop
        autoplay="autoplay"
        preload="auto"
    >
        <source src="./demo.mp4" type="video/mp4">
    </video>
    <div>mask</div>
</section>

video 元素上面绝对定位改了一个遮罩层,loop 循环播放,且不配置 controls 是不会展示控制组件的,嗯,看起来正常,不过视频并没有自动播放,机智的你又查查资料,发现视频自动播放是有限制的,只有无音轨的视频或者静音 video 元素才能播放,于是乎加上了 muted 你写下了:

<section>
    <video
        loop
        muted
        autoplay="autoplay"
        preload="auto"
    >
        <source src="./demo.mp4" type="video/mp4" >
    </video>
    <div>mask</div>
</section>

视频自动播放了,电脑上看起来没什么问题,要不换手机试试?

打开 charles 挂上代理,用果子的 Safari 打开,看起来没啥问题,换微信试试,然后你惊讶的发现:

  • 视频元素的层级错乱
  • 视频无法自动播放了

好吧找找解决方法

视频元素的层级错乱解决方案

为 video 设置内联播放标识 playsinline,然后为了兼容需要加上各种前缀:

  • playsinline="true"
  • webkit-playsinline="true"
  • mtt-playsinline="true"

指定微信端的页面播放器类型:

  • x5-video-player-type="h5-page"

视频无法自动播放

微信端可以监听页面的 WeixinJSBridgeReady 来触发视频播放

<section>
    <video
        muted
        loop
        class="player"
        autoplay="autoplay"
        preload="auto"
        playsinline="true"
        webkit-playsinline="true"
        mtt-playsinline="true"
        x5-video-player-type="h5-page"
    >
        <source src="./demo.mp4" type="video/mp4">
    </video>
    <div class="mask">
        遮罩层
    </div>
</section>
<script>
    window.onload = function () {
        const player = document.querySelector('.player');
        // 自动播放
        document.addEventListener('WeixinJSBridgeReady', player.play());
    }
</script>

微信试试,稳得很。保险起见试试安卓端的各种浏览器吧。

果不其然,video 还是那个 video,浏览器已经不是那个 chrome 了。

几乎每个国产品牌的安卓机自带浏览器都有对 video 元素都有些定制化处理,常见且不限于:

  • 静音视频无法自动播放
  • 无法隐藏控制条(即使未设置 controls
  • 视频为顶级元素无法被覆盖
  • 视频默认为弹层窗口播放
  • 更有甚者给你加广告
  • ...

video 元素在安卓端特性并不统一,目前并没有找到很好解决方案。

既然 video 不行,换 canvas 不就好了,video draw canvas 不是分分钟的事。

video draw canvas ?

放个图你们感受下:

同志们,要把 video 绘制到 canvas 得先播放呀!

即使是有些可以自动播放的视频的浏览器,将 video 绘制 canvas 时,video 元素必须是可见的,所以隐藏的 video 元素也是做不到的。

既然这样,我们可以将视频截取一张张的图片,然后打个压缩文件,通过 canvas 绘制出来?

等等这不就是视频文件吗?还犯得着去截图播放吗?所以我们需要的是将视频文件解码成可播放的图片帧!

video decode

视频解码的方案简单来说就是将视频文件解码成一一帧帧的图片,在 canvas 上按一定速率绘制出来。

前人栽树,后人乘凉,github 搜罗下找到几个可用的库

WasmVideoPlayer 原型演示,ffmepg + wasm 实现,移动端性能较差

WXInlinePlayer 依赖于 flv, 且体积较大

Broadway 作者实现了一套安卓端的 h264 解码器(c 语言实现 + wasm),支持特定编码的 mp4 格式的视频,且支持不完善有黑屏情况,且无法按照视频帧率进行播放(未对视频进行细颗粒度播放)

jsmpeg 作者手撸了一个 mpeg-ts 的解码器(支持 JavaScript 与 wasm 版本),支持 ts 格式(mpeg1 编码)的视频,支持较为稳定,可支持细颗粒度播放

因为 MP4 格式的视频较为常见,如果能直接播放 MP4 是最理想的,但 WasmVideoPlayerBroadway 的实现都不太理想,一个是移动端性能差,一个解码支持不完善。

上面的工具都未提供 npm 包支持,如果想在生产使用还需要自己做一下二次封装。

最终的选用的是 jsmpeg,jsmpeg 支持 ts 格式的视频有个优势。

ts 是日本高清摄像机拍摄下进行的封装格式,全称为 MPEG2-TS 。ts 即 "Transport Stream" 的缩写。MPEG2-TS 格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。

例如一个背景视频素材 10M 我们不需要将整个视频请求下来再进行播放,可以通过 Content-Range 拆分成多个片段来加速视频载入播放。jsmpeg 也支持视频的分片请求。

jsmpeg 本身模块拆分的比较清楚,所以二次包装成 npm 也比较方便。

silent-film-player 是我对 jsmpeg 做的简单二次封装,考虑是做背景视频播放的,所以去掉音频解码相关的模块,新增了 Web Workers 的支持:

  • 拆分模块支持 npm 包
  • 移除声音模块
  • 抽离核心的解析模块
  • 新增 Web Workers 支持

感兴趣的同学可以试试~

使用:

<template>
    <section>
        <canvas ref="canvas"></canvas>
        <div class="mask">
            遮罩层
        </div>
    </section>
</template>

<script>
import Player from 'silent-film-player';

export default {
    data() {
        return {
            url: 'https://xxx.ts',
        };
    },

    mounted() {
        const {
            url,
            $refs: { canvas },
        } = this;
        window.player = new Player(url, {
            canvas,
            loop: true,
            autoplay: true,
            disableWebAssembly: true,
            // 分片大小
            chunkSize: 1 * 1024 * 1024,
            videoBufferSize: 512 * 1024,
        });
    },
};
</script>

看几个示例:

示例仓库在这:github.com/kinglisky/b…

至此大概是个比较靠谱的背景视频实现方案。

前景视频实现

是不是觉得背景视频就只能放在视觉的底层,先来看个例子:

是不是很神奇,结构大概如下,canvas 覆盖在一个图片元素之上,但视频的内容却透出了底部图片。

<section class="container">
    <img
        class="view-bg"
        src="./demo.jpeg"
    />
    <canvas
        ref="canvas"
        class="view-canvas"
    >
    </canvas>
    <div class="view-mask">MASK</div>
</section>

具体实现为在 canvas 上附加上一个 css 属性

mix-blend-mode: screen;

对的,只需要设置上这个属性,视频中的黑色部分即可透出下层图片。

关于 mix-blend-mode 属性的详解可以参考张鑫旭的 深入理解CSS mix-blend-mode滤色screen混合模式

其他

  • 视频解码比较耗性能,注意控制播放的视频个数,可以对可视区域内视频元素做播放控制,不可见的视频不播放

  • wasm 解码在移动端支持和性能较差(特别是安卓端),不太建议使用

  • 对支持的标准 video 行为的浏览器,如桌面端浏览器可以直接使用 video 实现背景视频

  • iOS 虽然大部分浏览器对 video 元素支持不错,但有些特例如:夸克UC 极速版ALOOK 视频默认是弹层播放的,而且 ALOOK 浏览器的 userAgent 居然是和 Safari 一样的,所以你根本没法区分它俩

  • 微信页面的 WeixinJSBridgeReady 无法触发异步加载的视频

  • MP4 转 TS 会造成视频质量下降,建议提高转码的比率

简单讲讲实现的思路,目前这套方案已经在线上跑了,感兴趣的同学可以看看:

编辑模板:www.gaoding.com/template/18…

成品页面:g.gd-share.cn/p/gpmjb2gs

下期讲讲:阿里云函数计算 + OSS 触发器实现视频文件批量转码,

over~

参考资料