跳转与进度对齐
歌词组件会把你通过 setCurrentTime 推送的每一次进度分成两类:正常播放推进 与 跳转。本文介绍两者的区别、组件如何自动识别跳转,以及你可能在哪些场合需要显式告知。
阅读本文前,建议先了解 时序与生命周期 中关于逐帧推送播放进度的部分,本文假设你已经按那里所述持续调用 setCurrentTime。
为什么要区分跳转
Section titled “为什么要区分跳转”播放进度除了随时间自然前进,还可能产生跳变,常见于:
- 拖动进度条
- 快进快退
- 点击某一歌词行跳转
- 循环播放时,进度从结尾跳至开头
正常播放时,相邻两行歌词之间的位移很小,组件可以放心用细腻的动画表现它们:
- 歌词行按索引顺序错峰进行动画,形成阶梯式的位移
- 纵向弹簧的参数按相邻两行的时间间隔动态调整,间隔越短越迅捷
- 逐字遮罩由动画自行推进
跳转的位移则可能横跨整首歌。沿用上面那套行为,每一行歌词都会带着各自的递增延迟进行位移,会导致长距离跳转时,看到之前具有较大延迟的歌词行停留在原地;已经高亮的行的逐字遮罩也会停在跳转前的位置,与新的进度脱节。
因此,组件在识别到跳转的那一帧会切换成另一套行为:
| 行为 | 正常播放 | 跳转 |
|---|---|---|
| 逐字遮罩 | 只为刚进入播放的行对齐一次 | 把当前所有高亮行的遮罩对齐到目标时间 |
| 动画延迟 | 逐行递增,阶梯式位移 | 所有行同时位移 |
| 纵向弹簧 | 按相邻歌词的时间间隔动态调整 | 固定使用较缓慢的参数 |
| 间奏点动画 | 继续播放 | 从目标时间重新开始 |
| 用户滚动位置 | 保留 | 清除并恢复自动对齐 |
其中,如果用户滚动位置是触摸触发的,该位置会被保留。
跳转是逐帧推导的瞬时状态,只在发生跳转的那一帧成立,下一帧即回到正常播放,因此不需要在宿主侧维护一个持续的跳转状态,也不应该长时间保持跳转状态。
组件默认会自动识别跳转。在逐帧推送高精度进度这个常见前提下,推导足以很好地覆盖常见情况,你不需要为跳转做任何额外的事,只要在播放期间持续用 setCurrentTime 推送进度即可。显式传入 seek 标志是一个锦上添花的可选功能,并不是正常使用歌词组件的必要步骤。
不过,自动推导只能根据组件实际收到的进度变化进行判断,某些边界情况可能漏掉跳转或留下轻微的视觉瑕疵,详见自动推导的局限。在宿主明确知道跳转已经发生时,显式传入 seek 标志可以覆盖这些边界情况。
无论是否启用自动推导,进度倒退与停滞都会被视为跳转。
会被判为跳转的进度变化有三种,前两种无条件成立,只有第三种由自动推导负责:
- 进度倒退,无论幅度多小
- 进度不再前进,即重复推送同一个时间
- 进度前进的幅度明显超过应有的推进量
推送需要连续且足够密集
Section titled “推送需要连续且足够密集”推导靠前后两次推送的关系得出结论,因此请保持逐帧同步,不要只在发生跳转时才调用 setCurrentTime。
- 推送间隔超过约 1.2 秒时,每一次推送都会被判为跳转。
- 倍速播放对推送密度更敏感。60 fps 逐帧推送时约 10 倍速以内不会误判,30 fps 时降到约 5 倍速,推送间隔放宽到 250 毫秒时只剩约 1.6 倍速。使用高倍速播放时,请保持逐帧或更高频率推送。
进度来源的粒度不能粗于推送间隔
Section titled “进度来源的粒度不能粗于推送间隔”重复推送同一个时间总会被视为跳转,与自动推导的开关无关。这样用户反复点击进度条上的同一位置也能被识别,并拉回动画中的视觉效果,例如逐字遮罩效果。
代价是,如果你的进度来源粒度粗于推送间隔,绝大多数推送都会被判成跳转。例如逐帧推送一个 250 毫秒粒度的进度源时,约 的推送进度未变,于是几乎每一帧都按跳转处理。遇到这种情况,应当改善进度来源的精度,或在进度未发生变化时跳过推送。关闭自动推导不能规避这一点。
暂停期间不受影响,时间未发生变化的推送会被直接忽略,既不会被判成跳转,也不会触发任何重排。这依赖组件知道当前处于暂停状态,因此请按 时序与生命周期 所述正确调用 pause() 与 resume()。
自动推导的局限
Section titled “自动推导的局限”唯一的视觉瑕疵
Section titled “唯一的视觉瑕疵”满足上面的前提时,只有一种情况会留下可见的瑕疵:播放中用户把进度条向前拖动了 150 毫秒以内。这样的位移识别不出来,逐字遮罩会因此比实际进度慢至多 150 毫秒。
这个瑕疵通常可以忽略。150 毫秒的滞后在视觉上几乎看不出来,也不会累积,下一行开始播放时遮罩就会重新对齐。而且在进度条上精确拖出 150 毫秒以内的位移极难操作,用户一般不会跳转这么短的距离。
暂停时同理,150 毫秒以内的进度变化不会被识别,遮罩会停留在原地。同样地,用户几乎不可能只拖动这么点距离。
前提不成立时漏掉的跳转
Section titled “前提不成立时漏掉的跳转”下面两种情况也会漏掉跳转,但它们都源于前提没有成立,而不是推导本身的局限:
- 推送在中断期间停止:切到后台等情况下推送会被节流或停止,其间发生的跳转可能识别不出来。不过恢复播放的那一帧本来就会重新对齐,所以通常看不出差异。只有中断约 1 秒时才可能导致至多 400 毫秒的遮罩滞后,且同样会在下一行开始时消失。
- 播放状态未同步:若音频已暂停而组件仍以为在播放,即没有调用
pause(),则暂停期间约 1.2 秒以内的跳转都会被漏掉,遮罩停留在原地。按 时序与生命周期 所述同步播放状态,这个范围就会收窄回上面那个可忽略的 150 毫秒。
显式标记跳转
Section titled “显式标记跳转”setCurrentTime 的第二个参数的含义是强制按跳转处理:
function onSeeked() { player.setCurrentTime(Math.round(audio.currentTime * 1000), true);}audio.addEventListener("seeked", onSeeked);显式传入 seek 标志是锦上添花的可选功能。自动推导默认启用,在持续推送高精度进度时通常已经可以很好地工作。
由于跳转状态会消耗较多资源,且会打断手势交互(触摸除外),不建议长时间保持跳转状态。
如果你明确知道发生了跳转,也可以在对应的 setCurrentTime 调用中显式标记。这样判定不受推送节奏影响,可以避免上面提到的短距离跳转瑕疵和两种漏判。
显式传入跳转标志不会否决自动推导的结果,所以两者可以放心一起使用。
组件自行对齐的时机
Section titled “组件自行对齐的时机”下面这些场合由组件自己按跳转处理,你无需干预:
- 重建歌词视图时(如
setLyricLines、setOptimizeOptions、updateLyricProcessConfig),用重建时给定的初始时间对齐 - 页面显示(
pageshow)时,用当前进度重新对齐
关闭自动推导
Section titled “关闭自动推导”如果宿主的进度来源精度太差,以致进度前进的幅度频繁被误判,可以使用 setEnableAutoSeekDetection 关闭自动推导。
关闭后只有上面第三条规则失效,即不再比较媒体时钟与物理时钟;进度倒退与停滞仍然会被视为跳转。当前状态可以用 getEnableAutoSeekDetection 读取。
歌词行点击事件
Section titled “歌词行点击事件”组件提供了 line-click 事件,在某一歌词行被鼠标左键点击时触发,其事件类型为 LyricLineMouseEvent。
组件本身不会响应歌词行的点击操作。你需要监听该事件,并作出音频进度跳转等操作。例如:
import type { LyricLineMouseEvent } from "@applemusic-like-lyrics/core";
player.addEventListener("line-click", (event) => { const lineEvent = event as LyricLineMouseEvent; audio.currentTime = lineEvent.line.getLine().startTime / 1000; player.setCurrentTime(lineEvent.line.getLine().startTime, true);});点击歌词行跳转时也属于跳转,上面显式传入的 true 与自动推导的结果作用相同,因此即使省略它一般也能正确识别。
不再需要歌词组件时,别忘了移除这里添加的监听器,详见 时序与生命周期。
React 与 Vue 绑定
Section titled “React 与 Vue 绑定”React 绑定提供 isSeeking 属性,对应 setCurrentTime 的第二个参数,可以在跳转时传入:
<LyricPlayer lyricLines={lyricLines} currentTime={currentTime} isSeeking={isSeeking} playing={playing}/>由于自动推导默认启用,这个属性通常可以省略。和原生方式一样,它也不应长期保持为 true。
这个属性只标注 currentTime 的变化,本身不会触发推送,因此 currentTime 未变化时改动它不产生任何效果。
Vue 绑定没有对应的属性,但自动推导默认启用,因此同步 currentTime 就能正确处理跳转。如果需要显式标记跳转或关闭自动推导,可以通过组件 ref 取得底层 lyricPlayer 后自行调用对应方法。
- 播放期间持续用
setCurrentTime推送进度,不要只在跳转时调用。 - 进度来源的粒度不应粗于推送间隔。
- 使用倍速播放时保持逐帧推送,并注意组件不支持倍速,逐字遮罩仍会以 1 倍速推进。
- 已知发生跳转时(歌词行点击等),可选用 seek 标志显式告知。
- 不要让 seek 标志长期保持为
true。