小程序音频文本同步跟随开发实践
发布于 2 年前 作者 na16 4809 次浏览 来自 分享

最近接到一个这样的需求,要实现录音播放时和文本同步,

类似于音乐播放器播放歌曲时,歌词和声音同步的效果。要实现这个功能需要监听audio标签的实时播放时间,然后在监听事件中处理文本,通过当前时间定位到对应的文本数据,并展示在页面。

原生实现

使用原生的audio标签时,需要获取audio标签dom对象,并给它添加timeupdate事件,在事件中就可以实时处理文本了,代码步骤如下

javascript

// audio标签

// 1.获取 aduio 对象
var audio = document.querySelector("#myAudio");

// 2.注册事件
audio.addEventListener("timeupdate",function(){
    // 当前播放时间,单位秒
    var nowTime = audio.currentTime;
    // ...其他处理
});

在事件中获取当前时间,然后定位到文本数据,再把文本颜色设置为红色,这样就实现了一个简单的播放器音频和文本同步的效果,最终效果图如下

React中实现(taro)

上面所说的是原生实现,但目前主流的一般会使用React或者Vue,当然也可以在React或Vue组件中操作Dom,但这个不符合框架的设计思想。另外还有一个问题,歌词很长的情况下,播放到滚动条下面时,文本会被遮住,这就要求当播放到滚动条下面,歌词被遮住时,滚动条需要自动往下滚动。

滚动条自动下滑

要实现滚动条自动下滑,有三个步骤

  1. 获取当前文本标签的Dom对象,相对于父容器(有滚动条的容器)顶部的距离:sTopHeight
  2. 获取父容器上方被滚动条遮住的高度:zHeight
  3. 获取父容器的可视高度,这个高度一般设置是固定高度:xHeight。计算公式当 sTopHeight > zHeight + xHeight 为真时,说明文本已经超出可视高度,被遮住了,这时就需要向下滑动滚动条了

以下是实现代码,这时React中直接操作Dom方式的实现

javascript

constructor(props){
    super(props);
    this.state = {
      txtList:[
        {
          start:24,
          time:"00:24",
          txt:"月色烙印在城墙"
        },{
          start:26,
          time:"00:26",
          txt:"风声呼啸过苍莽"
        },{
          start:29,
          time:"00:29",
          txt:"有双眸如炬般明亮"
        },{
          start:35,
          time:"00:35",
          txt:"将生死握于手掌"
        },{
          start:38,
          time:"00:38",
          txt:"待尘沙磨砺翅膀"
        },{
          start:41,
          time:"00:41",
          txt:"褪变出崭新模样"
        },{
          start:46,
          time:"00:46",
          txt:"那团火在何处绽放"
        },{
          start:49,
          time:"00:49",
          txt:"开始侵袭六腑五脏"
        },{
          start:51,
          time:"00:51",
          txt:"却使我全身的血脉都膨胀"
        },{
          start:57,
          time:"00:57",
          txt:"有太多的相识背叛"
        },{
          start:60,
          time:"01:00",
          txt:"不必谁原谅"
        },{
          start:63,
          time:"01:03",
          txt:"只恨这英雄年少不轻狂"
        },{
          start:68,
          time:"00:26",
          txt:"我乘风而下"
        },{
          start:70,
          time:"01:10",
          txt:"要在破晓处羽化"
        },{
          start:73,
          time:"01:13",
          txt:"要让满座的喧哗 屏息惊诧"
        },{
          start:80,
          time:"01:20",
          txt:"凭誓约回答"
        },{
          start:82,
          time:"01:26",
          txt:"两人的命运交叉"
        },{
          start:85,
          time:"01:25",
          txt:"跃再多悬壁陡崖"
        },{
          start:93,
          time:"01:33",
          txt:"我不怕"
        }
      ]
    }
}

componentDidMount(){
    const audio = document.querySelector(".myAudio>audio");
    const list = this.state.txtList;
    const txtContain = document.querySelector('#txtContain');

    audio.addEventListener("timeupdate",() => {
      for(let i=0;i #txt'+item.start);
          //可见窗体高度的一半(歌词正好在中间)+被覆盖的高度
          let zHeight= parseInt(150+txtContain.scrollTop);
          // 如果相对父容器偏移量大于(可见窗体高度+被覆盖的高度),则滚动条下移100px
          if(txtDiv.offsetTop>zHeight){
            //this.myScrollTop(txtContain,22,220);
            txtContain.scrollTop=txtContain.scrollTop+22;
          }
          this.setState({txtList:list});
          break;
        }
      }
    });
    const txtContains = document.querySelector('#txtContains');
    this.setState({txtContains:txtContains});
  }

render() {

    const choseStyle={
      color:'red',
      height:'22px'
    };

    const noStyle={
      color:'black',
      height:'22px'
    };

    return (
       

              {this.state.txtList.map( (item) =>
                  
                    [{item.time}]:{item.txt}
                  

              )}
            

    );
  }


最终效果如下

效果美化

在React中直接操作Dom虽然可以实现效果,但看起来都不怎么优雅。由于我使用了React的组件react-h5-audio-player,这个组件提供了timeupdate事件,所以优化以下代码,顺便把效果美化以下,播放行时加粗文本字体和行的阴影效果,哈哈。

javascript

  audioTimeUpdate = (currentTime) => {
    let list = this.state.txtLists;
    let txtContain = this.state.txtContains;
    for(let i=0;i #txt'+item.start);
        //可见窗体高度的一半(歌词正好在中间)+被覆盖的高度
        let zHeight= parseInt(150+txtContain.scrollTop);
        // 如果相对父容器偏移量大于(可见窗体高度+被覆盖的高度),则滚动条下移100px
        if(txtDiv.offsetTop>zHeight){
          //this.myScrollTop(txtContain,22,220);
          txtContain.scrollTop=txtContain.scrollTop+22;
        }
        this.setState({txtLists:list});
        break;
      }
    }
  };

  render() {

    const choseStyles={
      boxShadow:'0 0 50px rgb(166,124,64)',
      height:'22px',
      borderRadius:'8px',
      paddingLeft:'5px',
      fontWeight:600
    };

    const noStyles={
      height:'22px'
    };
    return (
          

             

                  {this.state.txtLists.map( (item) =>
                    
                      [{item.time}]:{item.txt}
                    

                  )}
                

        );
    }

最终效果如下

总结

在开发过程中遇到的问题,当我通过元素的offsetTop属性获取元素相对于父容器的距离时,会出现距离不符合预期的情况,这实际上是offsetTop定位机制的问题,与它类似的属性还有offsetLeft,需要注意的是offsetTopoffsetLeft定位机制是相对于最近的祖先定位元素(即父元素的 position 属性被设置为 relativeabsolute或 fixed的元素)的向上或向左偏移值。如果父元素的 position 属性非 relativeabsolute或 fixed,则都是相对于body定位。

回到顶部