import { buffers , eventChannel } from 'redux-saga' ; import { call , put , race , select , take , takeLatest , spawn , delay } from 'redux-saga/effects' ; import { getJson } from '../common/utils' ; import { findInstantIndex } from './utils' ; import { ActionTypes } from "./actionTypes" ; import { ActionTypes as CommonActionTypes } from "../common/actionTypes" ; import { ActionTypes as StepperActionTypes } from "../stepper/actionTypes" ; import { ActionTypes as PlayerActionTypes } from "../player/actionTypes" ; import { getPlayerState } from "./selectors" ; import { AppStore , AppStoreReplay } from "../store" ; import { PlayerInstant } from "./index" ; import { Bundle } from "../linker" ; import { makeContext , QuickalgoLibraryCall , StepperContext } from "../stepper/api" ; import { App , Codecast } from "../index" ; import { ReplayApi } from "./replay" ; import { quickAlgoLibraries } from "../task/libs/quickalgo_librairies" ; import { ActionTypes as AppActionTypes } from "../actionTypes" ; import { taskLoad } from "../task" ; import { PrinterLibActionTypes } from "../task/libs/printer/printer_lib" ; import { RECORDING_FORMAT_VERSION } from "../version" ; import { getCurrentImmerState } from "../task/utils" ; import { createDraft , finishDraft } from "immer" ; export default function ( bundle ) { bundle . addSaga ( playerSaga ) ; } ; export interface ReplayContext { state : AppStoreReplay ; events : any [ ] ; recordingVersion ? : string ; instants : PlayerInstant [ ] ; instant : PlayerInstant ; applyEvent : any ; addSaga : Function ; addQuickAlgoLibraryCall : Function ; reportProgress : any ; stepperDone : any ; stepperContext : StepperContext ; } function * playerSaga ( action ) { yield takeLatest ( ActionTypes . PlayerPrepare , playerPrepare , action ) ; const anyReplayAction = [ ActionTypes . PlayerStart , ActionTypes . PlayerPause , ActionTypes . PlayerSeek ] ; yield takeLatest ( anyReplayAction , replaySaga , action ) ; yield takeLatest ( ActionTypes . PlayerApplyReplayEvent , playerReplayEvent , action ) ; } function * playerPrepare ( app , action ) { const { replayApi } = app ; const { baseDataUrl , audioUrl , resetTo } = action . payload ; const state = yield select ( ) ; const player = getPlayerState ( state ) ; if ( player . isPlaying ) { return ; } yield put ( { type : ActionTypes . PlayerPreparing } ) ; const audio = player . audio ; audio . src = audioUrl ; audio . load ( ) ; let data ; if ( action . payload . data ) { data = action . payload . data ; } else { data = yield call ( getJson , action . payload . eventsUrl ) ; } data = Object . freeze ( data ) ; if ( Array . isArray ( data ) ) { yield put ( { type : ActionTypes . PlayerPrepareFailure , payload : { message : "recording is incompatible with this player" } } ) ; return ; } const chan = yield call ( requestAnimationFrames , 50 ) ; let platform = 'unix' ; if ( data . options ) { platform = data . options . platform ; } if ( platform !== state . options . platform ) { yield put ( { type : CommonActionTypes . PlatformChanged , payload : platform } ) ; } const replayState = { task : { currentTask : state . task . currentTask , currentLevel : state . task . currentLevel , currentTestId : state . task . currentTestId , } , options : { platform } , } ; if ( data . options ) { replayState . options = data . options ; } console . log ( 'recording data' , data ) ; const replayContext = { state : replayState as AppStore , events : data . events , recordingVersion : data . version , instants : [ ] , instant : null , stepperContext : null , stepperDone : null , applyEvent : replayApi . applyEvent , addSaga , addQuickAlgoLibraryCall , reportProgress , } ; try { yield call ( computeInstants , replayApi , replayContext ) ; const instants = replayContext . instants ; const duration = instants [ instants . length - 1 ] . t ; yield put ( { type : ActionTypes . PlayerReady , payload : { baseDataUrl , duration , data , instants } } ) ; if ( 'end' === resetTo ) { yield call ( resetToAudioTime , app , duration ) ; } else { yield call ( resetToAudioTime , app , 0 ) ; } } catch ( ex ) { console . log ( ex ) ; yield put ( { type : ActionTypes . PlayerPrepareFailure , payload : { message : `${ ex . toString ( ) }` , context : replayContext } } ) ; return null ; } finally { chan . close ( ) ; } function addSaga ( saga ) { let { sagas } = replayContext . instant ; if ( ! sagas ) { sagas = replayContext . instant . sagas = [ ] ; } sagas . push ( saga ) ; } function addQuickAlgoLibraryCall ( quickalgoLibraryCall ) { let { quickalgoLibraryCalls } = replayContext . instant ; if ( ! quickalgoLibraryCalls ) { quickalgoLibraryCalls = replayContext . instant . quickalgoLibraryCalls = [ ] ; } quickalgoLibraryCalls . push ( quickalgoLibraryCall ) ; } function * reportProgress ( progress ) { yield put ( { type : ActionTypes . PlayerPrepareProgress , payload : { progress } } ) ; yield take ( chan ) ; } } function ensureBackwardsCompatibility ( events , version ? ) { let transformedEvents = [ ] ; let versionComponents = ( version ? version : RECORDING_FORMAT_VERSION ) . split ( '.' ) . map ( Number ) ; for ( let event of events ) { let [ t , key , ... params ] = event ; /**
         * For version < 6, the translate.success action, now renamed to compile.success used to contain :
         * {
         *   ast,
         *   diagnostics
         * }
         *
         * Now it should contain :
         * {
         *   response: {
         *     ast,
         *     diagnotics,
         *     platform: 'unix'
         *   }
         * }
         */ if ( key === 'translate.success' ) { params [ 0 ] = { response : { ... params [ 0 ] , platform : 'unix' } } ; } /**
         * Get the action name.
         * Note : translate.* actions have been replaced by compile.* actions from version 6.
         */ key = key . replace ( 'translate.' , 'compile.' ) ; if ( key === 'terminal.key' ) { key = PrinterLibActionTypes . PrinterLibTerminalInputKey ; } if ( key === 'terminal.backspace' ) { key = PrinterLibActionTypes . PrinterLibTerminalInputBackSpace ; } if ( key === 'terminal.enter' ) { key = PrinterLibActionTypes . PrinterLibTerminalInputEnter ; } if ( ( key === 'stepper.step' || key === 'stepper.progress' ) && versionComponents [ 0 ] < 7 ) { params . push ( 'immediate' ) ; } transformedEvents . push ( [ t , key , ... params ] ) ; if ( key === 'stepper.step' && params [ 0 ] === 'run' && versionComponents [ 0 ] < 7 ) { transformedEvents . push ( [ t , 'stepper.progress' ] ) ; } } return transformedEvents ; } function * computeInstants ( replayApi , replayContext ) { let pos , progress , lastProgress = 0 , range = null ; const recordingEvents = replayContext . events ; const events = ensureBackwardsCompatibility ( recordingEvents , replayContext . recordingVersion ) ; const duration = events [ events . length - 1 ] [ 0 ] ; const replayStore = Codecast . environments [ 'replay' ] . store ; yield call ( replayStore . dispatch , { type : AppActionTypes . AppInit , payload : { options : { ... replayContext . state . options } , environment : 'replay' } } ) ; yield call ( replayStore . dispatch , taskLoad ( ) ) ; for ( pos = 0 ; pos < events . length ; pos += 1 ) { const event = events [ pos ] ; const t = event [ 0 ] ; const key = event [ 1 ] ; const instant = { t , pos , event } as PlayerInstant ; replayContext . instant = instant ; console . log ( '-------- REPLAY ---- EVENT ----' , key , event ) ; yield new Promise ( resolve => { replayStore . dispatch ( { type : PlayerActionTypes . PlayerApplyReplayEvent , payload : { replayApi , key , replayContext , event , resolve } } ) ; } ) ; console . log ( 'END REPLAY EVENT (computeInstants)' ) ; const instantState = createDraft ( replayStore . getState ( ) ) ; const context = quickAlgoLibraries . getContext ( null , 'replay' ) ; instantState . task . state = getCurrentImmerState ( context . getInnerState ( ) ) ; instant . state = finishDraft ( instantState ) ; Object . freeze ( instant ) ; console . log ( 'new instant' , instant . state ) ; replayContext . instants . push ( instant ) ; progress = Math . round ( pos * 50 / events . length + t * 50 / duration ) / 100 ; if ( progress !== lastProgress ) { lastProgress = progress ; yield call ( replayContext . reportProgress , progress ) ; } } } /**
 * This redux saga has been dispatched in the replay store and occurs in this store
 */ function * playerReplayEvent ( app , { type , payload } ) { console . log ( 'START REPLAY EVENT (playerReplayEvent)' , type , payload ) ; const { replayApi , key , replayContext , event , resolve } = payload ; yield race ( { event : call ( replayApi . applyEvent , key , replayContext , event ) , inputNeeded : take ( 'task/taskInputNeeded' ) , } ) ; yield delay ( 0 ) ; console . log ( 'END REPLAY EVENT (playerReplayEvent)' ) ; resolve ( ) ; } function * replaySaga ( app , { type , payload } ) { const state = yield select ( ) ; const player = getPlayerState ( state ) ; const isPlaying = player . isPlaying ; const audio = player . audio ; const instants = player . instants ; let audioTime = player . audioTime ; let instant = player . current ; if ( type === ActionTypes . PlayerStart && ! player . isReady ) { return ; } if ( type === ActionTypes . PlayerStart ) { if ( instant . isEnd ) { audioTime = 0 ; audio . currentTime = 0 ; } yield call ( resetToAudioTime , app , audioTime ) ; yield put ( { type : StepperActionTypes . StepperDisabled } ) ; audio . play ( ) ; yield put ( { type : ActionTypes . PlayerStarted } ) ; } if ( type === ActionTypes . PlayerPause ) { audio . pause ( ) ; const audioTime = Math . round ( audio . currentTime * 1000 ) ; if ( ! payload || false !== payload . reset ) { yield call ( resetToAudioTime , app , audioTime ) ; } yield call ( restartStepper ) ; yield put ( { type : ActionTypes . PlayerPaused } ) ; return ; } if ( type === ActionTypes . PlayerSeek ) { let start = window . performance . now ( ) ; if ( ! isPlaying ) { yield put ( { type : StepperActionTypes . StepperDisabled } ) ; } const audioTime = Math . max ( 0 , Math . min ( player . duration , payload . audioTime ) ) ; yield call ( resetToAudioTime , app , audioTime ) ; if ( ! isPlaying ) { yield call ( restartStepper ) ; } audio . currentTime = audioTime / 1000 ; if ( ! isPlaying ) { return ; } } const chan = yield call ( requestAnimationFrames , 50 ) ; try { while ( ! ( yield select ( ( state ) => state . player . current . isEnd ) ) ) { let endTime = Math . round ( audio . currentTime * 1000 ) ; if ( audio . ended ) { endTime = instants [ instants . length - 1 ] . t ; } if ( endTime < audioTime || audioTime + 2000 < endTime ) { yield call ( resetToAudioTime , app , endTime ) ; } else { yield call ( replayToAudioTime , app , instants , audioTime , endTime ) ; } audioTime = endTime ; yield take ( chan ) ; } } finally { chan . close ( ) ; } yield put ( { type : ActionTypes . PlayerPause } ) ; } function * replayToAudioTime ( app , instants , startTime , endTime ) { let instantIndex = findInstantIndex ( instants , startTime ) ; const nextInstantIndex = findInstantIndex ( instants , endTime ) ; if ( instantIndex === nextInstantIndex ) { yield put ( { type : ActionTypes . PlayerTick , payload : { audioTime : endTime } } ) ; return ; } console . log ( 'replay, new instants' , instantIndex , nextInstantIndex ) ; instantIndex += 1 ; while ( instantIndex <= nextInstantIndex ) { console . log ( 'upgrade instant' ) ; let instant = instants [ instantIndex ] ; if ( instant . hasOwnProperty ( 'mute' ) ) { yield put ( { type : ActionTypes . PlayerEditorMutedChanged , payload : { isMuted : instant . mute } } ) ; } if ( instant . hasOwnProperty ( 'jump' ) ) { yield call ( jumpToAudioTime , app , instant . jump ) ; return ; } if ( instant . sagas ) { for ( let saga of instant . sagas ) { yield call ( saga , instant ) ; } } if ( instant . quickalgoLibraryCalls && instant . quickalgoLibraryCalls . length ) { console . log ( 'replay quickalgo library call' , instant . quickalgoLibraryCalls . map ( element => element . action ) . join ( ',' ) ) ; const context = quickAlgoLibraries . getContext ( null , 'main' ) ; const stepperState = instants [ instantIndex - 1 ] . state . stepper ; if ( context ) { const stepperContext = makeContext ( stepperState , { interactAfter : ( arg ) => { return new Promise ( ( resolve , reject ) => { app . dispatch ( { type : StepperActionTypes . StepperInteract , payload : { stepperContext , arg } , meta : { resolve , reject } } ) ; } ) ; } , dispatch : app . dispatch , environment : app . environment , } ) ; const executor = stepperContext . quickAlgoCallsExecutor ; for ( let quickalgoCall of instant . quickalgoLibraryCalls ) { const { module , action , args } = quickalgoCall ; console . log ( 'start call execution' , quickalgoCall ) ; yield spawn ( executor , module , action , args , ( ) => { console . log ( 'execution over' ) ; } ) ; } } } if ( instant . isEnd ) { endTime = instant . t ; break ; } instantIndex += 1 ; } yield call ( resetToAudioTime , app , endTime , true ) ; } function * resetToAudioTime ( app , audioTime , quick ? ) { const { replayApi } = app ; yield put ( { type : ActionTypes . PlayerTick , payload : { audioTime } } ) ; const state = yield select ( ) ; const instant = state . player . current ; yield call ( replayApi . reset , instant , quick ) ; } function * restartStepper ( ) { console . log ( 'restart stepper' ) ; yield put ( { type : StepperActionTypes . StepperEnabled } ) ; } function * jumpToAudioTime ( app , audioTime ) { const state = yield select ( ) ; const player = getPlayerState ( state ) ; audioTime = Math . max ( 0 , Math . min ( player . duration , audioTime ) ) ; const audio = player . audio ; audio . currentTime = audioTime / 1000 ; yield call ( resetToAudioTime , app , audioTime ) ; } function requestAnimationFrames ( maxDelta ) { let shutdown = false ; let lastTimestamp = 0 ; return eventChannel ( function ( emitter ) { function onAnimationFrame ( timestamp ) { if ( timestamp >= lastTimestamp + maxDelta ) { lastTimestamp = timestamp ; emitter ( timestamp ) ; } if ( ! shutdown ) { window . requestAnimationFrame ( onAnimationFrame ) ; } } window . requestAnimationFrame ( onAnimationFrame ) ; return function ( ) { shutdown = true ; } ; } , buffers . sliding ( 1 ) ) ; }