$ = jQuery context = window logger = context.JK.logger rest = context.JK.Rest() EVENTS = context.JK.EVENTS; SessionActions = @SessionActions @SessionStore = Reflux.createStore( { listenables: SessionActions userTracks: null # comes from the backend currentSessionId: null currentSession: null currentOrLastSession: null startTime: null currentParticipants: {} participantsEverSeen: {} users: {} # // User info for session participants requestingSessionRefresh: false pendingSessionRefresh: false sessionPageEnterTimeout: null sessionPageEnterDeferred: null gearUtils: null sessionUtils: null joinDeffered: null recordingModel: null currentTrackChanges: 0 init: -> # Register with the app store to get @app this.listenTo(context.AppStore, this.onAppInit) onAppInit: (@app) -> @gearUtils = context.JK.GearUtilsInstance @sessionUtils = context.JK.SessionUtils @recordingModel = new context.JK.RecordingModel(@app, this, rest, context.jamClient); onWatchedInputs: (inputTracks) -> logger.debug("obtained tracks at start of session") @sessionPageEnterDeferred.resolve(inputTracks); @sessionPageEnterDeferred = null onMixersChanged: (type, text, trackInfo) -> return unless @inSession() if text == 'RebuildAudioIoControl' if @backendMixerAlertThrottleTimer clearTimeout(@backendMixerAlertThrottleTimer) @backendMixerAlertThrottleTimer = setTimeout( () => @backendMixerAlertThrottleTimer = null 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 if trackInfo.userTracks.length > 0 logger.debug("obtained tracks at start of session") @sessionPageEnterDeferred.resolve(trackInfo.userTracks); @sessionPageEnterDeferred = null return // wait until we are fully in session before trying to sync tracks to server if @joinDeferred @joinDeferred .done(()=> @syncTracks() , 100) else if @session.inSession() && text == 'RebuildMediaControl' SessionActions.mediaTracksChanged.trigger(@mixers.getTrackInfo()) else if @session.inSession() && text == 'RebuildRemoteUserControl' SessionActions.otherTracksChanged.trigger(@mixers.getTrackInfo()) onJoinSession: (sessionId) -> # initialize webcamViewer if gon.global.video_available && gon.global.video_available != "none" @webcamViewer.beforeShow() # double-check that we are connected to the server via websocket return unless @ensureConnected() # just make double sure a previous session state is cleared out @sessionEnded() # update the session data to be empty @updateCurrentSession(null) # start setting data for this new session @currentSessionId = sessionId @startTime = new Date().getTime() # let's find out the public/private nature of this session, # so that we can decide whether we need to validate the audio profile more aggressively rest.getSessionHistory(@currentSessionId) .done((musicSession)=> musicianAccessOnJoin = musicSession.musician_access shouldVerifyNetwork = musicSession.musician_access; @gearUtils.guardAgainstInvalidConfiguration(@app, shouldVerifyNetwork).fail(() => SessionActions.leaveSession.trigger({location: '/client#/home'}) ).done(() => result = @sessionUtils.SessionPageEnter(); @gearUtils.guardAgainstActiveProfileMissing(@app, result) .fail((data) => leaveBehavior = {} if data && data.reason == 'handled' if data.nav == 'BACK' leaveBehavior.location = -1 else leaveBehavior.location = data.nav else leaveBehavior.location = '/client#/home'; SessionActions.leaveSession.trigger(leaveBehavior) ).done(() => @waitForSessionPageEnterDone() .done((userTracks) => @userTracks = userTracks @ensureAppropriateProfile(musicianAccessOnJoin) .done(() => logger.debug("user has passed all session guards") @joinSession() ) .fail((result) => unless result.controlled_location SessionActions.leaveSession.trigger({location: "/client#/home"}) ) ).fail((data) => if data == "timeout" context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.') else if data == 'session_over' # do nothing; session ended before we got the user track info. just bail logger.debug("session is over; bailing") else context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) SessionActions.leaveSession.trigger({location: '/client#/home'}) ) ) ) ) .fail(() => logger.error("unable to fetch session history") ) waitForSessionPageEnterDone: () -> @sessionPageEnterDeferred = $.Deferred() # see if we already have tracks; if so, we need to run with these inputTracks = 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) deferred = @sessionPageEnterDeferred @sessionPageEnterDeferred = null return deferred @sessionPageEnterTimeout = setTimeout(()=> if @sessionPageEnterTimeout if @sessionPageEnterDeferred @sessionPageEnterDeferred.reject('timeout') @sessionPageEnterDeferred = null @sessionPageEnterTimeout = null , 5000) @sessionPageEnterDeferred ensureAppropriateProfile: (musicianAccess) -> deferred = new $.Deferred(); if musicianAccess deferred = context.JK.guardAgainstSinglePlayerProfile(@app) else deferred.resolve(); deferred joinSession: () -> context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); #context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); context.jamClient.SessionSetConnectionStatusRefreshRate(1000); #context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen) # subscribe to events from the recording model @recordingRegistration() # tell the server we want to join @joinDeferred = rest.joinSession({ client_id: @app.clientId, ip_address: context.JK.JamServer.publicIP, as_musician: true, tracks: @userTracks, session_id: @currentSessionId, audio_latency: context.jamClient.FTUEGetExpectedLatency().latency }) .done((response) => unless @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 unless @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(response.id); context.jamClient.JoinSession({sessionID: response.id}); @refreshCurrentSession(true); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges); $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId}}) if document @handleAutoOpenJamTrack() ) .fail((xhr) => @updateCurrentSession(null) if xhr.status == 404 # we tried to join the session, but it's already gone. kick user back to join session screen leaveBehavior = location: "/client#/findSession" notify: title: "Unable to Join Session", text: " The session you attempted to join is over." SessionActions.leaveSession.trigger(leaveBehavior) else if xhr.status == 422 response = JSON.parse(xhr.responseText); if response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track" @app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')) else if response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"] leaveBehavior = location: "/client#/findSession" notify: title: "Unable to Join Session" text: "The session is currently recording." SessionActions.leaveSession.trigger(leaveBehavior) else @app.notifyServerError(xhr, 'Unable to Join Session'); else @app.notifyServerError(xhr, 'Unable to Join Session'); ) 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) handleAutoOpenJamTrack: () -> jamTrack = @sessionUtils.grabAutoOpenJamTrack(); if jamTrack # give the session to settle just a little (call a timeout of 1 second) setTimeout(()=> # tell the server we are about to open a jamtrack rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) .done((response) => logger.debug("jamtrack opened") # now actually load the jamtrack # TODO # context.JK.CurrentSessionModel.updateSession(response); # loadJamTrack(jamTrack); ) .fail((jqXHR) => @app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback") ) , 1000) inSession: () -> !!@currentSessionId alreadyInSession: () -> inSession = false for participant in @participants() if participant.user.id == context.JK.currentUserId inSession = true break participants: () -> if @currentSession @currentSession.participants; else [] refreshCurrentSession: (force) -> logger.debug("refreshCurrentSession(force=true)") if force @refreshCurrentSessionRest(force) refreshCurrentSessionRest: (force) -> unless @inSession() logger.debug("refreshCurrentSession skipped: ") return 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 rest.getSession(@currentSessionId) .done((response) => @updateSessionInfo(response, force) ) .fail((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") ) .always(() => @requestingSessionRefresh = false if @pendingSessionRefresh # and when the request is done, if we have a pending, fire it off again pendingSessionRefresh = false @refreshCurrentSessionRest(force) ) updateSessionInfo: (session, force) -> if force == true || @currentTrackChanges < session.track_changes_counter logger.debug("updating current track changes from %o to %o", @currentTrackChanges, session.track_changes_counter) @currentTrackChanges = session.track_changes_counter; @sendClientParticipantChanges(@currentSession, session); @updateCurrentSession(session); #if(callback != null) { # callback(); #} else logger.info("ignoring refresh because we already have current: " + @currentTrackChanges + ", seen: " + session.track_changes_counter); leaveSessionRest: () -> rest.deleteParticipant(@app.clientId); sendClientParticipantChanges: (oldSession, newSession) -> joins = [] leaves = [] leaveJoins = []; # Will hold JamClientParticipants oldParticipants = []; # will be set to session.participants if session oldParticipantIds = {}; newParticipants = []; newParticipantIds = {}; if oldSession && oldSession.participants for oldParticipant in oldSession.participants oldParticipantIds[oldParticipant.client_id] = oldParticipant if newSession && newSession.participants for newParticipant in newSession.participants newParticipantIds[newParticipant.client_id] = newParticipant for client_id, participant of newParticipantIds # grow the 'all participants seen' list unless (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); for client_id, participant of oldParticipantIds unless (client_id in newParticipantIds) # old participant id that's not in new participant ids: Leave leaves.push(participant); for i, v of joins if v.client_id != @app.clientId @participantJoined(newSession, v) for i,v of leaves if v.client_id != @app.clientId @participantLeft(newSession, v) for i,v of leaveJoins if v.client_id != @app.clientId logger.debug("participant had a rapid leave/join") @participantLeft(newSession, v) @participantJoined(newSession, v) participantJoined: (newSession, participant) -> logger.debug("jamClient.ParticipantJoined", participant.client_id) context.jamClient.ParticipantJoined(newSession, @toJamClientParticipant(participant)); @currentParticipants[participant.client_id] = {server: participant, client: {audio_established: null}} participantLeft: (newSession, participant) -> logger.debug("jamClient.ParticipantLeft", participant.client_id) context.jamClient.ParticipantLeft(newSession, @toJamClientParticipant(participant)); delete @currentParticipants[participant.client_id] toJamClientParticipant: (participant) -> { userID: "", clientID: participant.client_id, tcpPort: 0, udpPort: 0, localIPAddress: participant.ip_address, # ? globalIPAddress: participant.ip_address, # ? latency: 0, natType: "" } recordingRegistration: () -> logger.debug("recording registration not hooked up yet") updateCurrentSession: (sessionData) -> if sessionData != null @currentOrLastSession = sessionData @currentSession = sessionData console.log("SESSION CHANGED", sessionData) this.trigger(new context.SessionHelper(@app, @currentSession)) ensureConnected: () -> unless context.JK.JamServer.connected leaveBehavior = location: '/client#/home' notify: title: "Not Connected" text: 'To create or join a session, you must be connected to the server.' SessionActions.leaveSession.trigger(leaveBehavior) context.JK.JamServer.connected # called by anyone wanting to leave the session with a certain behavior onLeaveSession: (behavior) -> logger.debug("attempting to leave session", behavior) if behavior.notify @app.layout.notify(behavior.notify) SessionActions.allowLeaveSession.trigger() if behavior.location if jQuery.isNumeric(behavior.location) window.history.go(behavior.location) else window.location = behavior.location else logger.warn("no location specified in leaveSession action", behavior) window.location = '/client#/home' if gon.global.video_available && gon.global.video_available != "none" @webcamViewer.setVideoOff() @leaveSession() @sessionUtils.SessionPageLeave() leaveSession: () -> if @joinDeferred?.state() == 'resolved' deferred = new $.Deferred() @recordingModel.stopRecordingIfNeeded() .always(()=> @performLeaveSession(deferred) ) performLeaveSession: (deferred) -> logger.debug("SessionModel.leaveCurrentSession()") # TODO - sessionChanged will be called with currentSession = null\ # 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("performLeaveSession: calling jamClient.LeaveSession for clientId=" + @app.clientId) context.jamClient.LeaveSession({ sessionID: @currentSessionId }) @leaveSessionRest(@currentSessionId) .done(=> deferred.resolve(arguments[0], arguments[1], arguments[2])) .fail(=> deferred.reject(arguments[0], arguments[1], arguments[2]); ) # 'unregister' for callbacks context.jamClient.SessionRegisterCallback(""); #context.jamClient.SessionSetAlertCallback(""); context.jamClient.SessionSetConnectionStatusRefreshRate(0); @sessionEnded() this.trigger(new context.SessionHelper(@app, @currentSession)) sessionEnded: () -> # cleanup context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges); context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges); context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges); context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges); if @sessionPageEnterDeferred? @sessionPageEnterDeferred.reject('session_over') @sessionPageEnterDeferred = null if @backendMixerAlertThrottleTimer clearTimeout(@backendMixerAlertThrottleTimer) @backendMixerAlertThrottleTimer = null @userTracks = null; @startTime = null; if @joinDeffered?.state() == 'resolved' $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}}) @currentTrackChanges = 0 @currentSession = null @joinDeferred = null @currentSessionId = null @currentParticipants = {} @previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []} @openBackingTrack = null @shownAudioMediaMixerHelp = false @controlsLockedForJamTrackRecording = false; } )