一. 背景
[医生端-2024年度执医成就]需要使用复杂的转场动画,单纯使用 css 方式开发成本高。经调研,使用 mp4 作为背景动画可以低成本实现业务需求。 线上地址,大家可以感受一下:muzhi.baidu.com/f2c/twentyF…
但存在问题如下:
- 视觉侧交付的视频体积过大,难以保证用户体验
- 考虑到网络情况等不可控因素,视频加载 / 播放可能会出现失败
二. 资源体积优化
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年后发布的设备多支持)。
- H264
- 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.265 | Android/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编码时,profile和level是指视频编码的配置参数,它们决定了视频的质量、兼容性和性能。具体来说,这些参数包括:
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 播放视频失败
原因:
- 视频加载失败导致用户上滑时播放视频失败
- 兼容性问题,某些浏览器视频播放时机必须和用户交互强绑定。但通过视频转场时必须监听上一页的视频播放完毕,触发下一页视频的播放,无法和用户操作强绑定。
目的: 播放转场视频失败时也能正常翻页
核心逻辑: 子组件内监听用户手势操作**,并重写 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>
浏览器自动播放策略
- 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。
- 微信内置浏览器中自动播放
在 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 并不会被触发,因此我们没办法进行自动播放(参考了多个网站,都没有在微信做自动播放),只能引导用户手动播放。
- 夸克的自动播放策略
在 IOS 下:
-
视频播放器会被拦截
-
静音和非静音视频均不允许自动播放;
-
muted 的设置是无效的,即使后续用户播放了视频,也是带有声音的;
-
JS 可以直接调用
video.play()来播放视频,无需用户交互; -
loop 无效;
-
多音频和视频播放:
- 有声视频和音频之间会互相暂停;
- 被迫暂停后不会触发 pause 事件;
在 Android 下:
-
视频播放器会被拦截,yyeva 失效;
-
符合标准策略;
-
多音频和视频播放:
- 有声视频和音频之间会互相暂停;
- 被迫暂停后会触发 pause 事件;
- UC
在 iOS 下:
-
视频播放器会被拦截;
-
静音和非静音视频均不允许自动播放;
-
muted 的设置是无效的,即使后续用户播放了视频,也是带有声音的;
-
JS 直接调用
video.play()没有触发报错; -
多音频和视频播放:
- 当一个视频在播放中时,播放另一个视频时会触发 JS 报错;
- 当视频在播放中时播放音频,视频会被暂停,反之亦然;
- 视频被迫暂停后会触发 pause 事件;
在 Android 下:
-
可以使用
renderer="standard"防止播放器被浏览器拦截; -
符合标准策略;
-
多音频和视频播放:
- 同一个页面如果存在多个音频或视频,其中一个播放后,其他的视频会被暂停,并且不会触发 pause 事件,无论是否有声(yyeva 在播放中会出现突然暂停但是语音仍播放的情况);
- 百度 APP
在 iOS 下:
-
即使视频不静音也能自动播放;
-
可以直接在 JS 中调用
video.play(); -
多音频和视频播放:
- 有声视频点击播放,或者出发循环播放时,会暂停所有音频的播放;
- 音频的播放不会打断有声视频的播放;
- 无声视频和音频之间不会互相干扰播放状态;
- 多个有声视频同时播放时,当前播放的有声视频会暂停其他视频的播放;
- 视频被迫暂停时会触发 pause 事件;
在 Android 下:
-
即使视频不静音也能自动播放;
-
可以直接在 JS 中调用
video.play(); -
多音频和视频播放:
- 当多个有声视频同时播放时,最新播放的有声视频会暂停上一个有声视频;
- 当视频为 muted 时,视频之间的播放不受影响,即使用户手动将 control 上的静音按钮取消,也仍然能同时播放多个有声视频;
- 音频会暂停有声视频的播放,无声视频不受影响;
yyeva 卡住的根因:多个视频同时加载