前言
最近在项目中遇到短视频播放的需求,在实现过程中碰到很多坑,特记录一下,便于下次遇到同类型需求能更快地选择合适的技术方案同时减少采坑的次数。
背景
截止至2020年7月16日,video 标签的兼容性。
目前从这个数据看,video 标签的兼容性不错,但实际上不同的浏览器在同一个属性,表现可能不一致。同时摒弃自动播放功能,需要由用户与界面进行一次交互才能播放,其本质就不再是自动播放了(即一进入页面,视频自动开始播放)。
Controls 属性在不同浏览器中的表现均不一致。以下测试样式仅在 MAC 系统下,如果是 windows 系统和移动端,不一致的样式更多。
场景一、兼容格式
video 元素支持三种视频格式:MP4、WebM、Ogg。需求:PC 和 移动端需要优先支持播放 hls 格式,如果不支持hls,则采用 mp4。
M3u8 格式兼容性
HLS(Http Live Streaming) 是由苹果公司提出的基于 HTTP 的流媒体网络传输协议,直接把流媒体切片成一段段,信息保存到 m3u列表文件中, 可以将不同速率的版本切成相应的片;播放器可以直接使用 http 协议请求流数据。
Mp4 格式兼容性
经查,移动端大部分都支持 hls 格式的视频文件,所以htm结构编写如下:
<video>
<source src={url.m3u8}></source>
<source src={url.mp4}></source>
您的浏览器不支持 video 标签,请升级您的浏览器版本。
</video>
在实际应用场景下,发现移动端兼容 hls 并不好
- 安卓微信浏览器并不支持直接播放 hls,或者说是在 hls 格式的 url 后面加上参数就无法识别,会报错误提示:
- 1、网络连接异常,请稍后重试;
- 2、视频播放地址已失效,请刷新页面后再试!重试。
IOS 11以下系统会出现:初始化加载视频时间超过20秒以上;视频循环播放三次以上之后,就会没有声音。
pc 端并不支持 hls 格式,为了实现 pc 端兼容 hls 格式,引入了第三方常用插件 -- video.js.
- 引入第三插件后虽然能解决pc端播放 hls 格式的问题,但因 video.js 功能非常完善,封装性强可自定义性差,进度原因无法花费太多时间去研究 API 的实现原理,导致出现很多不必要的功能性提示,而又无法直接去除。比如 video.js 在检测 hls 格式资源时,如果资源无效会直接报错误提示,并不会自动切换视频资源。此时需要监听 error 事件,然后调用 src 方法重新设置为 mp4 格式,也就是说无法实现无缝隙切换视频资源,使用户在无感知的情况下切换视频资源。
- 而且 video.js 资源有11M左右大小,资源相对较大,而且此次业务场景大部分是移动场景,所以pc端只能直接将资源格式设置为 mp4。
场景二、样式处理
- 需求:宽高需要区分移动端和pc端两种,同时需要区分全屏和非全屏。非全屏交互逻辑点击切换播放/暂停;全屏交互逻辑点击显示/隐藏进度条,隐藏时仅显示mini进度条。
- 全屏实现方式:全屏实现逻辑采用 video 标签父级元素宽高撑满屏幕,video 的宽高则设置为100%。
- 细节处理:iphone 及 iphoneX 系列在全屏时候需要区分是否是 safari 以便处理底部安全距离。
特殊属性:
x5-video-player-type、x5-playsinline、webkit-playinline/playinline
- 同层页面内播放: 同层页面内播放是标准的视频播放形态,在 video 标签中添加
x5-video-player-type:h5-page属性来控制网页内部同层播放,可以在视频上方显示html元素 - 页面内播放:X5内核视频在用户点击后默认会进入全屏播放,前端可以设置 video 的
x5-playsinline属性来将视频限定于网页内部播放 webkit-playinline和playinline:视频播放时局部播放,不脱离文档流。
出现问题汇总
1、在大部分环境下,视频可实现在页面内播放(父级元素限定宽高内),但还是会出现video撑长的情况。
- 示例:pm.yeshj.com/secure/atta…
- 解决方式:非ios环境下在父级元素上添加
overflow: hidden。
2、QQ浏览器内打开视频频道页点击视频会查看图片。
- 示例:pm.yeshj.com/secure/atta…
- 解决方式:点击事件方式添加
e.preventDefault()
3、QQ浏览器内打开视频播放页未登录的情况下点击关注,登录窗口会被视频窗口盖住,无法登录。APP内嵌页视频播放按钮显示非期望样式。
解决方式:在安卓的微信和QQ浏览器环境下,添加
'x5-video-player-type' = 'h5-page'属性, 在其他环境下添加playsinline、webkit-playsinline、x5-playsinline、webkit-inline。
4、UC浏览器内video被强制劫持。
- 示例:pm.yeshj.com/secure/atta…
- 暂无法解决
5、旧版本微信内播放视频会自带全屏功能。
- 示例:pm.yeshj.com/secure/atta…
- 暂无法解决
场景三、播放控制
产品需求
- 自动播放:需要在 app 和 PCclient 内自动播放。
- 循环播放:播放结束上报播放时长,并从头开始继续播放。
- 交互逻辑:点赞、分享、全屏、播放/暂停、拖动进度。
- 数据统计:播放期间跳转其他页面/刷新/回退,即在组件卸载期间上报当前播放时长。
自动播放
autoPlay: boolean属性实现自动播放。指定后,视频会马上自动开始播放,不会停下来等着数据载入结束。
视频自动播放可以在页面打开且资源加载足够的情况下让视频自动播放,减少一次用户点击的交互,同时可以应用在动效背景、H5仿视频通话的功能。虽然 video 标签提供 autoPlay 属性,不过由于各种原因,自动播放无论在PC端还是移动端都有不同程度的限制。详见视频播放--踩坑小计。
循环播放
loop: boolean属性实现自动播放;指定后,会在视频结尾的地方,自动返回视频开始的地方。
使用 loop 开启自动循环,onended 事件将失效。如果产品需求要在 ended 事件执行一些逻辑,将不会生效。所以不可直接使用 loop 属性,需要在 ended 事件执行方法中添加播放逻辑,实现循环播放,同时执行一些其他业务逻辑。
交互逻辑
HTML
<div onClick={handleClickEvent}>
<video />
... other components ...
</div>
JS
const handleClickEvent = () => {
if (playing) {
videoElement.pause()
} else {
videoElement.play()
}
... other logic ...
}
一般情况下,html 结构如上进行设计。但是 video 标签在某些环境下(部分IOS系统)层级会比父级还要高,所以会导致添加在父级标签上的事件失效。如果产品要在video标签上面增加额外的交互功能(如点赞,分享等),一定要注意层级问题。
播放数据统计
一般正常思路,业务需求要在组件卸载期间执行某些逻辑,是在 beforeunload / unload 事件中执行。但是如果在 beforeunload / unload 中请求后端接口,则有概率会被浏览器 cancel,或一直处于 pending中,请求成功的概率会降低。出现这种现象的原因是:http 是建立在 tcp 基础上的,tcp 保持 c/s 通信,如果浏览器在发送 ajax 请求后,服务端响应请求后回传数据给到浏览器,而此时浏览器当前请求页面已不存在,即当前 tcp 通信中断,浏览器无法接受服务器返回的响应信息。
解决思路:
- 采用 window.name 处理刷新当前页面发送的请求;当前页面跳转逻辑,使用发送请求后再异步进行跳转。
useEffect(() => {
window.addEventListener('beforeunload', handlePostAddPlayTime, false)
return () => {
window.removeEventListener('beforeunload', handlePostAddPlayTime)
}
}, [])
// 处理上报失败问题: window.name 方案(只能用于刷新和回退到推荐页)
const handlePostAddPlayTime = () => {
let videoElement = videoEl.current
let currentPlayTime = videoElement.currentTime
let durationTime = videoElement.duration
if (currentPlayTime > 0 && currentPlayTime !== durationTime) {
if (isFirstAdd) {
let playTime = Math.floor(currentPlayTime * 1000)
let batchId = new Date().getTime()
let data = { videoId, playTime, batchId }
window.name = window.name + '|%' + JSON.stringify(data)
}
}
}
- 使用 Beacon API。Beacon API 是一个基于 JavaScript 的 Web API,用于将少量数据从浏览器发送到 Web 服务器,而无需等待响应。
// Blob
function handleAddPlayTime(url, data) {
const blob = new Blob([JSON.stringify(data), {
type: 'application/x-www-form-urlencoded'
}])
let b = window.navigator.sendBeacon(url, blob)
console.log('isAddPlayTime:' + b)
}
// formData
function handleAddPlayTime(url, data) {
const formData = new FormData()
Object.keys(data).forEach((key) => {
let value = data[key]
if (typeof value !== 'string') {
value = JSON.stringify(value)
}
formData.append(key, value)
})
let b = window.navigator.sendBeacon(url, data)
console.log('isAddPlayTime:' + b)
}
总结
通过参与本次项目,从兼容性、样式、业务逻辑三方面对 video 标签有了基本的理解,同时对于后续优化方向也有一定的想法。
- 兼容性:目前各个环境的最新版本基本上都支持,但是在某个属性方面表现可能不一致;
- 样式:想实现在所有环境表示一致,可能需要对特定环境做对应的处理。
- 业务逻辑:video 提供丰富的API和事件监听方法,可实现相对复杂的业务场景。
暂且记录至此,后续如果继续优化项目并遇到其他问题再继续梳理本文,如有错误之处,望指正,谢谢!