Video 视频播放
HTML5支持video和audio之前, 网页播放流媒体文件, 都是通过其它方法, 例如 activeX 插件 或者 flash 支持后,页面可以在web服务器上放置视频文件, 浏览器根据支持的视频格式请求相应的视频文件
标签规范
video 标签 :https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/video
HTMLMediaElement :https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement
常用 option
- src 视频地址
- poster 视频封面,没有播放时显示的图片
- preload 预加载
- autoplay 自动播放
- loop 循环播放
- controls 浏览器自带的控制条
- muted 静音播放
常用 属性 (HTMLMediaElement)
- buffered 返回哪部分已被下载,会返回一个 TimeRanges 对象,(只读)
- currentTime 当前播放时间,(只读)
- duration 视频总长,(只读)
- paused 是否处于暂停状态,(只读)
- playbackRate 播放速度
- volume 音量
常用 方法 (HTMLMediaElement)
- pause() 暂停播放。
- play() 开始播放。
常用 事件 (HTMLMediaElement)
- canplay 有足够的可用数据可以播放媒体时触发
- pause 当播放状态更改为暂停时发送
- play 当播放状态更改为播放时发送
- ratechange 播放速度变化时触发
- timeupdate 当前播放事件变化时触发
- volumechange 音量大小变化时触发
Tips
1. autoplay 属性限制
autoplay 属性在多种浏览器上有阻止策略,需要用户手动调整浏览器播放策略才能够实现自动播放。
解决办法:将视频设置为静音播放,并给予用户提示,参考刷新 bilibili 直播页面后的交互。
HTMLMediaElement.play().catch(async (err) => {
if (err.name === 'NotAllowedError') {
this.$refs.video.muted = true
await this.$refs.video.play()
this.$utils.message({ message: '您正在使用静音播放' })
}
})
2. 视频下载原理
一个非常大的视频,video 标签是如何实现快速开始播放的?又是如何实现播放进度控制的?
通过 请求头中的 Range 字段控制数据加载范围
首次加载 video 后会发送 一个 Range: bytes=0-
的请求,暂停时,在加载够一定数据量后停止数据加载。继续播放后当数据量不足时继续下载
当用户手动 seek 到一个未 buffer 的位置时,会发送一个 Range: bytes=seek位置-
的请求
具体参考 chrome 内核 BufferReader 源码
首先获取视频码率 -> 根据播放速度判断10s视频大小 -> 预加载 10s 的播放长度,并且最大不超过50MB
3. 自定义进度条时遇到的死循环问题
video 的 timeupdate 事件会在 视频播放进度发生改变时实时触发,在这个事件里 实时修改 data 里的 currentTime 和 timeProcess,进度条组件中 timeProgress 和 进度条位置是 双向绑定的,外部会 watch timeProgress 属性,根据 timeProgress 属性修改 video 播放进度,导致循环。
解决方式
- 不使用 watch 方式,单独监听 进度条的点击和拖动事件修改视频进度
- watch 时判断 element 组件的 dragging 属性,只有当进度条在被拖动时才去修改视频进度,之后再添加点击事件即可
4. 进度条预览、外部预览功能实现
进度条预览和 外部预览的原理是 后台会根据视频返回给前端一个预览雪碧图,前端根据进度百分比使用 background-position
属性调整显示的 图片样式。
B 站前端实现方式
对应的雪碧图
雪碧图由后台生成,生成方式可通过 javac 和 ffmpeg 相关 jar 包实现
5. “高能进度条”实现
什么是“高能进度条”?
实现方式可以理解为 后台返回一个 曲线数组,前端通过绘制 SVG 等方式实现
B 站前端实现方式
最终生成的 svg 如下
<path d="
M 0 100
L 0 80
C 5.0 80.0, 5.0 75.5, 10.0 75.5
C 15.0 75.5, 15.0 73.0, 20.0 73.0
C 25.0 73.0, 25.0 70.9, 30.0 70.9
C 35.0 70.9, 35.0 69.3, 40.0 69.3
C 45.0 69.3, 45.0 66.2, 50.0 66.2
C 55.0 66.2, 55.0 57.3, 60.0 57.3
C 65.0 57.3, 65.0 15.8, 70.0 15.8
C 75.0 15.8, 75.0 67.6, 80.0 67.6
C 85.0 67.6, 85.0 64.0, 90.0 64.0
C 95.0 64.0, 95.0 67.0, 100.0 67.0
L 1000 100
Z">
</path>
SVG 左上角为原点 x=0,y=0 B 站使用的是 3次贝塞尔曲线的方式实现的绘制, 其中 两个 clipPath 分别为曲线剪切和 已播放区域剪切,配合进度条可实现 已播放部分背景蓝色的需求。
https://cubic-bezier.com/#0,0,1,1
对应点: 80 75.5 73 70.9 69.3 66.2 57.3 15.8 67.6 64 67
分别用 100 减后,对应图中后台返回的数据 刚好符合对应规则:y = 0.00272727x + 20
因此我们可以非常容易地仿照 B 站规则生成 SVG,即取后台返回数组中最小的数字作为 80 最大的数字作为 0 来绘制 3次贝塞尔曲线即可
已播放部分变蓝这里涉及到 区间合并算法 问题,可简化为 给定[[1,5],[7,11],[15,16]] , 输入参数 [7,19] 获得合并后的结果 [[1,5],[7,19]]
类似于 leetCode 上的 有效括号问题, 首先合并数组,对每一项标记为开始或结尾,从大到小排序后,每遇到一个开始标记count++,每遇到一个结束标记 count– 当count为0的时候则生成一个区间
6. “弹幕功能”实现
使用 css animate 实现,包含 animation-play-state
遇到的问题有 Vue key 更新策略的问题,现象是 对数组同时进行 push 和 shift 操作后,尽管 v-for 设置了 key 属性,但是其动画仍被重置。
原因是 vue 是异步渲染的,先将数组push后实际并没有渲染,而是等 shift 也完成后一同进行渲染,导致 vue 实际上是使用 insertBefore 进行节点的移动,而不是先删除再添加的方式,而节点的移动会导致 动画重置。
解决方式是,在 push 之后,使用 $nextTick 包裹 shift 方法。
7. “防档弹幕”实现
css mask 背景遮罩
8. 使用 flv.js 中遇到的问题
https://developer.mozilla.org/zh-CN/docs/Web/API/Media_Source_Extensions_API
-
页面离开后,尽管调用了 .destroy() flv 文件仍在加载: fork 后添加 AbortController 控制
-
直播在暂停一段时间后 视频直播被断开:MSE 中 各个浏览器对 SourceBuffer 有大小限制,解决办法是新建一个自己的数组,用于暂存数据,判断只有当用户正在播放的时候 再将 数据输送到 SourceBuffer 中。/src/core/mse-controller.js :445