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

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

#やりたかったこと
ブラウザのカメラで得た動画から毎秒画像を切り出し、同時に録音している音声と併せてサーバーに送りたかったのです。
つまり、画像は1秒おきのもの、音声はその画像間の1秒間のものです。
![画像と音声.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371615/2e713396-811e-f98b-b538-a923ed180e74.png)




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

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

##具体的にやりたいこと
1. 親のstate変更で、子のMediaRecorderの録音を終了させ、待機。
2. 子は録音終了後、dataavailableイベントの発火を待つ。
3. dataavailableイベント発火が終わったら、待ってた親に子が音声データを渡す
![やりたいこと.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371615/c0eec58b-c967-514c-2265-67d6766c1ce3.png)



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

#解決策
子の準備完了(dataavailable → 録音後の処理)をカスタムイベントとして、親がそれを待機。
子はMediaRecorderのdataavailableイベントを待機して、発火したら音声処理をした後、親が待つカスタムイベントを発火。
つまり、親子それぞれイベントを待たせといて、MediaRecoderのdataavailableイベントで順次イベント発火バケツリレー返しする感じです。
![解決策.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371615/9a2cb44c-011a-bee2-afbe-6c77980b506f.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を使おう](https://blog.tes.co.jp/entry/2018/03/14/193601)

###子にカスタムイベントを発火させる
親は#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でカスタムイベントを作る](https://sota1235.hatenablog.com/entry/2015/12/01/101358)

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

##代替案

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

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

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

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