小程序 - 音频播放

133 阅读2分钟
<wxs src="/wxs/format.wxs" module="fmt" />

<view class="song">
  <view class="album">
    <image src="{{song.al.picUrl}}" class="poster" mode="widthFix"/>
  </view>

  <view class="title">{{song.name}}</view>
  <view class="author">{{fmt.formatAc(song.ar)}}</view>

  <view class="lyric">{{currentLyric}}</view>

  <!--
    slider是进度条
    只有当value的值是整数的时候,slider才会向前移动
  -->
  <slider
    block-size="12"
    class="progress"
    value="{{audioProgress}}"
    bindchange="sliderBindchange"
    bindchanging="sliderBindchanging"
  />

  <view class="time">
    <text>{{fmt.formatTime(currentTime)}}</text>
    <text>{{fmt.formatTime(durationTime)}}</text>
  </view>

  <view class="operation">
    <image
      wx:if="{{modeNames[playMode]}}"
      class="btn mode"
      src="{{'./images/play_' + modeNames[playMode] + '.png'}}"
      bindtap="changePlayModeAction"
    />

    <image
      class="btn prev"
      src="./images/play_prev.png"
      bindtap="prevSong"
    />

    <image
      class="btn pause"
      src="{{ isPlaying ? './images/play_pause.png' : './images/play_resume.png' }}"
      bindtap="changePlayerStatus"
    />

    <image
      class="btn next"
      src="./images/play_next.png"
      bindtap="nextSong"
    />

    <image
      class="btn music"
      src="./images/play_music.png"
    />
  </view>
</view>
import { throttle } from 'underscore'

// 创建音频上下文 --- 创建对应的播放器
const audioContext = wx.createInnerAudioContext()

Component({
  properties: {
    song: {
      type: Object,
      value: {}
    },

    lyrics: {
      type: Array,
      value: []
    }
  },

  data: {
    isPlaying: true,
    isMoving: false,
    currentIndex: 0,
    currentTime: 0,
    durationTime: 0,
    audioProgress: 0,
    currentLyric: 0
  },

  lifetimes: {
    attached() {
      this.playMusic()

      this.setData({
        durationTime: this.properties.song.dt
      })
    }
  },

  methods: {
    // 切换播放模式
    changePlayerStatus() {
      this.setData({
        isPlaying: !this.data.isPlaying
      })

      // audioContext.paused可以用来判断当前音频是否处于暂停状态
      if (audioContext.paused) {
        audioContext.play()
      } else {
        audioContext.pause()
      }
    },

    // 播放音频
    async playMusic() {
      // 终止前一首歌曲的播放
      audioContext.stop()

      audioContext.src = `https://music.163.com/song/media/outer/url?id=${this.properties.song.id}.mp3`

      // 设置自动播放
      audioContext.autoplay = true

      audioContext.onError(err => {
        console.log('audioContext error', err);
      })

      // 监听音频进度改变事件 -- 事件不需要频繁被触发,使用防抖进行处理
      audioContext.onTimeUpdate(throttle(() => {
        // 移动滑块的时候,不需要进行任何的操作
        if (!this.data.isMoving) {
          this.setData({
            // audioContext所获取到的时间的单位为S, 转换为ms
            currentTime: audioContext.currentTime * 1000,
            audioProgress: this.data.currentTime / this.durationTime * 100
          })

          this.matchCurrentMusic()
        }
      }, 1000))

      // 注意: 当拖动或点击进度条 到一个新的播放点的时候
      // 此时数据并没有完成被加载下来,会执行onWaiting回调
      // 数据加载完毕后会执行对应的onCanplay回调
      // 但是此时并不会重新触发onTimeUpdate
      // 只有重新播放才会再次重新触发onTimeUpdate回调
      // 所以需要在缓存的时候中止播放,缓存完毕后再重新进行播放
      audioContext.onWaiting(() => {
        audioContext.pause()
      })

      audioContext.onCanplay(() => {
        // 如果原本处于播放中 则继续播放音频
        // 如果原本处于暂停中 则更新歌词
        if (audioContext.paused) {
          audioContext.play()
        } else {
          this.matchCurrentMusic()
        }
      })

      // 歌曲播放完成 触发的回调
      // audioContext.onEnded(() => {})
    },


    // 滑块改变完成时候触发的回调
    sliderBindchange(e) {
      if (!audioContext.buffered) {
        return
      }

      const progress = e.detail.value

      this.setData({
        audioProgress: progress
      })

      const currentTime = this.data.durationTime * progress / 100

      audioContext.seek(currentTime / 1000)

      this.data.isMoving = false
    },

    // 滑块触发过程中 触发的回调
    sliderBindchanging: throttle(function(e) {
      if (this.data.isWaiting) {
        return
      }

      this.data.isMoving = true
      this.setData({
        currentTime: this.data.durationTime * e.detail.value / 100
      })
    }, 500),

    // 歌词匹配
    // [00:22.240]天空好想下雨
    // [00:24.380]我好想住你隔壁
    // [00:26.810]傻站在你家楼下
    //
    //  会被转换为
    //  [
    //    { time: 22240, content: '天空好想下雨' },
    //    { time: 24380, content: '我好想住你隔壁' },
    //    { time: 26810, content: '傻站在你家楼下' }
    //  ]
    matchCurrentMusic() {
      let currentTime = parseInt(audioContext.currentTime * 1000)

      // 设置默认索引为最后一条歌词
      let currentIndex = this.properties.lyrics.length - 1

      // 找到第一条时间大于当前时间的歌词
      // 那么当前歌词极为匹配到的歌词索引 - 1
      for (const index in this.properties.lyrics) {
        const lyric = this.properties.lyrics[index]

        if (lyric.time > currentTime) {
          currentIndex = index - 1

          if (currentIndex < 0) {
            currentIndex = 0
          }
        }
      }

      // 只有当前歌词索引发生了改变,才会去更新最新的当前歌词
      if (this.data.currentIndex !== currentIndex) {
        this.data.currentIndex = currentIndex

        this.setData({
          currentLyric: this.properties.lyrics[currentIndex].lyric
        })
      }
    }
  }
})