编者按:视频协作平台会涉及网络、编解码等众多技术栈,并且要支持各类终端。其中一个关键能力是实现毫秒级的同步,这对于视频协作平台十分重要。本文来自分秒帧 web多媒体开发工程师耿学岩的投稿,详解了实现毫秒级同步遇到的两个挑战和解决方案。最后,如果你有一定的经验和思考又乐于分享,欢迎通过 editors@livevideostack.com 投稿给我们。
文/耿学岩
背景
分秒帧是一个音视频生产协作平台,其中用户可以通过在视频的某个时间点提出意见或分享来沟通对视频的修改意见。由于客户有时需要对时间精确到帧进行定位,我们需要保证不同转码视频在播放时,时间定位能够精确到毫秒级别。在满足这一要求的同时,我们还必须考虑不同网络条件、不同端和不同使用场景。我们在解决这些问题的过程中发现了一些问题,本文将对这些问题进行讨论。
为什么需要画面帧的准确性?
图:用户发送的批注
图:审阅者看到的
当用户发送批注需要审阅者根据批注意见做出修改时,如果没有画面校准,此时审阅者一脸黑人问号, 哪来的“T” ? 然后再私下沟通吗?信息存在误差, 审阅批注就毫无意义。
问题一:保证浏览器中 Video 标签时间定位在 pause 时的准确性
当用户在播放视频时暂停,并对视频进行批注,然后继续播放时,有时会发现定位回原始批注时间点时画面会有一帧的偏差。这是因为,我们在暂停时记录了视频的当前时间(即 currentTime)并通过 seek() 方法回到该时间点,但是这个方法并不能保证回到的画面完全准确。
现象
暂停批注时 没有矫正currentTime,当批注发送成功后,自动跳回批注点,画面发生了变化,以下是用户所不想看到的画面:
https://mp.weixin.qq.com/s/9N...
JS代码如下:
JavaScript
var videoDom = document.getElementsByTagName('video')[0]
videoDom.pause()
var currenttime = videoDom.currentTime
videoDom.currentTime = currentTime // 此时画面有概率发生改变
问题产生原因
我们在解决这个问题时发现,这个问题是由 JavaScript 执行机制导致的。在浏览器中,JavaScript 是单线程执行的。当我们调用 pause 方法时,实际上是将该操作添加到了事件队列中。当事件轮询到这个暂停操作时,才会真正执行 pause 方法。而在这个过程中,获取 currentTime 的操作已经完成了。这就导致了两个操作之间的时间差。如果这个时间差恰好发生在视频帧切换的时候,就会导致画面偏差一帧。
举个例子,如果一个视频有 25 帧,那么第 0-40ms 是第一帧画面,第 41-80ms是第二帧画面,以此类推。
当用户在播放第一帧画面时按下暂停按钮,我们认为JavaScript 会立即执行逻辑并通知 Video 标签停止播放,但实际上暂停操作会被加入事件队列中等待执行。如果暂停操作前面还有其他事件正在排队,等执行到暂停操作时就会有一定的时间差。如果这个时间差恰好发生在第 41 ms,画面会跳到下一帧画面。但是,我们拿到的currentTime还是第一帧画面的。
解决方案
为了确保在暂停时和查看批注时 currentTime 的一致性,我们在暂停时对 currentTime 进行了矫正。这样,当用户暂停时进行批注,然后再设置 currentTime查看批注时,就不会出现画面偏差问题。通过这种方式,我们就能保证画面在暂停时和查看批注时的准确性。
问题二:HLS流中视频 duration 值变化异常
在我们的应用中,我们需要确保各端的视频总时长和总帧数一致。为了实现这个目的,我们通常会在浏览器 Video 标签的 durationchange 事件触发时获取视频总时长,并通过帧率计算出总帧数。durationchange 事件是当视频总时长发生改变时触发的。当视频加载前,总时长为默认值"NaN",当视频加载完成后,durationchange 事件触发,总时长会变成视频的实际总时长。
在加载和播放视频时,浏览器会用Video标签来追踪视频的状态。共有五个状态,分别是:[1]。
在视频加载和播放过程中,浏览器Video标签的 readyState 会发生变化。在这个过程中,MP4文件和HLS文件的 duration 变更时机是不同的。
MP4
在 MP4 文件的加载过程中,durationchange 事件会在资源开始加载(loadstart)之后,在元数据已加载(loadedmetadata)之前触发。此时,浏览器会解析 MP4 文件中的 moov box,并获取视频时长。因此,在 durationchange 事件触发时,可以获取到较为准确的 duration 。
HLS
我们发现在加载 HLS 流时,浏览器 video 标签的 duration 会发生多次变更。
第一次变更在loadstart之后 loadedmetadata 之前 并且 readyState === 0 时调用,此时已拿到相对准确的 duration,≈ ffmpeg取到的 durantion。
举个例子,ffmpeg截图如下:
第二次变更在loadstart之后 loadedmetadata 之前 并且 readyState === 1 时调用,此时拿到的时长由 m3u8 文件解析得到。
第三次变更在加载到最后一片 ts 时调用。我们发现这三次变更的时长并不一致。因此我们需要在这三次变更中取一个更准确的时长作为视频时长。
举个例子,三次时长比较:
HLS三次取值时长不一致的原因
第一次:在loadstart后loadedmetadata前readyState === 0时调用,视频的实际时长已被解析出来,时机和机制类似于MP4文件(第一次调用时就可以获取到duration的值)。
第二次:在loadstart后loadedmetadata前readyState === 1时调用,hls.js解析完m3u8索引文件并通过#EXTINF计算出视频的实际时长。
举个例子,以下是一个m3u8文件信息:
第三次:当加载完最后一片ts 此时所有音频和视频帧信息已经可以全部拿到。
举个例子,以下是帧信息:
best\_effort\_timestamp_time :媒体流中的一个标识符,用于标识每一帧的时间戳。
pkt\_duration\_time :媒体流中的一个标识符,用于标识每一帧的持续时间。
通常,best\_effort\_timestamp\_time 和 pkt\_duration_time 会用在音视频同步、流量控制、缓存等方面。[2]
尾音频/视频信息中的 best\_effort\_timestamp\_time 和 pkt\_duration\_time 可用来计算音频/视频的结束时长。在这个案例中,音频结束时长由 best\_effort\_timestamp\_time 和 pkt\_duration\_time 相加所得(即 96.230300 + 0.023211 = 96.253511),视频结束时长也是如此(即 96.229778 + 0.016667 = 96.246445)。
我们发现,音频结束时长 - 音频首个best\_effort\_timestamp_time约等于第三次获取的duration。具体来说,音频的结束时间比视频的结束时间长,同时音频的第一个时间戳早于视频的第一个时间戳。为了包含最完整的时间长度,需要将音频和视频时间戳中的最小值和最大值来进行计算。这种情况可能出现在音频和视频的录制或处理过程中,需要进行相应的调整以确保两者之间的同步和一致性。
参考资料
1.https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/readyState