jam-cloud/web/app/assets/javascripts/recordingModel.js

601 lines
26 KiB
JavaScript

// 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 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) {
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) {
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 recording = await currentRecording
if(recording.owner.id !== context.JK.currentUserId){
return;
}
if(stoppingRecording) {
logger.debug("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()) {
// logger.debug("preemptive stop media")
//context.jamClient.SessionStopPlay();
//}
//await jamClient.StopRecording(recording.id, groupedTracks);
await jamClient.FrontStopRecording(recording.id, groupedTracks);
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() {
currentlyRecording = false;
currentRecording = null;
currentRecordingId = null;
if(waitingOnStopTimer) {
clearTimeout(waitingOnStopTimer);
waitingOnStopTimer = null;
}
}
function onServerStartRecording() {
//alert("onServerStartRecording")
var session = context.SessionStore.getCurrentOrLastSession();
if (!session) {
logger.debug("no session, so no recording");
return;
}
context.SessionStore.updateSessionInfo(session, true)
}
function onServerStopRecording(recordingId) {
//alert("onServerStopRecording")
var session = context.SessionStore.getCurrentOrLastSession();
if (!session) {
logger.debug("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
logger.debug("recording is still running, so don't transition to stopped");
return;
}
stopRecording(recordingId, null, null);
});
}
function handleRecordingStartResult(recordingId, result) {
//alert("handleRecordingStartResult")
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) {
//alert("handleRecordingStopResult")
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) {
//alert("handleRecordingStarted")
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
currentRecording = rest.getRecordingPromise({id: recordingId})
currentRecording
.then(function(recording) {
currentRecordingId = recording.id;
currentOrLastRecordingId = recording.id;
})
.catch(function() {
//alert("handleRecordingStarted: recording not found " + recordingId)
//TODO: commenting the following line to temporary fix for recording not found
//abortRecording(recordingId, 'populate-recording-info', app.clientId);
});
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) {
//alert("handleRecordingStopped")
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;
}
logger.debug("handleRecordingStopped " + recordingId, result)
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
rest.stopRecording({
id: recordingId
})
.always(function() {
transitionToStopped();
})
.fail(function(jqXHR, textStatus, errorMessage) {
if(jqXHR.status == 422) {
logger.debug("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) {
logger.debug("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 handleRecordingStarted(recordingId, result, clientId) {
// 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
// currentRecording = rest.getRecordingPromise({ id: recordingId })
// currentRecording
// .then(function (recording) {
// currentRecordingId = recording.id;
// currentOrLastRecordingId = recording.id;
// })
// .catch(function () {
// abortRecording(recordingId, 'populate-recording-info', app.clientId);
// });
// 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) {
// var session = context.SessionStore.getCurrentOrLastSession();
// if (!session) {
// logger.debug("no session, so no recording");
// return;
// }
// context.SessionStore.updateSessionInfo(session, true)
// getCurrentRecordingState().then(function (recordingState) {
// if (recordingState.isRecording) {
// // we are still recording, so don't transition to stopped
// logger.debug("recording is still running, so don't transition to stopped");
// return;
// }
// if (recordingId == "video") {
// // comes from VideoRecordingStopped
// return;
// }
// logger.debug("handleRecordingStopped " + recordingId, result)
// 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
// rest.stopRecording({
// id: recordingId
// })
// .always(function () {
// transitionToStopped();
// })
// .fail(function (jqXHR, textStatus, errorMessage) {
// if (jqXHR.status == 422) {
// logger.debug("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) {
// logger.debug("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)
// })
// }).catch(function (error) {
// console.error("Error reading recording state:", error);
// });
// }
function handleRecordingAborted(recordingId, result) {
//alert("handleRecordingAborted")
if(recordingId == "video") {
// comes from AbortedVideoRecording
recordingId = result;
if (arguments.length == 2) {
result = arguments[2]
}
logger.debug("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
*/
// async function getRecordingState() {
// var isRecording = false;
// var recordingClientId = null;
// var currentRecId = await jamClient.GetCurrentRecordingId(); //if this passes recording Id it means there is an ongoing recording
// //var currentRecId = currentRecordingId
// console.log("_DEBUG_* getRecordingState", currentRecId)
// if(currentRecId && currentRecId !== '') {
// var recording = await rest.getRecordingPromise({id: currentRecId})
// if(recording) {
// isRecording = true;
// recordingClientId = recording.client_id;
// }
// } else {
// isRecording = false;
// recordingClientId = null;
// }
// return { isRecording: isRecording, recordingClientId: recordingClientId };
// }
async function getCurrentRecordingState() {
var recording = null;
var session = context.SessionStore.getCurrentOrLastSession();
if (!session) {
logger.debug("no session, so no recording");
return { isRecording: false, recordingOwnerId: null };
}
var recordingId = await context.jamClient.GetCurrentRecordingId();
var isRecording = recordingId && recordingId != "";
if (isRecording) {
recording = await rest.getRecordingPromise({ id: recordingId })
}
return {
isRecording: isRecording,
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.currentOrLastRecordingId = function () { return currentOrLastRecordingId; };
//this.getRecordingState = getRecordingState;
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);