オーディオコンポーネントをカプセル化する理由
オーディオコンポーネントの要件と制限
- クリックによる再生または一時停止
- 再生の進行状況と合計時間の表示
- アイコンの変化による現在のオーディオステータスの表示
- ページのオーディオが更新されると、コンポーネントの状態を更新
- グローバルに再生されるオーディオは1つだけです。
- ページを離れると自動的に再生を停止し、オーディオインスタンスを破棄します。
材料
icon_loading.gificon_playing.pngicon_paused.png
InnerAudioContextが提供するプロパティとメソッド
プロパティ
string src: 直接再生するオーディオリソースのアドレス。
bumber startTime: 再生を開始する位置。
boolean autoplay: 再生を自動的に開始するかどうか。
boolean loop: ループ再生するかどうか、デフォルトはfalse
number volume:音量。デフォルトは1
number playbackRate:再生速度。範囲は0.5-2.0、デフォルトは1。
number duration: 現在のオーディオの長さ。正規のsrcがある場合のみ返されます。
number currentTime: 現在のオーディオの再生位置。現在の正規のsrcが存在する場合のみ返され、時間は小数点以下6桁で保持されます。
boolean paused: 現在の状態が一時停止または停止されているかどうか。
number buffered: オーディオがバッファリングされる時点。現在の再生時間のみが、この時点までバッファリングされることが保証されます。
メソッドを使用します:
play(): プレイ
pause():一時停止。一時停止後のオーディオ再生は、一時停止した時点から開始されます。
stop():停止。一時停止後のオーディオ再生は最初から始まります。
seek(postions: number):指定した場所にジャンプ
destory(): 現在のインスタンスを破棄
onCanplay(callback):オーディオの再生準備ができたときにイベントを聞きます。その後のスムーズな再生を保証するものではありません。
offCanplay(callback)onCanplay(コールバック): オーディオの再生準備ができたときにイベントを待ちます。
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)onWaiting(callback):音声読み込みイベントを待ちます。
onSeeking(callback):オーディオがジャンプしたときのイベントを待ちます。
offSeeking(callback)onSeeking(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 // 現在のオーディオアイコンロゴ
 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コールバックで現在の再生時間と合計時間を変更する。
 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')
 })
 // 音声が再生可能になったら、ステータスをロードから再生可能に変更する。
 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 % ) >= 0 ? Math.floor(time % ) : 0,
 second = Math.floor(time % ) >=0 ? Math.floor(time % ) : 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 オブジェクトは停止されず、リサイクルされます。
- ページに同時に再生できる複数のオーディオコンポーネントがある場合、オーディオが乱雑になり、パフォーマンスが低下する可能性があります。
- innerAudioContextプロパティはComponentWillMountで初期化されるため、propsのaudioSrcが変更されても、コンポーネント自体はオーディオソース、コンポーネントの再生状態、再生時間を更新しません。
改善点
componentWillReceivePropspropsの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によって、1つのオーディオコンポーネントだけがグローバルに再生されるようになります。
// 太郎でのグローバル変数の定義は以下の仕様に従い、データの取得や変更も、定義されたgetメソッドやsetメソッドを使い、直接太郎を通して行う。.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の2つの関数をラップして、現在のオーディオソースを再生できるかどうかを判断します。
// 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コールバックで現在の再生時間と合計時間を変更する。
 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')
 })
 // 音声が再生可能になったら、ステータスをロードから再生可能に変更する。
 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 % ) >= 0 ? Math.floor(time % ) : 0,
 second = Math.floor(time % ) >=0 ? Math.floor(time % ) : 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;
 }
}





