1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React、子から親へイベント発火逆バケツリレー!

1
Last updated at Posted at 2019-11-04

タイトルだけだとが訳わからないと思うので、図を用いて説明します。
もっと良いやり方あったら教えてください。

やりたかったこと

ブラウザのカメラで得た動画から毎秒画像を切り出し、同時に録音している音声と併せてサーバーに送りたかったのです。
つまり、画像は1秒おきのもの、音声はその画像間の1秒間のものです。
画像と音声.png

MediaRecorderの音声取得が面倒臭い

画像はMediaRecorderのストリーミング切り出しでsetIntervalでやればすぐできますが、音声取得が厄介。
MediaRecoder.start()で録音開始、.stop()で録音終了ですが、stop後すぐに音声データは手に入りません。
エンコードしているため、'dataavailable'というイベントの発火を待たないといけないのです。

そのため、通常ならdataavailableイベントが発火する前に、画像が送信されてしまいます。
イベントの発火待ちなので、通常のasync/awaitも使えません。

具体的にやりたいこと

  1. 親のstate変更で、子のMediaRecorderの録音を終了させ、待機。
  2. 子は録音終了後、dataavailableイベントの発火を待つ。
  3. dataavailableイベント発火が終わったら、待ってた親に子が音声データを渡す
    やりたいこと.png

親のstate変更で子の録音終了なので、renderによる子へのprops渡しが実際のトリガーです。
renderを非同期にはできないらしいので困りました。

解決策

子の準備完了(dataavailable → 録音後の処理)をカスタムイベントとして、親がそれを待機。
子はMediaRecorderのdataavailableイベントを待機して、発火したら音声処理をした後、親が待つカスタムイベントを発火。
つまり、親子それぞれイベントを待たせといて、MediaRecoderのdataavailableイベントで順次イベント発火バケツリレー返しする感じです。
解決策.png

具体的なコード

録音その他の処理は省きます。

dataavailableイベント発火を待つ(その先に行かせない)方法

以下のコードで、子はdataavailableイベントが発火しないと先に進まない状態になります。

child.js
	waitForDataAvailableEvent=()=>{
		return new Promise(resolve=>{
		    const listener = resolve; 				
			mediaRecorder.addEventListener('dataavailable', listener);
		})
	}

    componentWillRecieveProps=(props)=>{
        mediaRecorder.stop()
        await waitForDataAvailableEvent()
        // 'dataavailable'イベントが発火されないとこの先には進まない
        hoge()
    }

親はカスタムイベントを待つ

同様に親もやるのですが、まずはconstructorでカスタムイベントを登録しておきます。
そして、てきとうなDOM(今回は#audio)にそのイベントをつけ、発火を待ちます。

parent.js

    onstructor(props){
		super(props);
		const finishRecordingEvent = new Event('finishRecording');
        this.state = {
            finishRecordingEvent    
        }
    }

	waitForFinishRecording(){
		return new Promise(resolve=>{
			const listner = resolve
			const audio = document.getElementById('audio');
			audio.addEventListener('finishRecording', listner)
		})
	}

これで、子はMediaRecorderのdataavailableを、親は#audioのfinishRecordingEventを待つ状態になりました。

参考 : [JavaScript]イベントにもasync/awaitを使おう

子にカスタムイベントを発火させる

親は#audioのfinishRecordingEventを待ってるので、子にそれをやらせます。

  1. 親で作ったfinishRecordingEventを子に渡す
  2. 子が#audioでfinishRecordingEventを発火

という手順です。

parent.js
    render(){
        return(
            <child finishRecordingEvent={this.state.finishRecordingEvent}/>
        )
    }
child.js
    waitForDataAvailableEvent=()=>{
		return new Promise(resolve=>{
		    const listener = resolve; 				
			mediaRecorder.addEventListener('dataavailable', listener);
		})
	}
    componentWillRecieveProps=(props)=>{
        mediaRecorder.stop()
        await waitForDataAvailableEvent()
        // 'stop'イベントが発火されないとこの先には進まない
        //ここから先追記
        const audio = document.getElementById('audio');
	    audio.dispatchEvent(this.props.finishRecordingEvent)
    }

これで、親から子にカスタムイベントがpropsで渡り、子がそれをdataavailableイベントを待って、発火させることができます。
参考 : JSでカスタムイベントを作る

これで無事、録音終了を待って、画像一緒に音声を送ることができました!
もう一度流れを書いておきます。

代替案

時間にルーズな人に、時間に厳格な人が合わせようとすると面倒です。
今回は来なかった!とかなるので。
逆に、厳格な人がルーズな人に合わせると効率的です。
来た時に行けばいいだけですので。

ということで、時間にルーズな音声を1秒おきに取り出して、その時の画像を撮る方がよさそうです。
音声を取り出す方を親にすれば、イベントが一つ減らせます。
が、今回は勉強だと思って、少し複雑な方にしました。

まとめ

普段async/awaitにかこつけてPromiseを疎かにしていたので、良い勉強になりました。
単純な非同期はasync/awaitでいけますが、イベント待ちとかはPromiseまだ必要ですね。
MediaRecorderの音声をデータにしてアップロードも初めてだったので、原因究明から結構途中めんどかったです。

今回は親と子だけでしたが、孫とか増えると、propsバケツリレーの再来になりますね。
Storeを使って対処するのがベストプラクティスなのでしょうか。
それもまた非同期で面倒かも。
何か良さげな案あったら教えてください!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?