Taro(React+TS)基于InnerAudioContext封装一个基本的音频组件(uni-app(vue)后续更新)

为什么要封装一个音频组件

主要因为微信小程序官方的audio不维护了,并且在很多iOS真机上确实也存在点击无法播放,总时长不显示等问题.

[图片上传失败...(image-b27cf8-1600315910562)]

音频组件的要求与限制

  1. 点击播放或者暂停

  2. 显示播放进度及总时长

  3. 通过图标变化显示当前音频所处状态(暂停/播放/加载中)

  4. 页面音频更新时刷新组件状态

  5. 全局有且只有一个音频处于播放状态

  6. 离开页面之后要自动停止播放并销毁音频实例

材料:

icon_loading.gif

icon_playing.png

icon_paused.png

InnerAudioContext提供的属性和方法

属性:

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: 音频缓冲的时间点,仅保证当前播放时间点到此时间点内容已缓冲(只读)

方法

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更新时组件的音源也做一个更新,播放时长和状态也做一个更新


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;

  }

}

最终效果~

在这里插入图片描述

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

有什么好的建议大家可以在评论区跟我讨论下哈,别忘了点赞收藏分享哦,下期就更uni-app版本的~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342