mp4 实现前端动画

1,079 阅读11分钟

一. 背景

[医生端-2024年度执医成就]需要使用复杂的转场动画,单纯使用 css 方式开发成本高。经调研,使用 mp4 作为背景动画可以低成本实现业务需求。 线上地址,大家可以感受一下:muzhi.baidu.com/f2c/twentyF…

但存在问题如下:

  1. 视觉侧交付的视频体积过大,难以保证用户体验
  2. 考虑到网络情况等不可控因素,视频加载 / 播放可能会出现失败

二. 资源体积优化

2.1 名词解释

分辨率:

分辨率是指图像或视频中的像素数量。常见的分辨率有720p(1280×720)、1080p(1920×1080)和4K(3840×2160)等。分辨率越高,图像或视频的细节和清晰度就越高。

码率:

码率是指视频或音频数据传输速率,通常以每秒传输的比特数来表示,单位为kbps(千比特每秒)或Mbps(兆比特每秒)。码率越高,视频或音频的质量就越高,但同时占用的带宽也会更大。

帧率:

帧率是指每秒显示的图像帧数。通常以“帧每秒”来表示。常见的帧率有24、30、60等。帧率越高,视频播放就越流畅,尤其在快速运动的场景中能够更好地展现细节。

编码格式:

编码的目的是压缩数据量,采用编码算法压缩冗余数据。常用的编码格式有如下这两种

视频元数据(Metadata)

技术性元数据 包括:

  • 文件大小
  • 文件格式(如MP4, AVI, MOV等)
  • 编码格式(如H.264, H.265)
  • 创建和修改日期
  • ......

内容相关元数据 包括:

  • 创建日期
  • 内容描述
  • ......

2.2 视频格式处理

2.2.1 编码格式

判断设备是否支持 H265 格式,支持则采用 H265 编码格式视频,否则采用 H264 编码格式

  • H265 视频压缩率高 & 对硬件设备要求较低
  • 内嵌 webveiw H5 页面可能会不兼容 H265 格式,则采用 H264 视频格式

各编码格式具体对比如下:

  • MPEG(MPEG-2、MPEG-4)
    • 压缩效率:较低,主要用于早期数字电视和DVD等应用。
    • 兼容性:历史最久,兼容性好
  • H.26X(H.263、H.264/AVC、H.265/HEVC)
    • H264
      • 压缩效率:显著提升,比MPEG-4高50%以上。适合高清(HD)和全高清(FHD)内容。
      • 兼容性:广泛支持,几乎所有现代设备(手机、PC、电视等)均提供硬件解码。
    • H265
      • 压缩效率:比H.264高约50%,适合4K和HDR视频。
      • 兼容性:逐渐普及,支持的设备较新(2015年后发布的设备多支持)。
  • AV1
    • 压缩效率:比H.265和VP9高约20-30%,目前压缩效率最高的主流标准,适合8K、HDR视频。
    • 兼容性:较新,2020年后发布的高端设备逐渐支持(如新款GPU、旗舰手机)。
  • VP9
    • 压缩效率:与H.265相近,比H.264高约50%。适合4K和高动态范围(HDR)。
    • 兼容性:现代设备(如Android手机、Chrome浏览器支持良好),但普及度略低于H.265。
编码标准压缩效率硬解码兼容性软解码开销硬解码开销
MPEG-2全面
MPEG-4部分支持
H.263较低几乎无较低较低
H.264非常全面中高
H.265更高新设备支持较好较低
VP9类似H.265Android/Chrome友好较低
AV1最高新设备逐渐普及极高较高

2.2.2 原视频格式分析

视觉侧交付的文件中,视频编码格式是 H264,2s 的视频体积为 3.3M,不符合预期

使用 ffmpeg 查看具体视频信息

ffprobe -show_streams 登榜次数-264.mp4

[STREAM]
index=0
codec_name=h264
profile=Main
codec_type=video
width=1242
height=2688
coded_width=1242
coded_height=2688
level=60    !!!
duration=2.000000
TAG:encoder=AVC Coding
......
[/STREAM]

当使用H.264编码时,profilelevel是指视频编码的配置参数,它们决定了视频的质量、兼容性和性能。具体来说,这些参数包括:

Profile(配置文件):

指定了编码器可以使用的特定功能和算法,影响了视频的压缩效率和质量。常见的profile包括Baseline、Main和High。Baseline适用于较低质量的视频,Main适用于一般质量的视频,而High适用于高质量的视频。

Level(级别):

指定了视频的参数,如分辨率、帧率和比特率的限制。不同的level对应不同的视频参数限制,例如Level 3.0适用于标清视频,Level 4.1适用于高清视频,Level 5.1适用于超高清视频。

简单来说:Profile 和 Level 的等级越高,文件压缩得越小,传输越快,但cpu消耗越多。

压缩级别越高不仅在压缩时cpu的消耗越高,视频在播放时也需要消耗更多的cpu进行解压,各类型手机的硬件参数不一样,所以支持的压缩级别也不同。

ios 设备中最高支持的 profile 为 High,Level 为 4.1

在兼顾视频质量 & 视频体积后,选择 Profile 为 Main,Level 为 4.1 等级的视频

2.2.3 视频格式处理

H265的视频体积会比H264 体积更小,但webview 内嵌 h5 页面可能对H265 的兼容性不好

通过运行时判断是否支持H265 格式,采用不同的视频格式

#!/bin/bash

# 指定你的视频文件夹路径
VIDEO_FOLDER="/Users/chenwenliang/Desktop/24"

OUTPUT_DIR="${VIDEO_FOLDER}/h264"

# 进入视频文件夹
cd "${VIDEO_FOLDER}"

# 循环处理文件夹内的每个MP4文件
for input in *.mp4; do
    # 跳过不存在的文件(例如如果没有找到任何匹配的.mp4文件)
    [ -f "$input" ] || continu

    # 使用ffmpeg转换视频到H.264编码
    ffmpeg -i "${input}" -c:v libx264 -profile:v main -level 4.1 "${OUTPUT_DIR}/${input%.mp4}_h264.mp4"
    # 使用ffmpeg转换视频到H.265编码
    ffmpeg -i "${input}" -c:v libx265 -vtag hvc1 "${OUTPUT_DIR}/${input%.mp4}_h265.mp4"
done

echo "转换完成!"

2.3 图片格式处理

  • SVG 是基于XML的矢量图片格式,不失真无限放大。支持动画。
  • JPEG 是有损压缩,不支持透明度或者动画。
  • PNG 是无损压缩,支持透明度。APNG 是 PNG 的扩展,支持动胡奥。
  • WebP 是无损和有损压缩,支持动画或者透明度。
  • GIF 是位图图片格式,支持动画。

结论:

    * 小图标或 logo 可以使用 SVG
    * 背景图片不需要透明度,使用 JPEG 格式
    * 对于超过特定大小的 GIF动图,可使用CSS动效或者视频替代。

2.4 处理后总体积

H264 视频18个 体积12.96 MB

H265 视频18个 体积3.93 MB

图片资源(含 gif) 42个 体积 5.48 MB

音频资源 1 个 体积 938.106KB

共计:23.29MB 最优情况只需加载 H265 视频 + 图片 + 音频 ≈ 11MB

三. 兜底处理

3.1 视频加载失败

**原因:**由于网络原因等不可控因素,视频加载可能会出现失败

**目的:**视频获取失败时能正常展示背景图片,保证用户体验

**核心逻辑:**在每个页面组件外层使用 backgroundImage 作为兜底处理

return pages.map((item, index) => {
    const match = index === currentPage;
    return (
        <div
            key={index}
            style={{
                zIndex,
                pointerEvents: match ? 'auto' : 'none',
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: '100%',
                backgroundSize: 'cover',
                backgroundPosition: 'center center',
                backgroundRepeat: 'no-repeat',
                backgroundImage: match ? `url(${item.bg})` : '',
            }}
        >
            <CSSTransition
                in={match}
                timeout={100}
                classNames={
                    enter: styles.reportEnter,
                    enterDone: styles.reportEnterDone,
                    exit: styles.reportExit,
                    exitDone: styles.reportExitDone
                }
                appear
                unmountOnExit={!!isApp}
            >
                <div
                    style={{
                        width: '100%', height: '100%',
                        visibility: currentPage === index ? 'visible' : 'hidden'
                    }}
                >
                    <item.component
                        order={index}
                        src={videoSrc}
                        bg={bg}
                        videoRef={videoRefArr.current[index]}
                    />
                </div>
            </CSSTransition>
        </div>
    );
});

3.2 播放视频失败

原因:

  1. 视频加载失败导致用户上滑时播放视频失败
  2. 兼容性问题,某些浏览器视频播放时机必须和用户交互强绑定。但通过视频转场时必须监听上一页的视频播放完毕,触发下一页视频的播放,无法和用户操作强绑定。

目的: 播放转场视频失败时也能正常翻页

核心逻辑: 子组件内监听用户手势操作**,并重写 video.play 方法,**播放成功后 / 失败都需翻页

import { useCallback, useEffect } from 'react';
import { ON_PAGE_NEXT } from '@/constants';
import { useTwentyFourContext } from './useTwentyFourContext';

const useHandleVideoEnd = (secondVideoRef, setShowFirstVideo) => {
    const { dcBill: { currentPage }, eventBus, updateDcBillData } = useTwentyFourContext();

    const goNext = useCallback(() => {
        updateDcBillData({
            key: 'currentPage',
            value: currentPage + 1
        });
    }, [currentPage, updateDcBillData]);

    const onNextPage = useCallback(() => {
        setShowFirstVideo(false);
        secondVideoRef.current?.play(goNext);
        secondVideoRef.current?.onVideoEnd(oNext);
    }, [goNext, secondVideoRef, setShowFirstVideo]);

    useEffect(() => {
        eventBus?.on(ON_PAGE_NEXT, onNextPage);

        return () => {
            eventBus?.off(ON_PAGE_NEXT, onNextPage);
        };
    }, [eventBus, onNextPage]);

    return onNextPage;
};

export default useHandleVideoEnd;

四. 兼容性问题处理

4.1 微信导航栏问题

问题: 微信路由跳转后会出现底部前进/后退导航栏,造成空间压缩影响视觉效果,且没有暴露 api 进行屏蔽

解决方案: 改造成单路由页面

4.2 safari 100vh问题

问题: safari 浏览器中 100vh 并不是指的可视区中的高度,会包括下面工具栏

使用 vh 进行布局时,就会可能造成功能列遮挡文字

解决方案: 通过 innerHeight 获取可视区高度,top / bottom 采用百分比布局

const width = window.innerWidth;
const height = window.innerHeight;

 <div
    style={{ maxHeight: height, maxWidth: width }}
    ref={contentRef}
>
    {buildPages}
</div>

浏览器自动播放策略

  1. Chrome 浏览器的自动播放策略

我们将 Chrome 的自动播放策略视为标准策略,与其有出入的我们在文章中会重点提示。

Chrome 66 之后的版本中,只有静音的自动播放是被允许的:

<vide src="xxx" muted autoplay></vide>

此外出现以下几种情况时,有声的自动播放是被允许的:

当用户与页面有交互后,可以调用 video.play() 方法来播放视频。或者在交互函数中创建的视频元素,此时创建的视频允许有声自动播放,比如:

function onButtonClick() {
        const testContainer = document.querySelector("#test-container");
        const video = document.createElement("video");
        video.id = "test-video";
        video.src = "https://b2b-web-vr.cdn.bcebos.com/aichuman/test3/cd0c8a3435c025410a9f0f15b64bce8b.mp4";
        video.autoplay = true;
        video.controls = true;
        testContainer.appendChild(video);
        video.play();
}

当网站的“媒体互动指数”超过了阈值,这通常表示用户之前在该网站上曾播放过有声视频,这时视频的有声自动播放是被允许的。

用户在移动设备上用户将网站添加到主屏幕,或者在桌面上安装了 PWA。

  1. 微信内置浏览器中自动播放

在 IOS 微信的内置浏览器中,视频即使是静音状态也无法自动播放,甚至连视频的 loadedmetadata 事件也不会自动触发(在普通浏览器中视频加载出之后即使没有自动播放,loadedmetadata 事件也应该会触发),这就会导致一些视频库,如 YYEVA 的执行失败。

解决方案:使用 WeixinJSBridgeReady 的回调。

在微信内置浏览器中,WeixinJSBridgeReady 表示微信提供的 JS bridge 加载完成,在其回调中创建的视频是允许自动播放的,或者直接在其回调中调用视频的 play() 方法也是被允许的,同时这时视频即使没有设置静音播放也能成功自动播放

const container = document.querySelector("#test-container");
const video = document.createElement("video");
video.src = "https://b2b-web-vr.cdn.bcebos.com/aichuman/test3/cd0c8a3435c025410a9f0f15b64bce8b.mp4";
video.autoplay = true;
video.controls = true;
video.playsinline = true
// video.muted = true; // 即使视频不设置静音太

container.appendChild(video);

video.addEventListener("loadedmetadata", (event) => {
        console.log(
                "The duration and dimensions of the media and tracks are now known.",
        );
});

function doPlay() {
        WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
                // 必须在 WeixinJSBridge 的回调函数中调用 `video.play()` 才生效,这是微信的限制
                video.play();
        });
}

// 在 WeixinJSBridge 加载成功后直接播放
if (window.WeixinJSBridge) {
        doPlay();
}
// 否则,等 WeixinJSBridge 加载成功
else {
        document.addEventListener('WeixinJSBridgeReady', ()=>{
                console.log("on WeixinJSBridgeReady")
                doPlay()
                // 你也可以直接在这里调用 video.play()
        }, false);
}

或者控制页面中已有的 video 元素:

const video = docuemnt.querySelector("#video");

function doPlay() {
        WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
                video.play();
        });
}

if (window.WeixinJSBridge) {
        doPlay();
}
else {
        document.addEventListener('WeixinJSBridgeReady', ()=>{
                console.log("on WeixinJSBridgeReady")
                doPlay()
        }, false);
}

在 Android 的微信浏览器中同样无法自动播放,并且 WeixinJSBridgeReady 并不会被触发,因此我们没办法进行自动播放(参考了多个网站,都没有在微信做自动播放),只能引导用户手动播放。

  1. 夸克的自动播放策略

在 IOS 下:

  • 视频播放器会被拦截

  • 静音和非静音视频均不允许自动播放;

  • muted 的设置是无效的,即使后续用户播放了视频,也是带有声音的;

  • JS 可以直接调用 video.play() 来播放视频,无需用户交互;

  • loop 无效;

  • 多音频和视频播放:

    • 有声视频和音频之间会互相暂停;
    • 被迫暂停后不会触发 pause 事件;

在 Android 下:

  • 视频播放器会被拦截,yyeva 失效;

  • 符合标准策略;

  • 多音频和视频播放:

    • 有声视频和音频之间会互相暂停;
    • 被迫暂停后会触发 pause 事件;
  1. UC

在 iOS 下:

  • 视频播放器会被拦截;

  • 静音和非静音视频均不允许自动播放;

  • muted 的设置是无效的,即使后续用户播放了视频,也是带有声音的;

  • JS 直接调用 video.play() 没有触发报错;

  • 多音频和视频播放:

    • 当一个视频在播放中时,播放另一个视频时会触发 JS 报错;
    • 当视频在播放中时播放音频,视频会被暂停,反之亦然;
    • 视频被迫暂停后会触发 pause 事件;

在 Android 下:

  • 可以使用 renderer="standard" 防止播放器被浏览器拦截;

  • 符合标准策略;

  • 多音频和视频播放:

    • 同一个页面如果存在多个音频或视频,其中一个播放后,其他的视频会被暂停,并且不会触发 pause 事件,无论是否有声(yyeva 在播放中会出现突然暂停但是语音仍播放的情况);
  1. 百度 APP

在 iOS 下:

  • 即使视频不静音也能自动播放;

  • 可以直接在 JS 中调用 video.play()

  • 多音频和视频播放:

    • 有声视频点击播放,或者出发循环播放时,会暂停所有音频的播放;
    • 音频的播放不会打断有声视频的播放;
    • 无声视频和音频之间不会互相干扰播放状态;
    • 多个有声视频同时播放时,当前播放的有声视频会暂停其他视频的播放;
    • 视频被迫暂停时会触发 pause 事件;

在 Android 下:

  • 即使视频不静音也能自动播放;

  • 可以直接在 JS 中调用 video.play()

  • 多音频和视频播放:

    • 当多个有声视频同时播放时,最新播放的有声视频会暂停上一个有声视频;
    • 当视频为 muted 时,视频之间的播放不受影响,即使用户手动将 control 上的静音按钮取消,也仍然能同时播放多个有声视频;
    • 音频会暂停有声视频的播放,无声视频不受影响;

yyeva 卡住的根因:多个视频同时加载