// The recording must be fed certain events, and as a simplification to consumers, it will emit state engine transition events. // This class automatically watches for server notifications relating to recordings (start/stop), as well as backend events // inputs: // * startRecording: user wants to start a recording // * stopRecording: user wants to stop recording // // events: // * startingRecording: a recording has been requested, but isn't confirmed started // * startedRecording: a recording is officially started // * stoppingRecording: a stop to the current recording has been requested, but it isn't confirmed yet // * stoppedRecording: a recording is not running // * (function(context,$) { "use strict"; context.JK = context.JK || {}; var logger = context.JK.logger; context.JK.RecordingModel = function(app, _rest, _jamClient) { var currentRecording = null; // the JSON response from the server for a recording var currentOrLastRecordingId = null; var currentRecordingId = null; var rest = _rest; var currentlyRecording = false; var startingRecording = false; var stoppingRecording = false; var waitingOnServerStop = false; var waitingOnClientStop = false; var waitingOnStopTimer = null; var jamClient = _jamClient; var sessionId = null; var $self = $(this); function getState() { return { waitingOnClientStop: waitingOnClientStop, waitingOnServerStop: waitingOnServerStop, stoppingRecording: stoppingRecording, startingRecording: startingRecording } } function isRecording (recordingId) { // if you specify recordingId, the check is more exact if(recordingId) { return recordingId == currentRecordingId; } else { // if you omit recordingId, then we'll just check if we are recording at all return currentlyRecording; } } /** called every time a session is joined, to ensure clean state */ function reset(_sessionId) { console.log("[RecordingState]: reset") currentlyRecording = false; waitingOnServerStop = false; waitingOnClientStop = false; if(waitingOnStopTimer != null) { clearTimeout(waitingOnStopTimer); waitingOnStopTimer = null; } currentRecording = null; currentRecordingId = null; stoppingRecording = false; sessionId = _sessionId } function groupTracksToClient(recording) { // group N tracks to the same client Id var groupedTracks = {}; var recordingTracks = recording["recorded_tracks"]; for (var i = 0; i < recordingTracks.length; i++) { var clientId = recordingTracks[i].client_id; var tracksForClient = groupedTracks[clientId]; if (!tracksForClient) { tracksForClient = []; groupedTracks[clientId] = tracksForClient; } tracksForClient.push(recordingTracks[i]); } return context.JK.dkeys(groupedTracks); } function startRecording(recordSettings) { const recordVideo = recordSettings.recordingType == context.JK.RECORD_TYPE_BOTH //const recordChat = recordSettings.includeChat //const recordFramerate = recordSettings.frameRate $self.triggerHandler('startingRecording', {}); currentlyRecording = true; stoppingRecording = false; context.RecordingActions.startingRecording({isRecording: false}) currentRecording = rest.startRecordingPromise({"music_session_id": sessionId, record_video: recordVideo}) currentRecording .then(async function(recording) { currentRecordingId = recording.id; currentOrLastRecordingId = recording.id; // ask the backend to start the session. var groupedTracks = groupTracksToClient(recording); //await jamClient.StartRecording(recording["id"], groupedTracks, recordVideo, recordChat, recordFramerate); console.log("jamClient#StartMediaRecording", recordSettings) await jamClient.StartMediaRecording(recording["id"], groupedTracks, recordSettings) }) .catch(function(jqXHR) { console.warn("failed to startRecording due to server issue:", jqXHR.responseJSON) var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false } $self.triggerHandler('startedRecording', details); currentlyRecording = false; context.RecordingActions.startedRecording(details); }) return true; } /** Nulls can be passed for all 3 currently; that's a user request. */ async function stopRecording(recordingId, reason, detail) { const userInitiated = recordingId == null && reason == null && detail == null const recording = await currentRecording //if(recording.owner.id !== context.JK.currentUserId){ // return; //} const isRecordingOwner = recording.owner.id == context.JK.currentUserId; console.log(`[RecordingState]: stopRecording userInitiated=${userInitiated} isRecordingOwner=${isRecordingOwner} reason=${reason} detail=${detail}`) if(stoppingRecording) { console.log("ignoring stopRecording because we are already stopping"); return; } stoppingRecording = true; waitingOnServerStop = waitingOnClientStop = true; waitingOnStopTimer = setTimeout(timeoutTransitionToStop, 5000); $self.triggerHandler('stoppingRecording', {reason: reason, detail: detail}); context.RecordingActions.stoppingRecording({reason: reason, detail: detail, isRecording:true}) // this path assumes that the currentRecording info has, or can be, retrieved // failure for currentRecording is handled elsewhere currentRecording .then(async function(recording) { var groupedTracks = groupTracksToClient(recording); //if(sessionModel.jamTracks() && isRecording()) { // console.log("preemptive stop media") //context.jamClient.SessionStopPlay(); //} //await jamClient.StopRecording(recording.id, groupedTracks); await jamClient.FrontStopRecording(recording.id, groupedTracks); if(isRecordingOwner) { rest.stopRecording({"id": recording.id}) .done(function () { waitingOnServerStop = false; attemptTransitionToStop(recording.id, reason, detail); stoppingRecording = false }) .fail(function (jqXHR) { stoppingRecording = false if (jqXHR.status == 422) { waitingOnServerStop = false; attemptTransitionToStop(recording.id, reason, detail); } else { logger.error("unable to stop recording %o", arguments); transitionToStopped(); var details = { 'recordingId': recording.id, 'reason': 'rest', 'details': arguments, isRecording: false } $self.triggerHandler('stoppedRecording', details); context.RecordingActions.stoppedRecording(details) stoppingRecording = false } }); } }); return true; } async function abortRecording(recordingId, errorReason, errorDetail) { await jamClient.AbortRecording(recordingId, {reason: errorReason, detail: errorDetail, success:false}); } function timeoutTransitionToStop() { // doh. couldn't stop waitingOnStopTimer = null; transitionToStopped(); $self.triggerHandler('stoppedRecordingFailed', { 'reason' : 'timeout' }); } // Only tell the user that we've stopped once both server and client agree we've stopped function attemptTransitionToStop(recordingId, errorReason, errorDetail) { if(!waitingOnClientStop && !waitingOnServerStop) { transitionToStopped(); var details = {recordingId: recordingId, reason: errorReason, detail: errorDetail, isRecording: false} $self.triggerHandler('stoppedRecording', details) context.RecordingActions.stoppedRecording(details) } } function transitionToStopped() { console.log("[RecordingState] transitionToStopped") currentlyRecording = false; currentRecording = null; currentRecordingId = null; if(waitingOnStopTimer) { clearTimeout(waitingOnStopTimer); waitingOnStopTimer = null; } } function onServerStartRecording() { //alert("onServerStartRecording") var session = context.SessionStore.getCurrentOrLastSession(); if (!session) { console.log("no session, so no recording"); return; } context.SessionStore.updateSessionInfo(session, true) } function onServerStopRecording(recordingId) { console.log("[RecordingState] onServerStopRecording") var session = context.SessionStore.getCurrentOrLastSession(); if (!session) { console.log("no session, so no recording"); return; } context.SessionStore.updateSessionInfo(session, true) getCurrentRecordingState().then(function (recordingState) { if (recordingState.isRecording && recordingState.recordingOwnerId === app.currentUserId) { // we are still recording, so don't transition to stopped console.log("recording is still running, so don't transition to stopped"); return; } stopRecording(recordingId, null, null); }); } function handleRecordingStartResult(recordingId, result) { console.log("[RecordingState] handleRecordingStartResult", { recordingId, result, currentRecordingId, currentlyRecording }); var success = result.success; var reason = result.reason; var detail = result.detail if(success) { var details = {clientId: app.clientId, isRecording:true} $self.triggerHandler('startedRecording', details) context.RecordingActions.startedRecording(details) } else { currentlyRecording = false; logger.error("unable to start the recording %o, %o", reason, detail); var details = { clientId: app.clientId, reason: reason, detail: detail, isRecording: false} $self.triggerHandler('startedRecording', details); context.RecordingActions.startedRecording(details) } } function handleRecordingStopResult(recordingId, result) { console.log("[RecordingState] handleRecordingStopResult", result) var success = result.success; var reason = result.reason; var detail = result.detail; waitingOnClientStop = false; if(success) { attemptTransitionToStop(recordingId, reason, detail); } else { transitionToStopped(); logger.error("backend unable to stop the recording %o, %o", reason, detail); var details = {recordingId: recordingId, reason: reason, detail : detail, isRecording: false} $self.triggerHandler('stoppedRecording', details); context.RecordingActions.stoppedRecording(details) } } function handleRecordingStarted(recordingId, result, clientId) { context.RecordingActions.resetRecordingState() console.log("[RecordingState] handleRecordingStarted called", { recordingId, result, clientId, currentRecordingId, currentlyRecording, sessionId }); var success = result.success; var reason = result.reason; var detail = result.detail; // in this scenario, we don't know all the tracks of the user. // we need to ask sessionModel to populate us with the recording data ASAP console.log("[RecordingState] Attempting to fetch recording data from server", { recordingId, sessionId, currentUserId: context.JK.currentUserId }); currentRecording = rest.getRecordingPromise({id: recordingId}) currentRecording .then(function(recording) { console.log("[RecordingState] Successfully fetched recording data from server", { recordingId: recording.id, ownerId: recording.owner?.id, currentUserId: context.JK.currentUserId, recordingState: recording }); currentRecordingId = recording.id; currentOrLastRecordingId = recording.id; }) .catch(function(error) { console.error("[RecordingState] Failed to fetch recording data", { recordingId, error, errorMessage: error.message, errorStatus: error.status, errorResponse: error.responseJSON, sessionId, currentUserId: context.JK.currentUserId, currentRecordingId, currentlyRecording }); }); var details = {recordingId: recordingId, isRecording: false} $self.triggerHandler('startingRecording', details); context.RecordingActions.startingRecording(details) currentlyRecording = true; details = {clientId: clientId, recordingId: recordingId, isRecording: true} $self.triggerHandler('startedRecording', details); context.RecordingActions.startedRecording(details) } function handleRecordingStopped(recordingId, result) { console.log("[RecordingState] handleRecordingStopped event_id=" + recordingId + " current_id=" + currentRecordingId, result) var session = context.SessionStore.getCurrentOrLastSession(); if(session) { context.SessionStore.updateSessionInfo(session, true) } // if(recordState.isRecording) { // //we are still recording, so don't stop // return; // } if(recordingId == "video") { // comes from VideoRecordingStopped return; } var success = result.success; var reason = result.reason; var detail = result.detail; var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: true } $self.triggerHandler('stoppingRecording', details); context.RecordingActions.stoppingRecording(details) // the backend says the recording must be stopped. // tell the server to stop it too if(recordingId == null || recordingId == "") { // this occurs when you are told by the backend to stop; i.e., you are a non-creator of the // recording. Stop, don't tell the server to stop; you aren't supposed to hve permission to var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false} context.RecordingActions.stoppedRecording(details) return; } rest.stopRecording({ id: recordingId }) .always(function() { transitionToStopped(); }) .fail(function(jqXHR, textStatus, errorMessage) { if(jqXHR.status == 422) { console.log("recording already stopped %o", arguments); var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false} $self.triggerHandler('stoppedRecording', details); context.RecordingActions.stoppedRecording(details) } else if(jqXHR.status == 404) { console.log("recording is already deleted %o", arguments); var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false} $self.triggerHandler('stoppedRecording', details); context.RecordingActions.stoppedRecording(details) } else { var details = {recordingId: recordingId, reason: textStatus, detail: errorMessage, isRecording: false} $self.triggerHandler('stoppedRecording', details); context.RecordingActions.stoppedRecording(details) } }) .done(function() { var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false} $self.triggerHandler('stoppedRecording', details); context.RecordingActions.stoppedRecording(details) }) } function handleRecordingAborted(recordingId, result) { console.log("[RecordingState] handleRecordingAborted") if(recordingId == "video") { // comes from AbortedVideoRecording recordingId = result; if (arguments.length == 2) { result = arguments[2] } console.log("video recording aborted", result) context.JK.Banner.showAlert("Video has stopped recording. Audio is still recording.") //context.RecordingActions.stopRecording() return; } var success = result.success; var reason = result.reason; var detail = result.detail; stoppingRecording = false; var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false } $self.triggerHandler('abortedRecording', details); context.RecordingActions.abortedRecording(details) // the backend says the recording must be stopped. // tell the server to stop it too rest.stopRecording({ id: recordingId }) .always(function() { currentlyRecording = false; }) } /** * If a stop is needed, it will be issued, and the deferred object will fire done() * If a stop is not needed (i.e., there is no recording), then the deferred object will fire immediately * @returns {$.Deferred} in all cases, only .done() is fired. */ function stopRecordingIfNeeded() { var deferred = new $.Deferred(); function resolved() { $self.off('stoppedRecording.stopRecordingIfNeeded', resolved); deferred.resolve(arguments); } if(!currentlyRecording) { deferred = new $.Deferred(); deferred.resolve(); } else { // wait for the next stoppedRecording event message $self.on('stoppedRecording.stopRecordingIfNeeded', resolved); if(!stopRecording()) { // no event is coming, so satisfy the deferred immediately $self.off('stoppedRecording.stopRecordingIfNeeded', resolved); deferred = new $.Deferred(); deferred.resolve(); } } return deferred; } /** * sync recording state with the client back end * Describe exactly when we should fetch recording state from the server * 1. If backend sends stopped/stopping/abort event * 2. If track changes occurs * 3. * TOOD: describe exactly when we should fetch recording state from the client * TODO: don't couple them into this one method * */ async function getCurrentRecordingState() { var recording = null; var session = context.SessionStore.getCurrentOrLastSession(); var recordingId = await context.jamClient.GetCurrentRecordingId(); var isRecording = recordingId && recordingId != ""; if (session && isRecording) { try { recording = await rest.getRecordingPromise({ id: recordingId }) } catch (error) { if(error.status != 404) { console.error("[RecordingState] Failed to fetch server recording state", { recordingId, error, errorMessage: error.message, errorStatus: error.status, errorResponse: error.responseJSON, sessionId: session.id, currentUserId: context.JK.currentUserId }); } } } if (!session) { console.debug("no session, so no recording"); return { isRecording: false, serverRecording: null, recordingOwnerId: null }; } return { isRecording: isRecording, isServerRecording: !!recording, recordingOwnerId: isRecording && recording ? recording.owner.id : null, } } this.initialize = function() { }; this.startRecording = startRecording; this.stopRecording = stopRecording; this.onServerStopRecording = onServerStopRecording; this.onServerStartRecording = onServerStartRecording; this.isRecording = isRecording; this.reset = reset; this.stopRecordingIfNeeded = stopRecordingIfNeeded; this.getState = getState; this.currentOrLastRecordingId = function () { return currentOrLastRecordingId; }; this.getCurrentRecordingState = getCurrentRecordingState; context.JK.HandleRecordingStartResult = handleRecordingStartResult; context.JK.HandleRecordingStopResult = handleRecordingStopResult; context.JK.HandleRecordingStopped = handleRecordingStopped; context.JK.HandleRecordingStarted = handleRecordingStarted; context.JK.HandleRecordingAborted = handleRecordingAborted; }; })(window,jQuery);