为什么要封装一个音频组件
次要因为微信小程序官网的 audio
不保护了,并且在很多 iOS
真机上的确也存在点击无奈播放,总时长不显示等问题.
音频组件的要求与限度
- 点击播放或者暂停
- 显示播放进度及总时长
- 通过图标变动显示以后音频所处状态(暂停 / 播放 / 加载中)
- 页面音频更新时刷新组件状态
- 全局有且只有一个音频处于播放状态
- 来到页面之后要主动进行播放并销毁音频实例
资料:
icon_loading.gif
icon_playing.png
icon_paused.png
InnerAudioContext 提供的属性和办法
属性:
string
src
: 音频资源的地址,用于间接播放。
bumberstartTime
: 开始播放的地位(单位:s),默认为 0
booleanautoplay
: 是否主动开始播放,默认为false
booleanloop
: 是否循环播放,默认为false
numbervolume
: 音量。范畴 0~1。默认为 1
numberplaybackRate
: 播放速度。范畴 0.5-2.0,默认为 1。(Android 须要 6 及以上版本)
numberduration
: 以后音频的长度(单位 s)。只有在以后有非法的 src 时返回(只读)
numbercurrentTime
: 以后音频的播放地位(单位 s)。只有在以后有非法的 src 时返回,工夫保留小数点后 6 位(只读)
booleanpaused
: 以后是是否暂停或进行状态(只读)
numberbuffered
: 音频缓冲的工夫点,仅保障以后播放工夫点到此工夫点内容已缓冲(只读)
办法:
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>
问题
乍一看咱们的组件曾经满足了
- 点击播放或者暂停
- 显示播放进度及总时长
- 通过图标变动显示以后音频所处状态(暂停 / 播放 / 加载中)
然而这个组件还有一些问题:
- 页面卸载之后没有对
innerAudioContext
对象进行播放和回收 - 一个页面如果有多个音频组件这些组件能够同时播放这会导致音源凌乱,性能升高
- 因为是在
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]
}
咱们通过封装两个函数去判断是否能够播放以后音源:beforeAudioPlay
和afterAudioPlay
// 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
版本的~