970 lines
38 KiB
JavaScript
970 lines
38 KiB
JavaScript
// The session model contains information about the music
|
|
// sessions that the current client has joined.
|
|
(function(context,$) {
|
|
|
|
"use strict";
|
|
|
|
context.JK = context.JK || {};
|
|
var logger = context.JK.logger;
|
|
|
|
// screen can be null
|
|
context.JK.SessionModel = function(app, server, client, sessionScreen) {
|
|
var ALERT_TYPES = context.JK.ALERT_TYPES;
|
|
var EVENTS = context.JK.EVENTS;
|
|
var MIX_MODES = context.JK.MIX_MODES;
|
|
var gearUtils = context.JK.GearUtilsInstance;
|
|
|
|
var userTracks = null; // comes from the backend
|
|
var clientId = client.clientID;
|
|
var currentSessionId = null; // Set on join, prior to setting currentSession.
|
|
var currentSession = null;
|
|
var currentOrLastSession = null;
|
|
var subscribers = {};
|
|
var users = {}; // User info for session participants
|
|
var rest = context.JK.Rest();
|
|
var requestingSessionRefresh = false;
|
|
var pendingSessionRefresh = false;
|
|
var recordingModel = new context.JK.RecordingModel(app, this, rest, context.jamClient);
|
|
var currentTrackChanges = 0;
|
|
var backendMixerAlertThrottleTimer = null;
|
|
// we track all the clientIDs of all the participants ever seen by this session, so that we can reliably convert a clientId from the backend into a username/avatar
|
|
var participantsEverSeen = {};
|
|
var currentParticipants = {};
|
|
var $self = $(this);
|
|
var sessionPageEnterDeferred = null;
|
|
var sessionPageEnterTimeout = null;
|
|
var startTime = null;
|
|
var joinDeferred = null;
|
|
var previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []};
|
|
var openBackingTrack = null;
|
|
var shownAudioMediaMixerHelp = false;
|
|
var controlsLockedForJamTrackRecording = false;
|
|
|
|
var mixerMode = MIX_MODES.PERSONAL;
|
|
|
|
server.registerOnSocketClosed(onWebsocketDisconnected);
|
|
|
|
function id() {
|
|
return currentSessionId;
|
|
}
|
|
|
|
function start(sessionId) {
|
|
currentSessionId = sessionId;
|
|
startTime = new Date().getTime();
|
|
}
|
|
|
|
function setUserTracks(_userTracks) {
|
|
userTracks = _userTracks;
|
|
}
|
|
|
|
function inSession() {
|
|
return !!currentSessionId;
|
|
}
|
|
|
|
function participants() {
|
|
if (currentSession) {
|
|
return currentSession.participants;
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// if any participant has the metronome open, then we say this session has the metronome open
|
|
function isMetronomeOpen() {
|
|
var metronomeOpen = false;
|
|
context._.each(participants(), function(participant) {
|
|
if(participant.metronome_open) {
|
|
metronomeOpen = true;
|
|
return false;
|
|
}
|
|
})
|
|
|
|
return metronomeOpen;
|
|
}
|
|
|
|
function isPlayingRecording() {
|
|
// this is the server's state; there is no guarantee that the local tracks
|
|
// requested from the backend will have corresponding track information
|
|
return !!(currentSession && currentSession.claimed_recording);
|
|
}
|
|
|
|
function recordedTracks() {
|
|
if(currentSession && currentSession.claimed_recording) {
|
|
return currentSession.claimed_recording.recording.recorded_tracks
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function recordedBackingTracks() {
|
|
if(currentSession && currentSession.claimed_recording) {
|
|
return currentSession.claimed_recording.recording.recorded_backing_tracks
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function backingTracks() {
|
|
var backingTracks = []
|
|
// this may be wrong if we loosen the idea that only one person can have a backing track open.
|
|
// but for now, the 1st person we find with a backing track open is all there is to find...
|
|
context._.each(participants(), function(participant) {
|
|
if(participant.backing_tracks.length > 0) {
|
|
backingTracks = participant.backing_tracks;
|
|
return false; // break
|
|
}
|
|
})
|
|
return backingTracks;
|
|
}
|
|
|
|
function jamTracks() {
|
|
if(currentSession && currentSession.jam_track) {
|
|
return currentSession.jam_track.tracks.filter(function(track) { return track.track_type == 'Track'})
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function recordedJamTracks() {
|
|
if(currentSession && currentSession.claimed_recording) {
|
|
return currentSession.claimed_recording.recording.recorded_jam_track_tracks
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function jamTrackName() {
|
|
if (currentSession && currentSession.jam_track) {
|
|
return currentSession.jam_track.name;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function recordedJamTrackName() {
|
|
if(currentSession && currentSession.claimed_recording && currentSession.claimed_recording.recording.jam_track) {
|
|
return currentSession.claimed_recording.recording.jam_track.name;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
// did I open up the current JamTrack?
|
|
function selfOpenedJamTracks() {
|
|
logger.debug("currentSession", currentSession)
|
|
return currentSession && (currentSession.jam_track_initiator_id == context.JK.currentUserId)
|
|
}
|
|
|
|
function backingTrack() {
|
|
if(currentSession) {
|
|
// TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668
|
|
return {
|
|
path: currentSession.backing_track_path
|
|
}
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function creatorId() {
|
|
if(!currentSession) {
|
|
throw "creator is not known"
|
|
}
|
|
return currentSession.user_id;
|
|
}
|
|
|
|
function alreadyInSession() {
|
|
var inSession = false;
|
|
$.each(participants(), function(index, participant) {
|
|
if(participant.user.id == context.JK.currentUserId) {
|
|
inSession = true;
|
|
return false;
|
|
}
|
|
});
|
|
return inSession;
|
|
}
|
|
|
|
function lockControlsforJamTrackRecording() {
|
|
controlsLockedForJamTrackRecording = true;
|
|
}
|
|
|
|
function unlockControlsforJamTrackRecording() {
|
|
controlsLockedForJamTrackRecording = false;
|
|
}
|
|
|
|
function areControlsLockedForJamTrackRecording() {
|
|
return controlsLockedForJamTrackRecording;
|
|
}
|
|
|
|
function onMixerModeChanged(newMixerMode)
|
|
{
|
|
mixerMode = newMixerMode;
|
|
var mode = newMixerMode == MIX_MODES.MASTER ? "master" : "personal";
|
|
logger.debug("onMixerModeChanged:" + mode);
|
|
$(document).triggerHandler(EVENTS.MIXER_MODE_CHANGED, {mode:mode});
|
|
}
|
|
|
|
async function waitForSessionPageEnterDone() {
|
|
sessionPageEnterDeferred = $.Deferred();
|
|
|
|
// see if we already have tracks; if so, we need to run with these
|
|
var inputTracks = await context.JK.TrackHelpers.getUserTracks(context.jamClient);
|
|
|
|
logger.debug("isNoInputProfile", gearUtils.isNoInputProfile())
|
|
if(inputTracks.length > 0 || gearUtils.isNoInputProfile() ) {
|
|
logger.debug("on page enter, tracks are already available")
|
|
sessionPageEnterDeferred.resolve(inputTracks);
|
|
var deferred = sessionPageEnterDeferred;
|
|
sessionPageEnterDeferred = null;
|
|
return deferred;
|
|
}
|
|
|
|
sessionPageEnterTimeout = setTimeout(function() {
|
|
if(sessionPageEnterTimeout) {
|
|
if(sessionPageEnterDeferred) {
|
|
sessionPageEnterDeferred.reject('timeout');
|
|
sessionPageEnterDeferred = null;
|
|
}
|
|
sessionPageEnterTimeout = null;
|
|
}
|
|
}, 5000);
|
|
|
|
return sessionPageEnterDeferred;
|
|
}
|
|
/**
|
|
* Join the session specified by the provided id.
|
|
*/
|
|
function joinSession(sessionId) {
|
|
logger.debug("SessionModel.joinSession(" + sessionId + ")");
|
|
joinDeferred = joinSessionRest(sessionId);
|
|
|
|
joinDeferred
|
|
.done(function(response){
|
|
|
|
if(!inSession()) {
|
|
// the user has left the session before they got joined. We need to issue a leave again to the server to make sure they are out
|
|
logger.debug("user left before fully joined to session. telling server again that they have left")
|
|
leaveSessionRest(response.id);
|
|
return;
|
|
}
|
|
|
|
logger.debug("calling jamClient.JoinSession");
|
|
// on temporary disconnect scenarios, a user may already be in a session when they enter this path
|
|
// so we avoid double counting
|
|
if(!alreadyInSession()) {
|
|
if(response.music_session.participant_count == 1) {
|
|
context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create);
|
|
}
|
|
else {
|
|
context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join);
|
|
}
|
|
}
|
|
|
|
recordingModel.reset();
|
|
client.JoinSession({ sessionID: sessionId });
|
|
refreshCurrentSession(true);
|
|
server.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, trackChanges);
|
|
server.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, trackChanges);
|
|
server.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, trackChanges);
|
|
server.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, trackChanges);
|
|
|
|
$(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId, lesson_session: session.lesson_session}});
|
|
})
|
|
.fail(function() {
|
|
updateCurrentSession(null);
|
|
});
|
|
|
|
return joinDeferred;
|
|
}
|
|
|
|
async function performLeaveSession(deferred) {
|
|
logger.debug("SessionModel.leaveCurrentSession()");
|
|
// TODO - sessionChanged will be called with currentSession = null\
|
|
server.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, trackChanges);
|
|
server.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, trackChanges);
|
|
server.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, trackChanges);
|
|
server.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, trackChanges);
|
|
|
|
//server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession);
|
|
// leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long
|
|
// time, for that entire duration you'll still be sending voice data to the other users.
|
|
// this may be bad if someone decides to badmouth others in the left-session during this time
|
|
//console.trace();
|
|
logger.debug("performLeaveSession: calling jamClient.LeaveSession for clientId=" + clientId);
|
|
await client.LeaveSession({ sessionID: currentSessionId });
|
|
leaveSessionRest(currentSessionId)
|
|
.done(function() {
|
|
sessionChanged();
|
|
deferred.resolve(arguments[0], arguments[1], arguments[2]);
|
|
})
|
|
.fail(function() {
|
|
deferred.reject(arguments[0], arguments[1], arguments[2]);
|
|
});
|
|
|
|
// 'unregister' for callbacks
|
|
await context.jamClient.SessionRegisterCallback("");
|
|
//context.jamClient.SessionSetAlertCallback("");
|
|
await context.jamClient.SessionSetConnectionStatusRefreshRate(0);
|
|
updateCurrentSession(null);
|
|
}
|
|
/**
|
|
* Leave the current session, if there is one.
|
|
* callback: called in all conditions; either after an attempt is made to tell the server that we are leaving,
|
|
* or immediately if there is no session
|
|
*/
|
|
function leaveCurrentSession() {
|
|
var deferred = new $.Deferred();
|
|
|
|
recordingModel.stopRecordingIfNeeded()
|
|
.always(function(){
|
|
performLeaveSession(deferred);
|
|
});
|
|
|
|
return deferred;
|
|
}
|
|
|
|
function trackChanges(header, payload) {
|
|
if(currentTrackChanges < payload.track_changes_counter) {
|
|
// we don't have the latest info. try and go get it
|
|
logger.debug("track_changes_counter = stale. refreshing...")
|
|
refreshCurrentSession();
|
|
}
|
|
else {
|
|
if(header.type != 'HEARTBEAT_ACK') {
|
|
// don't log if HEARTBEAT_ACK, or you will see this log all the time
|
|
logger.info("track_changes_counter = fresh. skipping refresh...", header, payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh the current session, and participants.
|
|
*/
|
|
function refreshCurrentSession(force) {
|
|
// XXX use backend instead: https://jamkazam.atlassian.net/browse/VRFS-854
|
|
//logger.debug("SessionModel.refreshCurrentSession(" + currentSessionId +")");
|
|
if(force) {
|
|
logger.debug("refreshCurrentSession(force=true)")
|
|
}
|
|
else {
|
|
|
|
}
|
|
refreshCurrentSessionRest(sessionChanged, force);
|
|
}
|
|
|
|
/**
|
|
* Subscribe for sessionChanged events. Provide a subscriberId
|
|
* and a callback to be invoked on session changes.
|
|
*/
|
|
function subscribe(subscriberId, sessionChangedCallback) {
|
|
logger.debug("SessionModel.subscribe(" + subscriberId + ", [callback])");
|
|
subscribers[subscriberId] = sessionChangedCallback;
|
|
}
|
|
|
|
/**
|
|
* Notify subscribers that the current session has changed.
|
|
*/
|
|
function sessionChanged() {
|
|
for (var subscriberId in subscribers) {
|
|
subscribers[subscriberId](currentSession);
|
|
}
|
|
}
|
|
|
|
// universal place to clean up, reset items
|
|
// fullyJoined means the user stayed in the screen until they had a successful rest.joinSession
|
|
// you might not get a fully joined if you join/leave really quickly before the REST API completes
|
|
function sessionEnded(fullyJoined) {
|
|
//cleanup
|
|
|
|
server.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, trackChanges);
|
|
server.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, trackChanges);
|
|
server.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, trackChanges);
|
|
server.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, trackChanges);
|
|
|
|
if(sessionPageEnterDeferred != null) {
|
|
sessionPageEnterDeferred.reject('session_over');
|
|
sessionPageEnterDeferred = null;
|
|
}
|
|
userTracks = null;
|
|
startTime = null;
|
|
joinDeferred = null;
|
|
mixerMode = MIX_MODES.PERSONAL;
|
|
if(fullyJoined) {
|
|
$(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionId}});
|
|
}
|
|
currentSessionId = null;
|
|
currentParticipants = {}
|
|
previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []}
|
|
openBackingTrack = null
|
|
shownAudioMediaMixerHelp = false
|
|
controlsLockedForJamTrackRecording = false;
|
|
}
|
|
|
|
// you should only update currentSession with this function
|
|
function updateCurrentSession(sessionData) {
|
|
//console.log("_DEBUG_* sessionModel#updateCurrentSession", sessionData)
|
|
if(sessionData != null) {
|
|
currentOrLastSession = sessionData;
|
|
}
|
|
|
|
var beforeUpdate = currentSession;
|
|
|
|
currentSession = sessionData;
|
|
|
|
// the 'beforeUpdate != null' makes sure we only do a clean up one time internally
|
|
if(sessionData == null) {
|
|
sessionEnded(beforeUpdate != null);
|
|
}
|
|
}
|
|
|
|
function updateSession(response) {
|
|
updateSessionInfo(response, null, true);
|
|
}
|
|
|
|
function updateSessionInfo(response, callback, force) {
|
|
//console.log("_DEBUG_* sessionModel#updateSessionInfo", response)
|
|
if(force === true || currentTrackChanges < response.track_changes_counter) {
|
|
logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter)
|
|
currentTrackChanges = response.track_changes_counter;
|
|
sendClientParticipantChanges(currentSession, response);
|
|
|
|
updateCurrentSession(response);
|
|
if(callback != null) {
|
|
callback();
|
|
}
|
|
}
|
|
else {
|
|
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reload the session data from the REST server, calling
|
|
* the provided callback when complete.
|
|
*/
|
|
function refreshCurrentSessionRest(callback, force) {
|
|
|
|
if(!inSession()) {
|
|
logger.debug("refreshCurrentSession skipped: ")
|
|
return;
|
|
}
|
|
|
|
var url = "/api/sessions/" + currentSessionId;
|
|
if(requestingSessionRefresh) {
|
|
// if someone asks for a refresh while one is going on, we ask for another to queue up
|
|
logger.debug("queueing refresh");
|
|
pendingSessionRefresh = true;
|
|
}
|
|
else {
|
|
requestingSessionRefresh = true;
|
|
$.ajax({
|
|
type: "GET",
|
|
url: url,
|
|
success: function(response) {
|
|
updateSessionInfo(response, callback, force);
|
|
},
|
|
error: function(jqXHR) {
|
|
if(jqXHR.status != 404) {
|
|
app.notifyServerError(jqXHR, "Unable to refresh session data")
|
|
}
|
|
else {
|
|
logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone")
|
|
}
|
|
},
|
|
complete: function() {
|
|
requestingSessionRefresh = false;
|
|
if(pendingSessionRefresh) {
|
|
// and when the request is done, if we have a pending, fire it off again
|
|
pendingSessionRefresh = false;
|
|
refreshCurrentSessionRest(sessionChanged, force);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Seems silly. We should just have the bridge take sessionId, clientId
|
|
*/
|
|
function _toJamClientParticipant(participant) {
|
|
return {
|
|
userID : "",
|
|
clientID : participant.client_id,
|
|
tcpPort : 0,
|
|
udpPort : 0,
|
|
localIPAddress : participant.ip_address, // ?
|
|
globalIPAddress : participant.ip_address, // ?
|
|
latency : 0,
|
|
natType : ""
|
|
};
|
|
}
|
|
|
|
async function ParticipantJoined(newSession, participant) {
|
|
await client.ParticipantJoined(newSession, _toJamClientParticipant(participant));
|
|
currentParticipants[participant.client_id] = {server:participant, client: {audio_established:null}}
|
|
}
|
|
|
|
async function ParticipantLeft(newSession, participant) {
|
|
await client.ParticipantLeft(newSession, _toJamClientParticipant(participant));
|
|
delete currentParticipants[participant.client_id]
|
|
}
|
|
|
|
function sendClientParticipantChanges(oldSession, newSession) {
|
|
var joins = [], leaves = [], leaveJoins = []; // Will hold JamClientParticipants
|
|
|
|
var oldParticipants = []; // will be set to session.participants if session
|
|
var oldParticipantIds = {};
|
|
var newParticipants = [];
|
|
var newParticipantIds = {};
|
|
|
|
if (oldSession && oldSession.participants) {
|
|
oldParticipants = oldSession.participants;
|
|
$.each(oldParticipants, function() {
|
|
oldParticipantIds[this.client_id] = this;
|
|
});
|
|
}
|
|
if (newSession && newSession.participants) {
|
|
newParticipants = newSession.participants;
|
|
$.each(newParticipants, function() {
|
|
newParticipantIds[this.client_id] = this;
|
|
});
|
|
}
|
|
|
|
$.each(newParticipantIds, function(client_id, participant) {
|
|
// grow the 'all participants seen' list
|
|
if(!(client_id in participantsEverSeen)) {
|
|
participantsEverSeen[client_id] = participant;
|
|
}
|
|
|
|
if(client_id in oldParticipantIds) {
|
|
// if the participant is here now, and here before, there is still a chance we missed a
|
|
// very fast leave/join. So check if joined_session_at is different
|
|
if(oldParticipantIds[client_id].joined_session_at != participant.joined_session_at) {
|
|
leaveJoins.push(participant)
|
|
}
|
|
}
|
|
else {
|
|
// new participant id that's not in old participant ids: Join
|
|
joins.push(participant);
|
|
}
|
|
});
|
|
$.each(oldParticipantIds, function(client_id, participant) {
|
|
if (!(client_id in newParticipantIds)) {
|
|
// old participant id that's not in new participant ids: Leave
|
|
leaves.push(participant);
|
|
}
|
|
});
|
|
|
|
$.each(joins, function(i,v) {
|
|
if (v.client_id != clientId) {
|
|
logger.debug("jamClient.ParticipantJoined", v.client_id)
|
|
ParticipantJoined(newSession, v)
|
|
}
|
|
});
|
|
$.each(leaves, function(i,v) {
|
|
if (v.client_id != clientId) {
|
|
logger.debug("jamClient.ParticipantLeft", v.client_id)
|
|
ParticipantLeft(newSession, v)
|
|
}
|
|
});
|
|
$.each(leaveJoins, function(i,v) {
|
|
if (v.client_id != clientId) {
|
|
logger.debug("participant had a rapid leave/join")
|
|
logger.debug("jamClient.ParticipantLeft", v.client_id)
|
|
ParticipantLeft(newSession, v)
|
|
|
|
logger.debug("jamClient.ParticipantJoined", v.client_id)
|
|
ParticipantJoined(newSession, v)
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function participantForClientId(clientId) {
|
|
var foundParticipant = null;
|
|
$.each(currentSession.participants, function(index, participant) {
|
|
if (participant.client_id === clientId) {
|
|
foundParticipant = participant;
|
|
return false;
|
|
}
|
|
});
|
|
return foundParticipant;
|
|
}
|
|
|
|
async function deleteTrack(sessionId, trackId) {
|
|
|
|
if (trackId) {
|
|
await client.TrackSetCount(1);
|
|
await client.TrackSaveAssignments();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make the server calls to join the current user to
|
|
* the session provided.
|
|
*/
|
|
function joinSessionRest(sessionId) {
|
|
var data = {
|
|
client_id: clientId,
|
|
ip_address: server.publicIP,
|
|
as_musician: true,
|
|
tracks: userTracks,
|
|
session_id: sessionId,
|
|
audio_latency: context.jamClient.FTUEGetExpectedLatency().latency
|
|
};
|
|
|
|
return rest.joinSession(data);
|
|
}
|
|
|
|
function leaveSessionRest(sessionId) {
|
|
var url = "/api/participants/" + clientId;
|
|
return $.ajax({
|
|
type: "DELETE",
|
|
url: url
|
|
});
|
|
}
|
|
|
|
function setMixerMode(newMixerMode) {
|
|
if(mixerMode != newMixerMode) {
|
|
onMixerModeChanged(newMixerMode);
|
|
}
|
|
}
|
|
|
|
function isMasterMixMode() {
|
|
return mixerMode == MIX_MODES.MASTER;
|
|
}
|
|
|
|
function isPersonalMixMode() {
|
|
return mixerMode == MIX_MODES.PERSONAL;
|
|
}
|
|
|
|
function getMixMode() {
|
|
return mixerMode;
|
|
}
|
|
|
|
function syncTracks(allTracks) {
|
|
// double check that we are in session, since a bunch could have happened since then
|
|
if(!inSession()) {
|
|
logger.debug("dropping queued up sync tracks because no longer in session");
|
|
return null;
|
|
}
|
|
|
|
if(allTracks === undefined) {
|
|
allTracks = context.JK.TrackHelpers.getTrackInfo(context.jamClient);
|
|
}
|
|
|
|
var inputTracks = allTracks.userTracks;
|
|
var backingTracks = allTracks.backingTracks;
|
|
var metronomeTracks = allTracks.metronomeTracks;
|
|
|
|
// create a trackSync request based on backend data
|
|
var syncTrackRequest = {};
|
|
syncTrackRequest.client_id = app.clientId;
|
|
syncTrackRequest.tracks = inputTracks;
|
|
syncTrackRequest.backing_tracks = backingTracks;
|
|
syncTrackRequest.metronome_open = metronomeTracks.length > 0;
|
|
syncTrackRequest.id = id();
|
|
|
|
return rest.putTrackSyncChange(syncTrackRequest)
|
|
.done(function() {
|
|
})
|
|
.fail(function(jqXHR) {
|
|
if(jqXHR.status != 404) {
|
|
app.notify({
|
|
"title": "Can't Sync Local Tracks",
|
|
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
|
|
"icon_url": "/assets/content/icon_alert_big.png"
|
|
});
|
|
}
|
|
else {
|
|
logger.debug("Unable to sync local tracks because session is gone.")
|
|
}
|
|
|
|
})
|
|
}
|
|
|
|
async function onWebsocketDisconnected(in_error) {
|
|
// kill the streaming of the session immediately
|
|
if(currentSessionId) {
|
|
logger.debug("onWebsocketDisconnect: calling jamClient.LeaveSession for clientId=" + clientId);
|
|
await client.LeaveSession({ sessionID: currentSessionId });
|
|
}
|
|
}
|
|
|
|
// returns a deferred object
|
|
function findUserBy(finder) {
|
|
if(finder.clientId) {
|
|
var foundParticipant = null;
|
|
$.each(participants(), function(index, participant) {
|
|
if(participant.client_id == finder.clientId) {
|
|
foundParticipant = participant;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if(foundParticipant) {
|
|
return $.Deferred().resolve(foundParticipant.user).promise();
|
|
}
|
|
}
|
|
|
|
// TODO: find it via some REST API if not found?
|
|
return $.Deferred().reject().promise();
|
|
}
|
|
|
|
function onDeadUserRemove(type, text) {
|
|
if(!inSession()) return;
|
|
var clientId = text;
|
|
var participant = participantsEverSeen[clientId];
|
|
if(participant) {
|
|
app.notify({
|
|
"title": ALERT_TYPES[type].title,
|
|
"text": participant.user.name + " is no longer sending audio.",
|
|
"icon_url": context.JK.resolveAvatarUrl(participant.user.photo_url)
|
|
});
|
|
var $track = $('div.track[client-id="' + clientId + '"]');
|
|
$('.disabled-track-overlay', $track).show();
|
|
}
|
|
}
|
|
|
|
function onWindowBackgrounded(type, text) {
|
|
app.user()
|
|
.done(function(userProfile) {
|
|
if(userProfile.show_whats_next &&
|
|
window.location.pathname.indexOf(gon.client_path) == 0 &&
|
|
!app.layout.isDialogShowing('getting-started')) {
|
|
app.layout.showDialog('getting-started');
|
|
}
|
|
})
|
|
|
|
if(!inSession()) return;
|
|
|
|
// the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen
|
|
if(sessionScreen) {
|
|
sessionScreen.setPromptLeave(false);
|
|
context.location = '/client#/home'
|
|
}
|
|
}
|
|
|
|
function onBroadcastSuccess(type, text) {
|
|
logger.debug("SESSION_LIVEBROADCAST_ACTIVE alert. reason:" + text);
|
|
|
|
if(currentSession && currentSession.mount) {
|
|
rest.createSourceChange({
|
|
mount_id: currentSession.mount.id,
|
|
source_direction: true,
|
|
success: true,
|
|
reason: text,
|
|
client_id: app.clientId
|
|
})
|
|
}
|
|
else {
|
|
logger.debug("unable to report source change because no mount seen on session")
|
|
}
|
|
}
|
|
|
|
function onBroadcastFailure(type, text) {
|
|
logger.debug("SESSION_LIVEBROADCAST_FAIL alert. reason:" + text);
|
|
|
|
if(currentSession && currentSession.mount) {
|
|
rest.createSourceChange({
|
|
mount_id: currentSession.mount.id,
|
|
source_direction: true,
|
|
success: false,
|
|
reason: text,
|
|
client_id: app.clientId
|
|
})
|
|
}
|
|
else {
|
|
logger.debug("unable to report source change because no mount seen on session")
|
|
}
|
|
}
|
|
|
|
function onBroadcastStopped(type, text) {
|
|
logger.debug("SESSION_LIVEBROADCAST_STOPPED alert. reason:" + text);
|
|
|
|
if(currentSession && currentSession.mount) {
|
|
rest.createSourceChange({
|
|
mount_id: currentSession.mount.id,
|
|
source_direction: false,
|
|
success: true,
|
|
reason: text,
|
|
client_id: app.clientId
|
|
})
|
|
}
|
|
else {
|
|
logger.debug("unable to report source change because no mount seen on session")
|
|
}
|
|
}
|
|
|
|
function onPlaybackStateChange(type, text) {
|
|
|
|
// if text is play_start or play_stop, tell the play_controls
|
|
|
|
if(sessionScreen) {
|
|
sessionScreen.onPlaybackStateChange(text);
|
|
}
|
|
}
|
|
async function onBackendMixerChanged(type, text) {
|
|
logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);
|
|
|
|
if(inSession() && text == "RebuildAudioIoControl") {
|
|
|
|
// the backend will send these events rapid-fire back to back.
|
|
// the server can still perform correctly, but it is nicer to wait 100 ms to let them all fall through
|
|
if(backendMixerAlertThrottleTimer) {clearTimeout(backendMixerAlertThrottleTimer);}
|
|
|
|
backendMixerAlertThrottleTimer = setTimeout(async function() {
|
|
|
|
if(sessionPageEnterDeferred) {
|
|
// this means we are still waiting for the BACKEND_MIXER_CHANGE that indicates we have user tracks built-out/ready
|
|
|
|
// we will get at least one BACKEND_MIXER_CHANGE that corresponds to the backend doing a 'audio pause', which won't matter much
|
|
// so we need to check that we actaully have userTracks before considering ourselves done
|
|
var inputTracks = await context.JK.TrackHelpers.getUserTracks(context.jamClient);
|
|
if(inputTracks.length > 0) {
|
|
logger.debug("obtained tracks at start of session")
|
|
sessionPageEnterDeferred.resolve(inputTracks);
|
|
sessionPageEnterDeferred = null;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// wait until we are fully in session before trying to sync tracks to server
|
|
if(joinDeferred) {
|
|
joinDeferred.done(function() {
|
|
syncTracks();
|
|
})
|
|
}
|
|
|
|
}, 100);
|
|
}
|
|
else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {
|
|
|
|
var allTracks = await context.JK.TrackHelpers.getTrackInfo(context.jamClient);
|
|
var backingTracks = allTracks.backingTracks;
|
|
var previousBackingTracks = previousAllTracks.backingTracks;
|
|
var metronomeTracks = allTracks.metronomeTracks;
|
|
var previousMetronomeTracks = previousAllTracks.metronomeTracks;
|
|
|
|
// the way we know if backing tracks changes, or recordings are opened, is via this event.
|
|
// but we want to report to the user when backing tracks change; so we need to detect change on our own
|
|
if(!(previousBackingTracks.length == 0 && backingTracks.length == 0) && previousBackingTracks != backingTracks) {
|
|
logger.debug("backing tracks changed", previousBackingTracks, backingTracks)
|
|
syncTracks(allTracks);
|
|
}
|
|
else if(!(previousMetronomeTracks.length == 0 && metronomeTracks.length == 0) && previousMetronomeTracks != metronomeTracks) {
|
|
logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks)
|
|
syncTracks(allTracks);
|
|
}
|
|
else {
|
|
refreshCurrentSession(true);
|
|
}
|
|
|
|
previousAllTracks = allTracks;
|
|
}
|
|
else if(inSession() && (text == 'Global Peer Input Mixer Mode')) {
|
|
setMixerMode(MIX_MODES.MASTER);
|
|
}
|
|
else if(inSession() && (text == 'Local Peer Stream Mixer Mode')) {
|
|
setMixerMode(MIX_MODES.PERSONAL);
|
|
}
|
|
}
|
|
|
|
// Public interface
|
|
this.id = id;
|
|
this.start = start;
|
|
this.backingTrack = backingTrack;
|
|
this.backingTracks = backingTracks;
|
|
this.recordedBackingTracks = recordedBackingTracks;
|
|
this.recordedJamTracks = recordedJamTracks;
|
|
this.setUserTracks = setUserTracks;
|
|
this.recordedTracks = recordedTracks;
|
|
this.jamTrackName = jamTrackName;
|
|
this.recordedJamTrackName = recordedJamTrackName;
|
|
this.jamTracks = jamTracks;
|
|
this.participants = participants;
|
|
this.joinSession = joinSession;
|
|
this.leaveCurrentSession = leaveCurrentSession;
|
|
this.refreshCurrentSession = refreshCurrentSession;
|
|
this.updateSession = updateSession;
|
|
this.subscribe = subscribe;
|
|
this.participantForClientId = participantForClientId;
|
|
this.isPlayingRecording = isPlayingRecording;
|
|
this.deleteTrack = deleteTrack;
|
|
this.onWebsocketDisconnected = onWebsocketDisconnected;
|
|
this.recordingModel = recordingModel;
|
|
this.findUserBy = findUserBy;
|
|
this.inSession = inSession;
|
|
this.setMixerMode = setMixerMode;
|
|
this.isMasterMixMode = isMasterMixMode;
|
|
this.isPersonalMixMode = isPersonalMixMode;
|
|
this.getMixMode = getMixMode;
|
|
this.selfOpenedJamTracks = selfOpenedJamTracks;
|
|
this.isMetronomeOpen = isMetronomeOpen;
|
|
this.areControlsLockedForJamTrackRecording = areControlsLockedForJamTrackRecording;
|
|
this.lockControlsforJamTrackRecording = lockControlsforJamTrackRecording;
|
|
this.unlockControlsforJamTrackRecording = unlockControlsforJamTrackRecording;
|
|
|
|
// ALERT HANDLERS
|
|
this.onBackendMixerChanged = onBackendMixerChanged;
|
|
this.onDeadUserRemove = onDeadUserRemove;
|
|
this.onWindowBackgrounded = onWindowBackgrounded;
|
|
this.waitForSessionPageEnterDone = waitForSessionPageEnterDone;
|
|
this.onBroadcastStopped = onBroadcastStopped;
|
|
this.onBroadcastSuccess = onBroadcastSuccess;
|
|
this.onBroadcastFailure = onBroadcastFailure;
|
|
this.onPlaybackStateChange = onPlaybackStateChange;
|
|
|
|
this.getCurrentSession = function() {
|
|
return currentSession;
|
|
};
|
|
this.getCurrentOrLastSession = function() {
|
|
return currentOrLastSession;
|
|
};
|
|
this.getParticipant = function(clientId) {
|
|
return participantsEverSeen[clientId]
|
|
};
|
|
this.setBackingTrack = function(backingTrack) {
|
|
openBackingTrack = backingTrack;
|
|
};
|
|
this.getBackingTrack = function() {
|
|
return openBackingTrack;
|
|
};
|
|
this.hasShownAudioMediaMixerHelp = function() {
|
|
return shownAudioMediaMixerHelp;
|
|
}
|
|
this.markShownAudioMediaMixerHelp = function() {
|
|
shownAudioMediaMixerHelp = true;
|
|
}
|
|
|
|
|
|
// call to report if the current user was able to establish audio with the specified clientID
|
|
this.setAudioEstablished = function(clientId, audioEstablished) {
|
|
var participant = currentParticipants[clientId]
|
|
if(participant) {
|
|
if(participant.client.audio_established === null) {
|
|
participant.client.audio_established = audioEstablished;
|
|
context.stats.write('client.audio_established.' + (audioEstablished ? 'success': 'failure'), {user_id: context.JK.currentUserId, name: context.JK.currentUserName, remote_user_id: participant.server.user.id, remote_name: participant.server.user.name, value: 1})
|
|
}
|
|
else if(participant.client.audio_established === false && audioEstablished === true) {
|
|
// this means the frontend had declared this audio failed, but later says it works.
|
|
// this could mean our threshold of time to wait before audio is considered failed is too low, and needs tweaking
|
|
context.stats.write('client.audio_established.delayed', {user_id: context.JK.currentUserId, name: context.JK.currentUserName, remote_user_id: participant.server.user.id, remote_name: participant.server.user.name, value: 1})
|
|
}
|
|
}
|
|
}
|
|
|
|
this.ensureEnded = function() {
|
|
updateCurrentSession(null);
|
|
}
|
|
|
|
};
|
|
|
|
})(window,jQuery); |