Taro (React+TS) encapsulates a basic audio component based on InnerAudioContext (uni app (vue) (updated later)

Why encapsulate an audio component

This is mainly because the official audio of wechat applet is not maintained, and there are some problems on many iOS real machines, such as clicking can not be played, and the total time is not displayed

Requirements and limitations of audio components

  1. Click play or pause
  2. Display playback progress and total duration
  3. Display the current audio status through icon change (pause / playing / loading)
  4. Refresh component status when page audio is updated
  5. There is and only one audio is playing globally
  6. After leaving the page, you should automatically stop playing and destroy the audio instance

Material Science:

icon_loading.gif
icon_playing.png
icon_paused.png

Properties and methods provided by InnerAudioContext

Properties:

string src: the address of the audio resource, which is used for direct playback.
bumber startTime: the position where the playback starts (unit: s). The default is 0
boolean autoplay: whether to start playing automatically. The default is false
boolean loop: whether to cycle playback. The default value is false
number volume: volume. Range 0 ~ 1. The default is 1
number playbackRate: playback speed. The range is 0.5-2.0, and the default is 1. (Android requires version 6 and above)
number duration: the length of the current audio (unit: s). Return only if there is currently a valid src (read-only)
number currentTime: the playback position of the current audio (unit: s). It is returned only when there is a valid src currently, and the time remains 6 digits after the decimal point (read-only)
Boolean: Yes or no
number buffered: the time point of audio buffering, which only ensures that the content at this time point is buffered (read-only)

method:

play(): play
pause(): pause. The audio playback after pause will start from the pause
stop(): stop. The stopped audio playback will start from the beginning.
Seek (positions: number): jump to the specified position
Destroy(): destroy the current instance
On can play (callback): listen for events when the audio enters the playable state. But there is no guarantee that the back can be played smoothly
The status of the audio playback can be canceled: the event of the playback can be canceled
onPlay(callback): listen for audio playback events
offPlay(callback): cancels listening for audio playback events
onPause(callback): listen for audio pause events
Off pause (callback): cancels listening for audio pause events
onStop(callback): listen for audio stop events
Off stop (callback): cancels listening for audio stop events
onEnded(callback): listen for events from the natural playback of audio to the end
Off ended (callback): cancels the event that the listening audio is naturally played to the end
onTimeUpdate(callback): listen for audio playback progress update events
offTimeUpdate(callback): cancels listening for audio playback progress update events
onError(callback): listen for audio playback error events
offError(callbcak): cancels listening for audio playback error events
onWaiting(callback): listen for events in audio loading. Triggered when the audio needs to stop loading due to insufficient data
offWaiting(callback): cancels listening for events in audio loading
onSeeking(callback): listen for audio jump events
offSeeking(callback): an event that cancels listening to audio for jump operation
onSeeked(callback): listen for the event that the audio completes the jump operation
offSeeked(callback): cancels listening for audio and completes the jump operation

Let's start 🛠

Taro(React + TS)

  • First, build a simple jsx structure:
<!-- playOrPauseAudio()Is a way to play or pause audio -->
<!-- fmtSecond(time)Is a method of formatting seconds into minutes: seconds -->
<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>
  • Define the parameters that the component accepts
type PageOwnProps = {
  audioSrc: string // src of incoming audio
}
  • Define the initialization related operations of CustomAudio components, and add a write behavior to the callback of 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 instance
  audioImg: string // Current audio icon ID
  currentTime: number // Current playback time
  duration: number // Total current audio duration
}

class CustomAudio extends Component<{}, StateInterface> {

  constructor(props) {
    super(props)
    this.fmtSecond = this.fmtSecond.bind(this)
    this.state = {
      audioCtx: Taro.createInnerAudioContext(),
      audioImg: iconLoading, // The default is the state in loading audio
      currentTime: 0,
      duration: 0
    }
  }

  componentWillMount() {
    const {
      audioCtx,
      audioImg
    } = this.state
    audioCtx.src = this.props.audioSrc
    // When playing, change the current playback duration and total duration through the callback of TimeUpdate (the total duration update will make an error when put into the onCanplay callback)
    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')
    })
    // When the audio can be played, change the status from loading to playable
    audioCtx.onCanplay(() => {
      if (audioImg === iconLoading) {
        this.setAudioImg(iconPaused)
        console.log('onCanplay')
      }
    })
    // When the audio is buffered, change the state to loading
    audioCtx.onWaiting(() => {
      if (audioImg !== iconLoading) {
        this.setAudioImg(iconLoading)
      }
    })
    // After playing, change the icon status to playing
    audioCtx.onPlay(() => {
      console.log('onPlay')
      this.setAudioImg(iconPlaying)
    })
    // After pause, change the icon status to pause
    audioCtx.onPause(() => {
      console.log('onPause')
      this.setAudioImg(iconPaused)
    })
    // Change icon status after playback
    audioCtx.onEnded(() => {
      console.log('onEnded')
      if (audioImg !== iconPaused) {
        this.setAudioImg(iconPaused)
      }
    })
    // An exception is thrown when audio loading fails
    audioCtx.onError((e) => {
      Taro.showToast({
        title: 'Audio loading failed',
        icon: 'none'
      })
      throw new Error(e.errMsg)
    })
  }

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

  // Play or pause
  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('Must be numeric type')
      } 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>

problem

At first glance, our components have met the requirements

  1. Click play or pause
  2. Display playback progress and total duration
  3. Display the current audio status through icon change (pause / playing / loading)

But there are still some problems with this component:

  1. After the page is unloaded, the playback and recycling of innerAudioContext object are not stopped
  2. If there are multiple audio components on a page, these components can be played at the same time, which will lead to confusion of sound source and degradation of performance
  3. Because the property of innerAudioContext is initialized in ComponentWillMount, when audioSrc in props changes, the component itself will not update the sound source, playback status and playback duration of the component

improvement

Add some behaviors in componentWillReceiveProps. When the audioSrc in props is updated, the sound source of the component is also updated, and the playback duration and status are also updated

componentWillReceiveProps(nextProps) {
  const newSrc = nextProps.audioSrc || ''
  console.log('componentWillReceiveProps', nextProps)
  if (this.props.audioSrc !== newSrc && newSrc !== '') {
    const audioCtx = this.state.audioCtx
    if (!audioCtx.paused) { // If it is still playing, stop playing first
        audioCtx.stop()
    }
    audioCtx.src = nextProps.audioSrc
    // Reset current playback time and total duration
    this.setState({
      currentTime: 0,
      duration: 0,
    })
  }
}

At this time, when we switch the sound source, there will be no problem of playing the old sound source

Stop playing and destroy innerAudioContext in componentWillUnmount to improve performance

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

A global variable audioPlaying is used to ensure that only one audio component can be played globally

// Define global variables in Taro, and use the defined get and set methods to obtain and change data according to the following specifications, directly through Taro Getapp () doesn't work
// src/lib/Global.ts
const globalData = {
  audioPlaying: false, // No audio components are playing by default
}

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

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

We package two functions to determine whether the current sound source can be played: before audioplay and after audioplay

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

// Every time a sound source pauses or stops playing, reset the global ID audioPlaying to false so that subsequent audio can be played
export function afterAudioPlay() {
  setGlobalData('audioPlaying', false)
}

// Before each audio playback, check whether the global variable audioPlaying is true. If true, the current audio cannot be played. You need to end the previous audio or manually pause or stop the previous audio playback. If false, return true and set audioPlaying to true
export function beforeAudioPlay() {
  const audioPlaying = getGlobalData('audioPlaying')
  if (audioPlaying) {
    Taro.showToast({
      title: 'Please pause other audio playback first',
      icon: 'none'
    })
    return false
  } else {
    setGlobalData('audioPlaying', true)
    return true
  }
}

Next, let's transform the previous CustomAudio component

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

/* ... */
// Don't forget to change the state of global audioPlaying when the playback is stopped due to component unloading
componentWillUnmount() {
  console.log('componentWillUnmount')
  this.state.audioCtx.stop()
  this.state.audioCtx.destory()
  ++ afterAudioPlay()
}

/* ... */
// After audioplay () needs to be executed every time the player pauses or finishes playing to give the opportunity to play audio to other audio components
audioCtx.onPause(() => {
  console.log('onPause')
  this.setAudioImg(iconPaused)
  ++ afterAudioPlay()
})
audioCtx.onEnded(() => {
  console.log('onEnded')
  if (audioImg !== iconPaused) {
    this.setAudioImg(iconPaused)
  }
  ++ afterAudioPlay()
})

/* ... */

// Before playing, check whether there are other audio being played. If not, the current audio can be played
playOrStopAudio() {
  const audioCtx = this.state.audioCtx
  if (audioCtx.paused) {
    ++ if (beforeAudioPlay()) {
      audioCtx.play()
    ++ }
  } else {
    audioCtx.pause()
  }
}

Final code

// 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
    // When playing, change the current playback duration and total duration through the callback of TimeUpdate (the total duration update will make an error when put into the onCanplay callback)
    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')
    })
    // When the audio can be played, change the status from loading to playable
    audioCtx.onCanplay(() => {
      if (audioImg === iconLoading) {
        this.setAudioImg(iconPaused)
        console.log('onCanplay')
      }
    })
    // When the audio is buffered, change the state to loading
    audioCtx.onWaiting(() => {
      if (audioImg !== iconLoading) {
        this.setAudioImg(iconLoading)
      }
    })
    // After playing, change the icon status to playing
    audioCtx.onPlay(() => {
      console.log('onPlay')
      this.setAudioImg(iconPlaying)
    })
    // After pause, change the icon status to pause
    audioCtx.onPause(() => {
      console.log('onPause')
      this.setAudioImg(iconPaused)
      afterAudioPlay()
    })
    // Change icon status after playback
    audioCtx.onEnded(() => {
      console.log('onEnded')
      if (audioImg !== iconPaused) {
        this.setAudioImg(iconPaused)
      }
      afterAudioPlay()
    })
    // An exception is thrown when audio loading fails
    audioCtx.onError((e) => {
      Taro.showToast({
        title: 'Audio loading failed',
        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) { // If it is still playing, stop playing first
        audioCtx.stop()
      }
      audioCtx.src = nextProps.audioSrc
      // Reset current playback time and total duration
      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('Must be numeric type')
      } 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>

Provide a style file, or you can play it by yourself

// 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;
  }
}

Final effect~

★,°:. ☆( ̄▽ ̄)/$:*. °★* . Perfect * ★, ° *: ☆( ̄▽ ̄)/$:. °★ . 🎉🎉🎉

If you have any good suggestions, you can discuss with me in the comment area. Don't forget to like the collection and sharing. The next issue will be the uni app version~

Tags: React TypeScript Vue.js html Mini Program

Posted by CashBuggers on Wed, 25 May 2022 02:19:32 +0300