import { call , delay , put , race , select , take , takeEvery , takeLatest } from 'redux-saga/effects' ; import { RECORDING_FORMAT_VERSION } from '../version' ; import { spawnWorker } from '../utils/worker_utils' ; import AudioWorker from '../audio_worker/index.worker' ; import { ActionTypes } from "./actionTypes" ; import { ActionTypes as CommonActionTypes } from '../common/actionTypes' ; import { ActionTypes as PlayerActionTypes } from '../player/actionTypes' ; import { ActionTypes as StepperActionTypes } from '../stepper/actionTypes' ; import { getPlayerState } from "../player/selectors" ; import { getRecorderState } from "./selectors" ; import { AppStore } from "../store" ; import { RecorderStatus } from "./store" ; import { ReplayContext } from "../player/sagas" ; import { App } from "../index" ; import { Screen } from "../common/screens" ; import { StepperStatus } from "../stepper" ; export default function ( bundle , deps ) { bundle . use ( 'recordApi' ) ; bundle . defineAction ( ActionTypes . RecorderResume ) ; bundle . defineAction ( ActionTypes . RecorderResuming ) ; bundle . addReducer ( ActionTypes . RecorderResuming , ( state ) => { state . recorder . status = RecorderStatus . Resuming ; } ) ; bundle . defineAction ( ActionTypes . RecorderResumed ) ; bundle . addReducer ( ActionTypes . RecorderResumed , ( state ) => { state . recorder . status = RecorderStatus . Recording ; } ) ; bundle . defineAction ( ActionTypes . AudioContextSuspended ) ; bundle . addReducer ( ActionTypes . AudioContextSuspended , ( state , { payload : { audioTime } } ) => { state . recorder . suspendedAt = audioTime ; } ) ; bundle . addSaga ( function * watchRecorderPrepare ( ) { yield takeLatest ( ActionTypes . RecorderPrepare , recorderPrepare ) ; } ) ; bundle . addSaga ( function * recorderTicker ( ) { while ( true ) { yield take ( ActionTypes . RecorderStarted ) ; while ( true ) { const outcome = yield race ( { tick : delay ( 1000 ) , stopped : take ( ActionTypes . RecorderStopped ) } ) ; if ( 'stopped' in outcome ) { break ; } const state = yield select ( ) ; const junkTime = state . recorder . junkTime ; const recorderContext = state . recorder . context ; const elapsed = Math . round ( recorderContext . audioContext . currentTime * 1000 ) - junkTime ; yield put ( { type : ActionTypes . RecorderTick , elapsed } ) ; } } } ) ; bundle . addSaga ( function * watchRecorderActions ( ) { yield takeEvery ( ActionTypes . RecorderStart , recorderStart ) ; yield takeEvery ( ActionTypes . RecorderStop , recorderStop ) ; yield takeEvery ( ActionTypes . RecorderPause , recorderPause ) ; yield takeEvery ( ActionTypes . RecorderResume , recorderResume ) ; } ) ; bundle . defer ( function ( { replayApi } ) { replayApi . on ( 'end' , function ( replayContext ) { replayContext . instant . isEnd = true ; } ) ; } ) ; function * recorderPrepare ( ) { try { yield put ( { type : CommonActionTypes . AppSwitchToScreen , payload : { screen : Screen . Record } } ) ; const state = yield select ( ) ; const recorder = getRecorderState ( state ) ; let recorderContext = recorder . context ; if ( recorderContext ) { const { worker : oldWorker } = recorderContext ; if ( oldWorker ) { yield call ( oldWorker . kill ) ; } } yield put ( { type : ActionTypes . RecorderPreparing , payload : { progress : 'start' } } ) ; yield put ( { type : ActionTypes . MemoryUsageChanged , payload : { heapSize : 0 } } ) ; const stream = yield call ( getAudioStream ) ; yield put ( { type : ActionTypes . RecorderPreparing , payload : { progress : 'stream_ok' } } ) ; const audioContext = new AudioContext ( ) ; const source = audioContext . createMediaStreamSource ( stream ) ; const analyser = audioContext . createAnalyser ( ) ; const scriptProcessor = audioContext . createScriptProcessor ( 4096 , 2 , 2 ) ; source . connect ( analyser ) ; source . connect ( scriptProcessor ) ; analyser . smoothingTimeConstant = 0.8 ; analyser . fftSize = 1024 ; scriptProcessor . connect ( audioContext . destination ) ; yield call ( ( ) => audioContext . suspend ( ) ) ; yield put ( { type : ActionTypes . RecorderPreparing , payload : { progress : 'audio_ok' } } ) ; const worker = yield call ( spawnWorker , AudioWorker ) ; yield put ( { type : ActionTypes . RecorderPreparing , payload : { progress : 'worker_ok' , worker } } ) ; yield call ( worker . call , 'init' , { sampleRate : audioContext . sampleRate , numberOfChannels : source . channelCount } ) ; yield put ( { type : ActionTypes . RecorderPreparing , payload : { progress : 'worker_init_ok' , analyser } } ) ; scriptProcessor . onaudioprocess = function ( event ) { const ch0 = event . inputBuffer . getChannelData ( 0 ) ; const ch1 = event . inputBuffer . getChannelData ( 1 ) ; worker . post ( 'addSamples' , { samples : [ ch0 , ch1 ] } ) ; } ; recorderContext = { audioContext , worker , scriptProcessor } ; yield put ( { type : ActionTypes . RecorderReady , payload : { recorderContext } } ) ; } catch ( error ) { yield put ( { type : CommonActionTypes . Error , payload : { source : 'recorderPrepare' , error } } ) ; } } function * recorderStart ( ) { try { const state = yield select ( ) ; const recorder = getRecorderState ( state ) ; const recorderStatus = recorder . status ; if ( recorderStatus !== RecorderStatus . Ready ) { console . log ( 'not ready' , recorder ) ; return ; } if ( state . stepper && state . stepper . status !== StepperStatus . Clear ) { yield put ( { type : StepperActionTypes . StepperExit } ) ; } yield put ( { type : ActionTypes . RecorderStarting } ) ; yield call ( resumeAudioContext , recorder . context . audioContext ) ; yield put ( { type : ActionTypes . RecorderStarted } ) ; yield call ( deps . recordApi . start ) ; } catch ( error ) { yield put ( { type : CommonActionTypes . Error , payload : { source : 'recorderStart' , error } } ) ; } } function * recorderStop ( ) { try { let state = yield select ( ) ; let recorder = getRecorderState ( state ) ; let recorderStatus = recorder . status ; if ( ! /recording|paused/ . test ( recorderStatus ) ) { return ; } yield put ( { type : ActionTypes . RecorderStopping } ) ; const { audioContext } = recorder . context ; if ( recorderStatus === 'recording' ) { yield call ( suspendAudioContext , audioContext ) ; } yield put ( { type : ActionTypes . RecorderStopped , payload : { } } ) ; } catch ( error ) { yield put ( { type : CommonActionTypes . Error , payload : { source : 'recorderStop' , error } } ) ; } } function * recorderPause ( ) { try { let state = yield select ( ) ; let recorder = getRecorderState ( state ) ; if ( recorder . status !== RecorderStatus . Recording ) { return ; } yield put ( { type : ActionTypes . RecorderPausing } ) ; const { audioContext , worker } = recorder . context ; yield call ( suspendAudioContext , audioContext ) ; const { wav , duration } = yield call ( worker . call , 'export' , { wav : true } , pauseExportProgressSaga ) ; const audioUrl = URL . createObjectURL ( wav ) ; const endTime = Math . floor ( duration * 1000 ) ; yield put ( { type : ActionTypes . RecorderAddEvent , event : [ endTime , 'end' ] } ) ; const version = RECORDING_FORMAT_VERSION ; state = yield select ( ) ; const options = state . options ; recorder = getRecorderState ( state ) ; const data = { version , options , events : recorder . events , subtitles : [ ] } ; const eventsBlob = new Blob ( [ JSON . stringify ( data ) ] , { type : "application/json;charset=UTF-8" } ) ; const eventsUrl = URL . createObjectURL ( eventsBlob ) ; yield put ( { type : PlayerActionTypes . PlayerPrepare , payload : { audioUrl , eventsUrl , resetTo : 'end' } } ) ; yield take ( PlayerActionTypes . PlayerReady ) ; yield put ( { type : ActionTypes . RecorderPaused } ) ; } catch ( error ) { yield put ( { type : CommonActionTypes . Error , payload : { source : 'recorderPause' , error } } ) ; } function * pauseExportProgressSaga ( ) { } } function * recorderResume ( ) { try { const state = yield select ( ) ; const recorder = getRecorderState ( state ) ; const recorderStatus = recorder . status ; const player = getPlayerState ( state ) ; const isPlaying = player . isPlaying ; if ( recorderStatus !== RecorderStatus . Paused || isPlaying ) { console . log ( 'bad state' , recorderStatus ) ; return ; } yield put ( { type : PlayerActionTypes . PlayerPause , payload : { reset : false } } ) ; yield take ( PlayerActionTypes . PlayerPaused ) ; yield put ( { type : PlayerActionTypes . PlayerClear } ) ; yield put ( { type : ActionTypes . RecorderPopEvent } ) ; yield put ( { type : ActionTypes . RecorderResuming } ) ; yield call ( truncateRecording , player . audioTime , player . current ) ; yield call ( resumeAudioContext , recorder . context . audioContext ) ; yield put ( { type : ActionTypes . RecorderResumed } ) ; } catch ( error ) { yield put ( { type : CommonActionTypes . Error , payload : { source : 'recorderResume' , error } } ) ; } } function * truncateRecording ( audioTime , instant ) { const state = yield select ( ) ; const { worker } = getRecorderState ( state ) . context ; yield call ( worker . call , 'truncate' , { position : audioTime / 1000 } ) ; if ( instant ) { const position = instant . pos + 1 ; yield put ( { type : ActionTypes . RecorderTruncate , payload : { audioTime , position } } ) ; } } function * resumeAudioContext ( audioContext ) { const outcome = yield race ( { resumed : call ( ( ) => audioContext . resume ( ) ) , timeout : delay ( 1000 ) } ) ; if ( 'timeout' in outcome ) { throw new Error ( 'audio device is busy' ) ; } } function * suspendAudioContext ( audioContext ) { yield call ( ( ) => audioContext . suspend ( ) ) ; const audioTime = Math . round ( audioContext . currentTime * 1000 ) ; yield put ( { type : ActionTypes . AudioContextSuspended , payload : { audioTime } } ) ; } } ; function getAudioStream ( ) { const constraints = { audio : true } ; if ( typeof navigator . mediaDevices === 'object' && typeof navigator . mediaDevices . getUserMedia === 'function' ) { return navigator . mediaDevices . getUserMedia ( constraints ) ; } else { const getUserMedia = navigator . getUserMedia || navigator . webkitGetUserMedia || navigator . mozGetUserMedia || navigator . msGetUserMedia ; return new Promise ( function ( resolve , reject ) { getUserMedia . call ( navigator , constraints , resolve , reject ) ; } ) ; } }