blog

(太郎はInnerAudioContextをベースに基本的なオーディオ・コンポーネントをラップしている。)

オーディオコンポーネントをカプセル化する理由\n\nオーディオコンポーネントの要件と制限\nクリックによる再生・一時停止\n再生の進行状況と合計時間の表示\nアイコンの変化による現在のオーディオ状態の...

Feb 9, 2020 · 13 min. read
シェア

オーディオコンポーネントをカプセル化する理由

オーディオコンポーネントの要件と制限

  1. クリックによる再生または一時停止
  2. 再生の進行状況と合計時間の表示
  3. アイコンの変化による現在のオーディオステータスの表示
  4. ページのオーディオが更新されると、コンポーネントの状態を更新
  5. グローバルに再生されるオーディオは1つだけです。
  6. ページを離れると自動的に再生を停止し、オーディオインスタンスを破棄します。

材料

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>

質問

一見したところ、このコンポーネントは既に

  1. クリックによる再生または一時停止
  2. 再生の進行状況と合計時間の表示
  3. アイコンを変更することで、オーディオの現在の状態を表示します。

しかし、このコンポーネントにはまだいくつかの問題があります:

  1. ページがアンロードされた後、innerAudioContext オブジェクトは停止されず、リサイクルされます。
  2. ページに同時に再生できる複数のオーディオコンポーネントがある場合、オーディオが乱雑になり、パフォーマンスが低下する可能性があります。
  3. 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;
 }
}
:.☆/ :.
Read next

マルチスレッド同時実行は、JAVAのメモリモデルの基盤をサポートする

Javaのメモリモデルは、Javaの並行処理を理解するために、Javaのメモリモデルを理解し、Javaの並行処理の基盤となるサポートと言うことができます。 ある変数 "value = 1; "をメモリにセットした場合、他のスレッドはその結果をいつ読めるでしょうか?すぐに読めるとは限りません。たとえば、命令の順序がソースコードの順序と異なっていたり、コンパイラが変数をレジスタに保存する代わりに...

Feb 9, 2020 · 4 min read