nuxt中自定义音频播放组件

写在前面

在nuxt中自定义音频播放组件,遇到了一个问题:当存在多个音频播放器时,一次只允许播放一个音频。Google了大半天,写出来的效果并不好,后面老大出手解决了,觉得他的这个方式很精巧,记录下以备忘。

最终效果

做一个最简单的自定义音频播放组件AudioPlayer,props中只有一个参数:文件的source地址。

效果图:

实现细节

为了实现这个效果,需要将功能拆分下:

1、获取音频文件的时长duration,这个可以通过audio的canplay事件。

2、获取音频的currentTime, 这个可以通过audio的timeupdate事件

3、用变量state来显示audio的三种状态:paused, playing, stopped,并分别定义三种状态下的方法: play(), pause(), stop(),根据用户操作来调用不同的方法。

4、定义numberToStardardMinuteSecond函数,将currentTime,duration处理成时间,这里引入库numbro来做这件事。

5、自定义进度条,添加click事件处理函数seek,通过获取点击位置占整个长度的百分比,换算出此时audio的currentTime,设置audio的currentTime

6、使用eventbus进行组件间的通信,在audio play之前,触发beforeplay事件,同时在AuidoPlay组件中监听beforeplay事件。

分步来:

先布局出音频播放组件的模子:

<template>
  <div class="d-flex w-100 align-items-top">
    <div class="audio-player mr-2">
      <audio :src="src" ref="audio" />
      <i v-if="state === 'playing'" class="far fa-pause-circle fa-2x" @click="pause"></i>
      <i v-if="state != 'playing'" class="far fa-play-circle fa-2x" @click="play"></i>
    </div>
    <div class="w-100">
      <span class="float-right text-muted small">{{ currentTime }} / {{ mediaDuration }}</span>
      <span class="small">点击后开始播放</span>
      <div class="progress">
        <div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
      </div>
    </div>
  </div>
</template>

<script>
import numbro from 'numbro'

export default {
  props: [ 'src' ],
  data() {
    return {
      currentTime: 0,
      mediaDuration: 0,
      state: 'stopped'
    }
  },
  computed: {
    currentPercentage() {
      if (this.mediaDuration === 0) {
        return numbro(this.mediaDuration).format({ output: 'percent', mantissa: 0 })
      } else {
        return numbro(this.currentTime / this.mediaDuration ).format({ output: 'percent', mantissa: 0 })
      }
    }
  }
}
</script>

Step1: 获取duration

在audio上添加事件监听@canplay,在methods中定义对应的处理函数onCanplay。

<audio :src="src" ref="audio" @canplay="onCanplay" />

.....

    onCanplay(event) {
      const audio = event.target
      this.mediaDuration = parseInt(audio.duration)
    }

Step2: 获取currentTime

在audio上添加事件监听@timeupdate,在methods中定义对应的处理函数onTimeupdate. 这里调用的stop函数,会在下一步定义。

<audio :src="src" @canplay="onCanplay" @timeupdate="onTimeupdate" ref="audio" />
.......
    onTimeupdate(event) {
      const audio = event.target
      if (audio.currentTime >= this.mediaDuration) {
        this.currentTime = this.mediaDuration
        this.stop()
      } else {
        this.currentTime = audio.currentTime
      }
    }

Step3: 在methods中,定义play, pause, stop函数,用以处理audio的状态。

    play() {
      const audio = this.$refs.audio
      if (this.state === 'stopped') {
        audio.currentTime = 0
      }
      if (audio.paused) {
        audio.play()
      }
      this.state = 'playing'
    },
    pause() {
      const audio = this.$refs.audio
      if (!audio.paused) {
        audio.pause()
      }
      this.state = 'paused'
    },

    stop() {
      const audio = this.$refs.audio
      if (!audio.paused) {
        audio.pause()
      }
      this.state = 'stopped'
    }

Step4: 定义numberToStardardMinuteSecond函数,将currentTime,duration转化为时间。

引入numbro.

yarn add numbro

在methods中定义numberToStardardMinuteSecond:

    numberToStardardMinuteSecond(num) {
      const number = parseInt(num)
      const time = numbro(number).format({ output: 'time' })

      if (time.substr(0, 1) === '0') {
        return time.substr(2, time.length - 2)
      } else {
        return time
      }
    }

Step5: 在进度条上监听click事件,在methods中定义seek函数来处理click事件。

<div class="progress" @click="seek">
  <div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
</div>

......

    seek(event) {
      const $this = $(event.target)

      const widthclicked = event.pageX - $this.offset().left
      const totalWidth = $this.width()

      this.$refs.audio.currentTime = widthclicked / totalWidth * this.mediaDuration
    }

为了使progress可以点击,需要在style中给progress设置pointer-events。

.progress {
  pointer-events: auto;
}

Step6: 使用eventbus进行组件间的通信,实现一次只播放一个视频。

在audio play之前,$emit一个事件beforeplay, 同时将audio作为参数传出去。

修改play函数,添加事件派发:

    play() {
      const audio = this.$refs.audio
      if (this.state === 'stopped') {
        audio.currentTime = 0
      }
      if (audio.paused) {
        this.$nuxt.$emit('beforeplay', audio)
        audio.play()
      }
      this.state = 'playing'
    }

任何其他的组件都可以监听这个beforeplay事件,当然也包括AudioPlayer组件。

在AudioPlayer组件添加对beforeplay的监听:

  created() {
    this.$nuxt.$on('beforeplay', (player) => {
      if (player === this.$refs.audio) {
        return
      }
      if (this.state === 'playing') {
        this.pause()
      }
    })
  }

这里判断传过来的参数,如果是自身emit了这个事件,就跳过,如果是其他audio,同时自身还在播放,则自身停止播放。

OK,大功告成。

最终的完整代码:

<template>
  <div class="d-flex w-100 align-items-top">
    <div class="audio-player mr-2">
      <audio :src="src" @timeupdate="onTimeupdate" @canplay="onCanplay" ref="audio" />
      <i v-if="state === 'playing'" class="far fa-pause-circle fa-2x" @click="pause"></i>
      <i v-if="state != 'playing'" class="far fa-play-circle fa-2x" @click="play"></i>
    </div>
    <div class="w-100">
      <span class="float-right text-muted small">{{ numberToStardardMinuteSecond(currentTime) }} / {{ numberToStardardMinuteSecond(mediaDuration) }}</span>
      <span class="small">点击后开始播放</span>
      <div class="progress" @click="seek">
        <div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
      </div>
    </div>
  </div>
</template>

<script>
import $ from 'jquery'
import numbro from 'numbro'

export default {
  props: [ 'src' ],
  data() {
    return {
      currentTime: 0,
      mediaDuration: 0,
      state: 'stopped'
    }
  },
  created() {
    this.$nuxt.$on('beforeplay', (player) => {
      if (player === this.$refs.audio) {
        return
      }
      if (this.state === 'playing') {
        this.pause()
      }
    })
  },
  computed: {
    currentPercentage() {
      if (this.mediaDuration === 0) {
        return numbro(this.mediaDuration).format({ output: 'percent', mantissa: 0 })
      } else {
        const progress = this.currentTime / this.mediaDuration
        return numbro(progress).format({ output: 'percent', mantissa: 0 })
      }
    }
  },
  methods: {
    play() {
      const audio = this.$refs.audio
      if (this.state === 'stopped') {
        audio.currentTime = 0
      }
      if (audio.paused) {
        this.$nuxt.$emit('beforeplay', audio)
        audio.play()
      }
      this.state = 'playing'
    },

    pause() {
      const audio = this.$refs.audio
      if (!audio.paused) {
        audio.pause()
      }
      this.state = 'paused'
    },

    stop() {
      const audio = this.$refs.audio
      if (!audio.paused) {
        audio.pause()
      }
      this.state = 'stopped'
    },

    onTimeupdate(event) {
      const audio = event.target
      if (audio.currentTime >= this.mediaDuration) {
        this.currentTime = this.mediaDuration
        this.stop()
      } else {
        this.currentTime = audio.currentTime
      }
    },

    seek(event) {
      const $this = $(event.target)

      const widthclicked = event.pageX - $this.offset().left
      const totalWidth = $this.width()

      this.$refs.audio.currentTime = widthclicked / totalWidth * this.mediaDuration
    },

    onCanplay(event) {
      const audio = event.target
      this.mediaDuration = parseInt(audio.duration)
    },

    numberToStardardMinuteSecond(num) {
      const number = parseInt(num)
      const time = numbro(number).format({ output: 'time' })

      if (time.substr(0, 1) === '0') {
        return time.substr(2, time.length - 2)
      } else {
        return time
      }
    }
  }
}
</script>

参考

Media events

EventBus for Nuxt.js components