Taro(React+TS)基於InnerAudioContext封裝一個基本的音頻組件(uni-app(vue)後續更新)

爲何要封裝一個音頻組件

主要由於微信小程序官方的audio不維護了,而且在不少iOS真機上確實也存在點擊沒法播放,總時長不顯示等問題.
javascript

音頻組件的要求與限制

  1. 點擊播放或者暫停
  2. 顯示播放進度及總時長
  3. 經過圖標變化顯示當前音頻所處狀態(暫停/播放/加載中)
  4. 頁面音頻更新時刷新組件狀態
  5. 全局有且只有一個音頻處於播放狀態
  6. 離開頁面以後要自動中止播放並銷燬音頻實例

材料:

icon_loading.gif
icon_playing.png
icon_paused.pngcss

InnerAudioContext提供的屬性和方法

屬性:html

string src: 音頻資源的地址,用於直接播放。
bumber startTime: 開始播放的位置(單位:s),默認爲 0
boolean autoplay: 是否自動開始播放,默認爲 false
boolean loop: 是否循環播放,默認爲 false
number volume: 音量。範圍 0~1。默認爲 1
number playbackRate: 播放速度。範圍 0.5-2.0,默認爲 1。(Android 須要 6 及以上版本)
number duration: 當前音頻的長度(單位 s)。只有在當前有合法的 src 時返回(只讀)
number currentTime: 當前音頻的播放位置(單位 s)。只有在當前有合法的 src 時返回,時間保留小數點後 6 位(只讀)
boolean paused: 當前是是否暫停或中止狀態(只讀)
number buffered: 音頻緩衝的時間點,僅保證當前播放時間點到此時間點內容已緩衝(只讀)

方法java

play(): 播放
pause(): 暫停。暫停後的音頻再播放會從暫停處開始播放
stop(): 中止。中止後的音頻再播放會從頭開始播放。
seek(postions: number):跳轉到指定位置
destory(): 銷燬當前實例
onCanplay(callback): 監聽音頻進入能夠播放狀態的事件。但不保證後面能夠流暢播放
offCanplay(callback): 取消監聽音頻進入能夠播放狀態的事件
onPlay(callback): 監聽音頻播放事件
offPlay(callback): 取消監聽音頻播放事件
onPause(callback): 監聽音頻暫停事件
offPause(callback): 取消監聽音頻暫停事件
onStop(callback): 監聽音頻中止事件
offStop(callback): 取消監聽音頻中止事件
onEnded(callback): 監聽音頻天然播放至結束的事件
offEnded(callback): 取消監聽音頻天然播放至結束的事件
onTimeUpdate(callback): 監聽音頻播放進度更新事件
offTimeUpdate(callback): 取消監聽音頻播放進度更新事件
onError(callback): 監聽音頻播放錯誤事件
offError(callbcak): 取消監聽音頻播放錯誤事件
onWaiting(callback): 監聽音頻加載中事件。當音頻由於數據不足,須要停下來加載時會觸發
offWaiting(callback): 取消監聽音頻加載中事件
onSeeking(callback): 監聽音頻進行跳轉操做的事件
offSeeking(callback): 取消監聽音頻進行跳轉操做的事件
onSeeked(callback): 監聽音頻完成跳轉操做的事件
offSeeked(callback): 取消監聽音頻完成跳轉操做的事件

讓咱們開始吧🛠小程序

Taro(React + TS)

  • 首先構建一個簡單的jsx結構:
<!-- playOrPauseAudio()是一個播放或者暫停播放音頻的方法 -->
<!-- fmtSecond(time)是一個將秒格式化爲 分:秒 的方法 -->
<View className='custom-audio'>
  <Image onClick={() => this.playOrStopAudio()} src={audioImg} className='audio-btn' />
  <Text>{this.fmtSecond(Math.floor(currentTime))}/{this.fmtSecond(Math.floor(duration))}</Text>
</View>
  • 定義組件接受的參數
type PageOwnProps = {
  audioSrc: string // 傳入的音頻的src
}
  • 定義CustomAudio組件的初始化相關的操做,並給innerAudioContext的回調添加一寫行爲
// src/components/widget/CustomAudio.tsx
import Taro, { Component, ComponentClass } from '@tarojs/taro'
import { View, Image, Text } from "@tarojs/components";

import iconPaused from '../../../assets/images/icon_paused.png'
import iconPlaying from '../../../assets/images/icon_playing.png'
import iconLoading from '../../../assets/images/icon_loading.gif'

interface StateInterface {
  audioCtx: Taro.InnerAudioContext // innerAudioContext實例
  audioImg: string // 當前音頻icon標識
  currentTime: number // 當前播放的時間
  duration: number // 當前音頻總時長
}

class CustomAudio extends Component<{}, StateInterface> {

  constructor(props) {
    super(props)
    this.fmtSecond = this.fmtSecond.bind(this)
    this.state = {
      audioCtx: Taro.createInnerAudioContext(),
      audioImg: iconLoading, // 默認是在加載音頻中的狀態
      currentTime: 0,
      duration: 0
    }
  }

  componentWillMount() {
    const {
      audioCtx,
      audioImg
    } = this.state
    audioCtx.src = this.props.audioSrc
    // 當播放的時候經過TimeUpdate的回調去更改當前播放時長和總時長(總時長更新放到onCanplay回調中會出錯)
    audioCtx.onTimeUpdate(() => {
      if (audioCtx.currentTime > 0 && audioCtx.currentTime <= 1) {
        this.setState({
          currentTime: 1
        })
      } else if (audioCtx.currentTime !== Math.floor(audioCtx.currentTime)) {
        this.setState({
          currentTime: Math.floor(audioCtx.currentTime)
        })
      }
      const tempDuration = Math.ceil(audioCtx.duration)
      if (this.state.duration !== tempDuration) {
        this.setState({
          duration: tempDuration
        })
      }
      console.log('onTimeUpdate')
    })
    // 當音頻能夠播放就將狀態從loading變爲可播放
    audioCtx.onCanplay(() => {
      if (audioImg === iconLoading) {
        this.setAudioImg(iconPaused)
        console.log('onCanplay')
      }
    })
    // 當音頻在緩衝時改變狀態爲加載中
    audioCtx.onWaiting(() => {
      if (audioImg !== iconLoading) {
        this.setAudioImg(iconLoading)
      }
    })
    // 開始播放後更改圖標狀態爲播放中
    audioCtx.onPlay(() => {
      console.log('onPlay')
      this.setAudioImg(iconPlaying)
    })
    // 暫停後更改圖標狀態爲暫停
    audioCtx.onPause(() => {
      console.log('onPause')
      this.setAudioImg(iconPaused)
    })
    // 播放結束後更改圖標狀態
    audioCtx.onEnded(() => {
      console.log('onEnded')
      if (audioImg !== iconPaused) {
        this.setAudioImg(iconPaused)
      }
    })
    // 音頻加載失敗時 拋出異常
    audioCtx.onError((e) => {
      Taro.showToast({
        title: '音頻加載失敗',
        icon: 'none'
      })
      throw new Error(e.errMsg)
    })
  }

  setAudioImg(newImg: string) {
    this.setState({
      audioImg: newImg
    })
  }

  // 播放或者暫停
  playOrStopAudio() {
    const audioCtx = this.state.audioCtx
    if (audioCtx.paused) {
      audioCtx.play()
    } else {
      audioCtx.pause()
    }
  }

  fmtSecond (time: number){
    let hour = 0
    let min = 0
    let second = 0
       if (typeof time !== 'number') {
         throw new TypeError('必須是數字類型')
      } else {
        hour = Math.floor(time / 3600) >= 0 ? Math.floor(time / 3600) : 0,
        min = Math.floor(time % 3600 / 60) >= 0 ? Math.floor(time % 3600 / 60) : 0,
        second = Math.floor(time % 3600 % 60) >=0 ? Math.floor(time % 3600 % 60) : 0
      }
    }
    return `${hour}:${min}:${second}`
  }
  
  render () {
    const {
      audioImg,
      currentTime,
      duration
    } = this.state
    return(
      <View className='custom-audio'>
        <Image onClick={() => this.playOrStopAudio()} src={audioImg} className='audio-btn' />
        <Text>{this.fmtSecond(Math.floor(currentTime))}/{this.fmtSecond(Math.floor(duration))}</Text>
      </View>
    )
  }
}

export default CustomAudio as ComponentClass<PageOwnProps, PageState>

問題

乍一看咱們的組件已經知足了微信小程序

  1. 點擊播放或者暫停
  2. 顯示播放進度及總時長
  3. 經過圖標變化顯示當前音頻所處狀態(暫停/播放/加載中)

可是這個組件還有一些問題:微信

  1. 頁面卸載以後沒有對innerAudioContext對象中止播放和回收
  2. 一個頁面若是有多個音頻組件這些組件能夠同時播放這會致使音源混亂,性能下降
  3. 由於是在ComponentWillMount中初始化了innerAudioContext的屬性因此當props中的audioSrc變化的時候組件自己不會更新音源、組件的播放狀態和播放時長

改進

componentWillReceiveProps中增長一些行爲達到props中的audioSrc更新時組件的音源也作一個更新,播放時長和狀態也作一個更新app

componentWillReceiveProps(nextProps) {
  const newSrc = nextProps.audioSrc || ''
  console.log('componentWillReceiveProps', nextProps)
  if (this.props.audioSrc !== newSrc && newSrc !== '') {
    const audioCtx = this.state.audioCtx
    if (!audioCtx.paused) { // 若是還在播放中,先進行中止播放操做
        audioCtx.stop()
    }
    audioCtx.src = nextProps.audioSrc
    // 重置當前播放時間和總時長
    this.setState({
      currentTime: 0,
      duration: 0,
    })
  }
}

這時候咱們在切換音源的時候就不會存在還在播放舊音源的問題函數

經過在componentWillUnmount中中止播放和銷燬innerAudioContext達到一個提高性能的目的

componentWillUnmount() {
  console.log('componentWillUnmount')
  this.state.audioCtx.stop()
  this.state.audioCtx.destory()
}

經過一個全局變量audioPlaying來保證全局有且僅有一個音頻組件能夠處於播放狀態

// 在Taro中定義全局變量按照一下的規範來,獲取和更改數據也要使用定義的get和set方法,直接經過Taro.getApp()是不行的
// src/lib/Global.ts
const globalData = {
  audioPlaying: false, // 默認沒有音頻組件處於播放狀態
}

export function setGlobalData (key: string, val: any) {
  globalData[key] = val
}

export function getGlobalData (key: string) {
  return globalData[key]
}

咱們經過封裝兩個函數去判斷是否能夠播放當前音源:beforeAudioPlayafterAudioPlay

// src/lib/Util.ts
import Taro from '@tarojs/taro'
import { setGlobalData, getGlobalData } from "./Global";

// 每次在一個音源暫停或者中止播放的時候將全局標識audioPlaying重置爲false,用以讓後續的音頻能夠播放
export function afterAudioPlay() {
  setGlobalData('audioPlaying', false)
}

// 在每次播放音頻以前檢查全局變量audioPlaying是否爲true,若是是true,當前音頻不能播放,須要以前的音頻結束或者手動去暫停或者中止以前的音頻播放,若是是false,返回true,並將audioPlaying置爲true
export function beforeAudioPlay() {
  const audioPlaying = getGlobalData('audioPlaying')
  if (audioPlaying) {
    Taro.showToast({
      title: '請先暫停其餘音頻播放',
      icon: 'none'
    })
    return false
  } else {
    setGlobalData('audioPlaying', true)
    return true
  }
}

接下來咱們改造以前的CustomAudio組件

import { beforeAudioPlay, afterAudioPlay } from '../../lib/Utils';

/* ... */
// 由於組件卸載致使的中止播放別忘了也要改變全局audioPlaying的狀態
componentWillUnmount() {
  console.log('componentWillUnmount')
  this.state.audioCtx.stop()
  this.state.audioCtx.destory()
  ++ afterAudioPlay()
}

/* ... */
// 每次暫停或者播放完畢的時候須要執行一次afterAudioPlay()讓出播放音頻的機會給其餘的音頻組件
audioCtx.onPause(() => {
  console.log('onPause')
  this.setAudioImg(iconPaused)
  ++ afterAudioPlay()
})
audioCtx.onEnded(() => {
  console.log('onEnded')
  if (audioImg !== iconPaused) {
    this.setAudioImg(iconPaused)
  }
  ++ afterAudioPlay()
})

/* ... */

// 播放前先檢查有沒有其餘正在播放的音頻,沒有的狀況下才能播放當前音頻
playOrStopAudio() {
  const audioCtx = this.state.audioCtx
  if (audioCtx.paused) {
    ++ if (beforeAudioPlay()) {
      audioCtx.play()
    ++ }
  } else {
    audioCtx.pause()
  }
}

最終代碼

// src/components/widget/CustomAudio.tsx
import Taro, { Component, ComponentClass } from '@tarojs/taro'
import { View, Image, Text } from "@tarojs/components";
import { beforeAudioPlay, afterAudioPlay } from '../../lib/Utils';

import './CustomAudio.scss'
import iconPaused from '../../../assets/images/icon_paused.png'
import iconPlaying from '../../../assets/images/icon_playing.png'
import iconLoading from '../../../assets/images/icon_loading.gif'

type PageStateProps = {
}

type PageDispatchProps = {
}

type PageOwnProps = {
  audioSrc: string
}

type PageState = {}

type IProps = PageStateProps & PageDispatchProps & PageOwnProps

interface CustomAudio {
  props: IProps
}

interface StateInterface {
  audioCtx: Taro.InnerAudioContext
  audioImg: string
  currentTime: number
  duration: number
}

class CustomAudio extends Component<{}, StateInterface> {

  constructor(props) {
    super(props)
    this.fmtSecond = this.fmtSecond.bind(this)
    this.state = {
      audioCtx: Taro.createInnerAudioContext(),
      audioImg: iconLoading,
      currentTime: 0,
      duration: 0
    }
  }

  componentWillMount() {
    const {
      audioCtx,
      audioImg
    } = this.state
    audioCtx.src = this.props.audioSrc
    // 當播放的時候經過TimeUpdate的回調去更改當前播放時長和總時長(總時長更新放到onCanplay回調中會出錯)
    audioCtx.onTimeUpdate(() => {
      if (audioCtx.currentTime > 0 && audioCtx.currentTime <= 1) {
        this.setState({
          currentTime: 1
        })
      } else if (audioCtx.currentTime !== Math.floor(audioCtx.currentTime)) {
        this.setState({
          currentTime: Math.floor(audioCtx.currentTime)
        })
      }
      const tempDuration = Math.ceil(audioCtx.duration)
      if (this.state.duration !== tempDuration) {
        this.setState({
          duration: tempDuration
        })
      }
      console.log('onTimeUpdate')
    })
    // 當音頻能夠播放就將狀態從loading變爲可播放
    audioCtx.onCanplay(() => {
      if (audioImg === iconLoading) {
        this.setAudioImg(iconPaused)
        console.log('onCanplay')
      }
    })
    // 當音頻在緩衝時改變狀態爲加載中
    audioCtx.onWaiting(() => {
      if (audioImg !== iconLoading) {
        this.setAudioImg(iconLoading)
      }
    })
    // 開始播放後更改圖標狀態爲播放中
    audioCtx.onPlay(() => {
      console.log('onPlay')
      this.setAudioImg(iconPlaying)
    })
    // 暫停後更改圖標狀態爲暫停
    audioCtx.onPause(() => {
      console.log('onPause')
      this.setAudioImg(iconPaused)
      afterAudioPlay()
    })
    // 播放結束後更改圖標狀態
    audioCtx.onEnded(() => {
      console.log('onEnded')
      if (audioImg !== iconPaused) {
        this.setAudioImg(iconPaused)
      }
      afterAudioPlay()
    })
    // 音頻加載失敗時 拋出異常
    audioCtx.onError((e) => {
      Taro.showToast({
        title: '音頻加載失敗',
        icon: 'none'
      })
      throw new Error(e.errMsg)
    })
  }

  componentWillReceiveProps(nextProps) {
      const newSrc = nextProps.audioSrc || ''
    console.log('componentWillReceiveProps', nextProps)
    if (this.props.audioSrc !== newSrc && newSrc !== '') {
      const audioCtx = this.state.audioCtx
      if (!audioCtx.paused) { // 若是還在播放中,先進行中止播放操做
        audioCtx.stop()
      }
      audioCtx.src = nextProps.audioSrc
      // 重置當前播放時間和總時長
      this.setState({
        currentTime: 0,
        duration: 0,
      })
    }
  }

  componentWillUnmount() {
    console.log('componentWillUnmount')
    this.state.audioCtx.stop()
    this.state.audioCtx.destory()
    afterAudioPlay()
  }

  setAudioImg(newImg: string) {
    this.setState({
      audioImg: newImg
    })
  }

  playOrStopAudio() {
    const audioCtx = this.state.audioCtx
    if (audioCtx.paused) {
      if (beforeAudioPlay()) {
        audioCtx.play()
      }
    } else {
      audioCtx.pause()
    }
  }

  fmtSecond (time: number){
    let hour = 0
    let min = 0
    let second = 0
       if (typeof time !== 'number') {
         throw new TypeError('必須是數字類型')
      } else {
        hour = Math.floor(time / 3600) >= 0 ? Math.floor(time / 3600) : 0,
        min = Math.floor(time % 3600 / 60) >= 0 ? Math.floor(time % 3600 / 60) : 0,
        second = Math.floor(time % 3600 % 60) >=0 ? Math.floor(time % 3600 % 60) : 0
      }
    }
    return `${hour}:${min}:${second}`
  }

  render () {
    const {
      audioImg,
      currentTime,
      duration
    } = this.state
    return(
      <View className='custom-audio'>
        <Image onClick={() => this.playOrStopAudio()} src={audioImg} className='audio-btn' />
        <Text>{this.fmtSecond(Math.floor(currentTime))}/{this.fmtSecond(Math.floor(duration))}</Text>
      </View>
    )
  }

}

export default CustomAudio as ComponentClass<PageOwnProps, PageState>

提供一份樣式文件,也能夠本身自行發揮

// src/components/widget/CustomAudio.scss
.custom-audio {
  border-radius: 8vw;
  border: #CCC 1px solid;
  background: #F3F6FC;
  color: #333;
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: space-between;
  padding: 2vw;
  font-size: 4vw;
  .audio-btn {
    width: 10vw;
    height: 10vw;
    white-space: nowrap;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

最終效果~

在這裏插入圖片描述

★,°:.☆( ̄▽ ̄)/$:*.°★* 。完美*★,°*:.☆( ̄▽ ̄)/$:.°★ 。🎉🎉🎉oop

有什麼好的建議你們能夠在評論區跟我討論下哈,別忘了點贊收藏分享哦,下期就更uni-app版本的~

相關文章
相關標籤/搜索