555 lines
23 KiB
JavaScript
555 lines
23 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) {
|
|
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 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) {
|
|
console.log("recordModel.js: 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) {
|
|
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")
|
|
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) {
|
|
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")
|
|
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 handleRecordingAborted(recordingId, result) {
|
|
console.log("[RecordingState] 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 getCurrentRecordingState() {
|
|
console.log("[RecordingState] Getting current recording state", {
|
|
sessionId,
|
|
currentUserId: context.JK.currentUserId,
|
|
currentRecordingId,
|
|
currentlyRecording
|
|
});
|
|
|
|
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();
|
|
console.log("[RecordingState] Local client recording state", {
|
|
recordingId,
|
|
sessionId: session.id,
|
|
currentUserId: context.JK.currentUserId
|
|
});
|
|
|
|
var isRecording = recordingId && recordingId != "";
|
|
if (isRecording) {
|
|
try {
|
|
console.log("[RecordingState] Attempting to fetch server recording state", {
|
|
recordingId,
|
|
sessionId: session.id
|
|
});
|
|
recording = await rest.getRecordingPromise({ id: recordingId })
|
|
console.log("[RecordingState] Server recording state", {
|
|
recordingId,
|
|
ownerId: recording?.owner?.id,
|
|
currentUserId: context.JK.currentUserId,
|
|
recordingState: recording
|
|
});
|
|
} catch (error) {
|
|
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
|
|
});
|
|
}
|
|
}
|
|
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.getCurrentRecordingState = getCurrentRecordingState;
|
|
|
|
context.JK.HandleRecordingStartResult = handleRecordingStartResult;
|
|
context.JK.HandleRecordingStopResult = handleRecordingStopResult;
|
|
context.JK.HandleRecordingStopped = handleRecordingStopped;
|
|
context.JK.HandleRecordingStarted = handleRecordingStarted;
|
|
context.JK.HandleRecordingAborted = handleRecordingAborted;
|
|
|
|
|
|
};
|
|
|
|
})(window,jQuery); |