// 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; context.JK.SessionModel = function(app, server, client) { 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); function id() { return currentSession ? currentSession.id : null; } function participants() { if (currentSession) { return currentSession.participants; } else { return []; } } 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 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; } /** * Join the session specified by the provided id. */ function joinSession(sessionId) { currentSessionId = sessionId; logger.debug("SessionModel.joinSession(" + sessionId + ")"); var deferred = joinSessionRest(sessionId); deferred .done(function(){ logger.debug("calling jamClient.JoinSession"); if(!alreadyInSession()) { // on temporary disconnect scenarios, a user may already be in a session when they enter this path // so we avoid double counting context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join); } recordingModel.reset(); client.JoinSession({ sessionID: sessionId }); refreshCurrentSession(); server.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, refreshCurrentSession); server.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, refreshCurrentSession); }) .fail(function() { currentSessionId = null; }); return deferred; } function performLeaveSession(deferred) { logger.debug("SessionModel.leaveCurrentSession()"); // TODO - sessionChanged will be called with currentSession = null server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession); server.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, refreshCurrentSession); server.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, refreshCurrentSession); //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 logger.debug("calling jamClient.LeaveSession for clientId=" + clientId); console.time('jamClient.LeaveSession'); client.LeaveSession({ sessionID: currentSessionId }); console.timeEnd('jamClient.LeaveSession'); leaveSessionRest(currentSessionId) .done(function() { sessionChanged(); deferred.resolve(arguments); }) .fail(function() { deferred.reject(arguments); }); // 'unregister' for callbacks context.jamClient.SessionRegisterCallback(""); context.jamClient.SessionSetAlertCallback(""); context.jamClient.SessionSetConnectionStatusRefreshRate(0); updateCurrentSession(null); currentSessionId = 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; } /** * Refresh the current session, and participants. */ function refreshCurrentSession() { // XXX use backend instead: https://jamkazam.atlassian.net/browse/VRFS-854 logger.debug("SessionModel.refreshCurrentSession(" + currentSessionId +")"); refreshCurrentSessionRest(sessionChanged); } /** * 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() { logger.debug("SessionModel.sessionChanged()"); for (var subscriberId in subscribers) { subscribers[subscriberId](currentSession); } } // you should only update currentSession with this function function updateCurrentSession(sessionData) { if(sessionData != null) { currentOrLastSession = sessionData; } currentSession = sessionData; } /** * Reload the session data from the REST server, calling * the provided callback when complete. */ function refreshCurrentSessionRest(callback) { 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 pendingSessionRefresh = true; } else { requestingSessionRefresh = true; $.ajax({ type: "GET", url: url, success: function(response) { sendClientParticipantChanges(currentSession, response); updateCurrentSession(response); if(callback != null) { callback(); } }, error: function(jqXHR) { app.notifyServerError(jqXHR, "Unable to refresh session data") }, complete: function() { requestingSessionRefresh = false; if(pendingSessionRefresh) { // and when the request is done, if we have a pending, fire t off again pendingSessionRefresh = false; refreshCurrentSessionRest(null); } } }); } } /** * 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 : "" }; } function sendClientParticipantChanges(oldSession, newSession) { var joins = [], leaves = []; // 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.push(this.client_id); }); } if (newSession && newSession.participants) { newParticipants = newSession.participants; $.each(newParticipants, function() { newParticipantIds.push(this.client_id); }); } $.each(newParticipantIds, function(i,v) { if ($.inArray(v, oldParticipantIds) === -1) { // new participant id that's not in old participant ids: Join joins.push(_toJamClientParticipant(newParticipants[i])); } }); $.each(oldParticipantIds, function(i,v) { if ($.inArray(v, newParticipantIds) === -1) { // old participant id that's not in new participant ids: Leave leaves.push(_toJamClientParticipant(oldParticipants[i])); } }); $.each(joins, function(i,v) { if (v.client_id != clientId) { client.ParticipantJoined(newSession, v); } }); $.each(leaves, function(i,v) { if (v.client_id != clientId) { client.ParticipantLeft(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; } function addTrack(sessionId, data) { logger.debug("updating tracks on the server %o", data); var url = "/api/sessions/" + sessionId + "/tracks"; $.ajax({ type: "POST", dataType: "json", contentType: 'application/json', url: url, async: false, data: JSON.stringify(data), processData:false, success: function(response) { // save to the backend logger.debug("successfully updated tracks on the server"); //refreshCurrentSession(); }, error: function(jqXHR) { app.notifyServerError(jqXHR, "Unable to refresh session data") } }); } function updateTrack(sessionId, trackId, data) { var url = "/api/sessions/" + sessionId + "/tracks/" + trackId; $.ajax({ type: "POST", dataType: "json", contentType: 'application/json', url: url, async: false, data: JSON.stringify(data), processData:false, success: function(response) { logger.debug("Successfully updated track info (" + JSON.stringify(data) + ")"); }, error: function(jqXHR) { app.notifyServerError(jqXHR, "Unable to refresh session data") } }); } function deleteTrack(sessionId, trackId) { if (trackId) { client.TrackSetCount(1); client.TrackSaveAssignments(); /** $.ajax({ type: "DELETE", url: "/api/sessions/" + sessionId + "/tracks/" + trackId, async: false, success: function(response) { // TODO: if in recording, more cleanup to do??? // update backend client (for now, only the second track can be removed) client.TrackSetCount(1); client.TrackSaveAssignments(); // refresh Session screen refreshCurrentSession(); }, error: function(jqXHR, textStatus, errorThrown) { logger.error("Error deleting track " + trackId); } }); */ } } /** * Make the server calls to join the current user to * the session provided. */ function joinSessionRest(sessionId) { var tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); var data = { client_id: clientId, ip_address: server.publicIP, as_musician: true, tracks: tracks }; var url = "/api/sessions/" + sessionId + "/participants"; return $.ajax({ type: "POST", dataType: "json", contentType: 'application/json', url: url, data: JSON.stringify(data), processData:false }); } function leaveSessionRest(sessionId) { var url = "/api/participants/" + clientId; return $.ajax({ type: "DELETE", url: url }); } function reconnect() { context.JK.CurrentSessionModel.leaveCurrentSession() .always(function() { window.location.reload(); }); } function registerReconnect(content) { $('a.disconnected-reconnect', content).click(function() { var template = $('#template-reconnecting').html(); var templateHtml = context.JK.fillTemplate(template, null); var content = context.JK.Banner.show({ html : template }); rest.serverHealthCheck() .done(function() { reconnect(); }) .fail(function(xhr, textStatus, errorThrown) { if(xhr && xhr.status >= 100) { // we could connect to the server, and it's alive reconnect(); } else { var template = $('#template-could-not-reconnect').html(); var templateHtml = context.JK.fillTemplate(template, null); var content = context.JK.Banner.show({ html : template }); registerReconnect(content); } }); return false; }); } function onWebsocketDisconnected(in_error) { if(app.clientUpdating) { // we don't want to do a 'cover the whole screen' dialog // because the client update is already showing. return; } // kill the streaming of the session immediately logger.debug("calling jamClient.LeaveSession for clientId=" + clientId); client.LeaveSession({ sessionID: currentSessionId }); if(in_error) { var template = $('#template-disconnected').html(); var templateHtml = context.JK.fillTemplate(template, null); var content = context.JK.Banner.show({ html : template }) ; registerReconnect(content); } } // 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(); } // Public interface this.id = id; this.recordedTracks = recordedTracks; this.participants = participants; this.joinSession = joinSession; this.leaveCurrentSession = leaveCurrentSession; this.refreshCurrentSession = refreshCurrentSession; this.subscribe = subscribe; this.participantForClientId = participantForClientId; this.isPlayingRecording = isPlayingRecording; this.addTrack = addTrack; this.updateTrack = updateTrack; this.deleteTrack = deleteTrack; this.onWebsocketDisconnected = onWebsocketDisconnected; this.recordingModel = recordingModel; this.findUserBy = findUserBy; this.getCurrentSession = function() { return currentSession; }; this.getCurrentOrLastSession = function() { return currentOrLastSession; }; this.unsubscribe = function() { server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession); server.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, refreshCurrentSession); server.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, refreshCurrentSession); } }; })(window,jQuery);