タイトルだけだとが訳わからないと思うので、図を用いて説明します。
もっと良いやり方あったら教えてください。
やりたかったこと
ブラウザのカメラで得た動画から毎秒画像を切り出し、同時に録音している音声と併せてサーバーに送りたかったのです。
つまり、画像は1秒おきのもの、音声はその画像間の1秒間のものです。

MediaRecorderの音声取得が面倒臭い
画像はMediaRecorderのストリーミング切り出しでsetIntervalでやればすぐできますが、音声取得が厄介。
MediaRecoder.start()で録音開始、.stop()で録音終了ですが、stop後すぐに音声データは手に入りません。
エンコードしているため、'dataavailable'というイベントの発火を待たないといけないのです。
そのため、通常ならdataavailableイベントが発火する前に、画像が送信されてしまいます。
イベントの発火待ちなので、通常のasync/awaitも使えません。
具体的にやりたいこと
- 親のstate変更で、子のMediaRecorderの録音を終了させ、待機。
- 子は録音終了後、dataavailableイベントの発火を待つ。
- dataavailableイベント発火が終わったら、待ってた親に子が音声データを渡す
親のstate変更で子の録音終了なので、renderによる子へのprops渡しが実際のトリガーです。
renderを非同期にはできないらしいので困りました。
解決策
子の準備完了(dataavailable → 録音後の処理)をカスタムイベントとして、親がそれを待機。
子はMediaRecorderのdataavailableイベントを待機して、発火したら音声処理をした後、親が待つカスタムイベントを発火。
つまり、親子それぞれイベントを待たせといて、MediaRecoderのdataavailableイベントで順次イベント発火バケツリレー返しする感じです。

具体的なコード
録音その他の処理は省きます。
dataavailableイベント発火を待つ(その先に行かせない)方法
以下のコードで、子はdataavailableイベントが発火しないと先に進まない状態になります。
waitForDataAvailableEvent=()=>{
return new Promise(resolve=>{
const listener = resolve;
mediaRecorder.addEventListener('dataavailable', listener);
})
}
componentWillRecieveProps=(props)=>{
mediaRecorder.stop()
await waitForDataAvailableEvent()
// 'dataavailable'イベントが発火されないとこの先には進まない
hoge()
}
親はカスタムイベントを待つ
同様に親もやるのですが、まずはconstructorでカスタムイベントを登録しておきます。
そして、てきとうなDOM(今回は#audio)にそのイベントをつけ、発火を待ちます。
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を待ってるので、子にそれをやらせます。
- 親で作ったfinishRecordingEventを子に渡す
- 子が#audioでfinishRecordingEventを発火
という手順です。
render(){
return(
<child finishRecordingEvent={this.state.finishRecordingEvent}/>
)
}
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を使って対処するのがベストプラクティスなのでしょうか。
それもまた非同期で面倒かも。
何か良さげな案あったら教えてください!