慢慢改,你就知道怎么改了。

尽量用几句话说完版本

  • 其实flv.js做的事情仅仅只是将flv的视频流格式转换成html5video标签所能识别的数据源,更多的还是依赖video标签原生的API来进行优化;

  • videoElement.buffered.end(0) 获取到video标签的缓存长度, videElement.currentTime 获取当前播放的相对时间,两者的差就是缓存与播放点的延迟;

  • 如果上边两者的差大于某数值,就进行直接跳转到快要接近 videoElement.buffered.end(0)

  • 视频在追帧的整个过程中,存在着一个动态的播放速度,通过 videoElement.playbackRate 来进行修改;

  • 播放速度的调节是为了尽量让 videoElement.buffered.end(0) - videoElement.currentTime 稳定在某一个范围内;

flv.js做了什么

我在优化这个直播流的过程中,最开始犯的错误就是把 flv.js 看成是一个播放器了,然后在 flv.js 身上寻找跳帧,加/减速播放等等接口,但其实 flv.js 做的事情特别的专一,就是把 flv 视频流转换成 video 标签所能识别的数据源进行播放。

明白了这一点,接上来的事情就好办了许多,既然 video 标签才是我们操作的目标,那直接去查 videoAPI,看看我们都能做些什么就好了。这里说一下,MDN的文档在video的接口上似乎不是很全,有些属性跟接口并没有统一放置到video标签的说明页面,所以需要使用搜索引擎来找比较好。

跳帧

既然是直播,实时性应该都是我们的第一追求?但是如果服务器端给到的数据缓存比较大(实时性比较落后),那么缓存下来的第一帧往往都是比较滞后,所以这个时候我选择了跳帧,直接跳到一个接近缓存尾部的时间进行播放。这里不能直接跳到最后一帧,理由很明显,会出现卡顿。当然,也可以修改视频的播放速度进行追赶,但是会有些鬼畜的感觉。

1
2
3
4
5
6
7
const end = videoElement.buffered.end(0)
const current = videoElement.currentTime
const diff = end - current
if (diff > 4) { // 这里设定了超过4秒以上就进行跳帧
videoElement.currentTime = end - 0.45 // 这里不跳到end,是因为如果buffer的增长速度相对较快的时候,会出现频繁追帧而导致明显的卡顿
if (videoElement.paused) flvPlayer.play()
}

但并不是跳了帧时候就完事了,因为在页面刷新后或者初始化后,video标签的缓存增长速度是会比平时要快的,如果你上边设置跳帧的时间间隔比较短,而buffer的增长速度又相对较快,这个时候就会出现明显的卡顿,因为会陷入到一个持续跳帧的状态,直到缓存的增长速度降了下来。

为了解决这个问题,我采取的策略是修改播放速度进行追帧。

追帧

修改播放速度进行追帧的明显的好处就是可以避免频繁跳帧所带来的频繁卡顿。而播放速度的修改最好也是一个动态的值,需要依据video标签当前buffer的大小,currentTime,以及buffer的增长速度(如果能实时知道的话)来动态地修改这个播放速度的值是比较理想的。

这里追帧的宗旨是为了让 videoElement.buffered.end(0) - videoElement.currentTime 维持在一个比较稳定的范围内,而这个范围,就是除去网络延迟后,缓存跟当前播放内容之间的延迟了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setInterval(() => { // 定时追帧
const end = videoElement.buffered.end(0)
const current = videoElement.currentTime
const diff = end - current
const diffCritical = 4 // 这里设定了超过4秒以上就进行跳转
const diffSpeedUp = 1 // 这里设置了超过1秒以上则进行视频加速播放
const maxPlaybackRate = 4 // 自定义设置允许的最大播放速度
let playbackRate = 1.0 // 播放速度
if (diff > diffCritical) {
videoElement.currentTime = end - 0.45
playbackRate = Math.max(1, Math.min(diffCritical, 16))
} else if (diff > diffSpeedUp) {
playbackRate = Math.max(1, Math.min(diff, maxPlaybackRate, 16))
}
videoElement.playbackRate = playbackRate
if (videoElement.paused) flvPlayer.play()
}, 1000)

总结

上边所举的接口跟例子应该还可以继续优化,比如声音的加入可能会让事情变得更复杂,跳帧跟追帧的标准可以再动态化一点,这应该就意味着需要更多的参数单元加入到这场直播盛宴当中来。

最后,希望你也能享受到慢慢地让一件事情变得更好所带来的乐趣。