reactJs写一个带字幕的音乐播放器

641 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

今天学习如何给自己的音乐播放器配上同步的歌词字幕,并且过长的字幕还能够自动移动到视野中。

1、获取歌词和时长

从网易云app上获取歌词和字幕时长,并记录为json文件

json格式:

// lyrics文件
export default [
  { t: '2', w: 'There once was a ship that put to sea', c: '曾经有一艘船出海远航' },
  { t: '5', w: "And the name of that ship was the Billy o' Tea", c: '那艘船的名字是“比利·茶”号' },
  { t: '7', w: "The winds blew hard, her bow dipped down", c: '风刮得很大,船头向下倾' },
  ...
];

t表示播放开始时间单位秒,w是英文字幕,c是中文字幕,如果要更准确的设定字幕播放时长,还可以定义一个播放结束时间。

2、文字颜色移动

通过设置background-position属性来移动渐变颜色

...
background-image: linear-gradient(to right, #df5000 0%, #df5000 50%, #999 50%, #999 100%);
background-size: 200%;
-webkit-background-clip: text;
color: transparent;
background-position: 100% center;
animation: textColorMove 1s linear;
animation-fill-mode: forwards;
...
@keyframes textColorMove {
  to {
    background-position: 0% center;
  }
}

3、歌词时间判断

首先要获取当前时间下的歌词:

<audio
...
onTimeUpdate={() => {
  if (audioRef.current) {
    ...
    getCurWord();
    ...
  }
}}
...

监听Audio的onTimeUpdate事件,getCurWord函数获取当前时间下的歌词:

const getCurWord = () => {
    if (!audioRef.current) return;
    const curTime = audioRef.current.currentTime;
    const { lyrics } = curSong;
    const index = lyrics.findIndex((item: any, i: number) => {
      return (
        +item.t <= curTime &&
        (i == lyrics.length - 1 || curTime < +lyrics[i + 1].t)
      );
    });
    if (index != curWord) {
      setCurWord(index);
      onceRef.current = false;
      once2Ref.current = false;
    }
};

判断歌词json中 lyrics[i].t < audio.currentTime并且lyrics[i + 1].t > audio.currentTime。

歌词元素展示:

{curWord != -1 && curSong.lyrics[curWord] && (
  <div
    className={styles.word}
    key={curWord + 'ol'}
    style={{ animationDuration: offsetTime + 's' }}
    ref={(dom) => setDomAnimation(dom, 0)}
  >
    {curWord != -1 && curSong.lyrics[curWord].w}
  </div>
)}

key是必要的,当curWord切换时,因为key不同,元素div会重新渲染并且重置属性animation。setDomAnimation函数用来修改动画相关css变量。

wrapper是固定外壳宽度,width是文字长度。如果文字长度大于wrapper,视野中左移长度为width - wrapper,offsetTime为上下两个歌词的时间差。t1和t2两个时间段和等于offsetTime,offsetTime / width表示px/s,t1 = 左移长度 * px/s,t2表示停留时长。如果文字长度小于wrapper,t1等于0。

const setDomAnimation = (dom: HTMLDivElement | null, type: 0 | 1,) => {
    const curref = type == 0 ? onceRef : once2Ref;
    if (dom && !curref.current) {
      curref.current = true;
      const width = dom.clientWidth;
      const lyricsDom = document.getElementById('lyrics');
      const wrapper = lyricsDom?.clientWidth ?? 0;
      if (width > wrapper) {
        const t1 = Number(
          (((width - wrapper) * offsetTime) / width).toFixed(1),
        );
        let time = offsetTime - t1;
        time = time - 1 < 0 ? 0 : time - 1;
        dom.style.setProperty('--offset', wrapper - width + 'px');
        dom.style.setProperty('--time0', offsetTime + 's');
        dom.style.setProperty('--time', t1 + 's');
        dom.style.setProperty('--delay', time + 's');
        setTimeout(() => {
          if (!dom.className.includes(styles.animate)) {
            dom.className = dom.className + ' ' + styles.animate;
          }
        }, 10);
      }
    }
}
.animate {
  animation-name: textColorMove, textMove;
  animation-duration: var(--time0), var(--time) !important;
  animation-timing-function: linear;
  animation-delay: 0s, var(--delay);
  animation-fill-mode: forwards;
}
@keyframes textMove {
  to {
    left: var(--offset);
  }
}

textMove动画中--offset定义文字过长时向左移动的距离。

Ending...