$ = jQuery context = window logger = context.JK.logger rest = context.JK.Rest() EVENTS = context.JK.EVENTS MIX_MODES = context.JK.MIX_MODES CLIENT_ROLE = context.JK.CLIENT_ROLE JamTrackActions = @JamTrackActions SessionActions = @SessionActions RecordingActions = @RecordingActions NotificationActions = @NotificationActions VideoActions = @VideoActions ConfigureTracksActions = @ConfigureTracksActions @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 joinDeferred: null recordingModel: null currentTrackChanges: 0 isRecording: false previousAllTracks: {userTracks: [], backingTracks: [], metronomeTracks: []} webcamViewer: null openBackingTrack: null helper: null downloadingJamTrack: false init: -> # Register with the app store to get @app this.listenTo(context.AppStore, this.onAppInit) this.listenTo(context.RecordingStore, this.onRecordingChanged) this.listenTo(context.VideoStore, this.onVideoChanged) onAppInit: (@app) -> @gearUtils = context.JK.GearUtilsInstance @sessionUtils = context.JK.SessionUtils @recordingModel = new context.JK.RecordingModel(@app, rest, context.jamClient); RecordingActions.initModel(@recordingModel) @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?) onSessionJoinedByOther: (payload) -> clientId = payload.client_id parentClientId = context.jamClient.getParentClientId() if parentClientId? && parentClientId != '' if parentClientId == clientId # auto nav to session logger.debug("autonav to session #{payload.session_id}") context.SessionActions.navToSession(payload.session_id) onNavToSession: (sessionId) -> context.location = '/client#/session/' + sessionId onMixdownActive: (mixdown) -> if @currentSession?.jam_track? @currentSession.jam_track.mixdown = mixdown @issueChange() onVideoChanged: (@videoState) -> issueChange: () -> @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?) this.trigger(@helper) onWindowBackgrounded: () -> @app.user() .done((userProfile) => if userProfile.show_whats_next && window.location.pathname.indexOf(gon.client_path) == 0 && !@app.layout.isDialogShowing('getting-started') @app.layout.showDialog('getting-started') ) return unless @inSession() # the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen logger.debug("leaving session because window was closed") SessionActions.leaveSession({location: '/client#/home'}) onBroadcastFailure: (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") onBroadcastSuccess: (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") onBroadcastStopped: (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") onShowNativeMetronomeGui: () -> context.jamClient.SessionShowMetronomeGui() onOpenMetronome: () -> unstable = @unstableNTPClocks() if @participants().length > 1 && unstable.length > 0 names = unstable.join(", ") logger.debug("Unstable clocks: ", names, unstable) context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names: names}, { variable: 'data' })); else data = value: 1 session_size: @participants().length user_id: context.JK.currentUserId user_name: context.JK.currentUserName context.stats.write('web.metronome.open', data) rest.openMetronome({id: @currentSessionId}) .done((response) => MixerActions.openMetronome() @updateSessionInfo(response, true) ) .fail((jqXHR) => @app.notify({ "title": "Couldn't open metronome", "text": "Couldn't inform the server to open metronome. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }) ) onMetronomeCricketChange: (isCricket) -> context.jamClient.setMetronomeCricketTestState(isCricket); unstableNTPClocks: () -> unstable = [] # This should be handled in the below loop, actually: myState = context.jamClient.getMyNetworkState() map = null for participant in @participants() isSelf = participant.client_id == @app.clientId if isSelf isStable = myState.ntp_stable else map = context.jamClient.getPeerState(participant.client_id) isStable = map.ntp_stable if !isStable name = participant.user.name if isSelf name += " (this computer)" unstable.push(name) unstable onDownloadingJamTrack: (downloading) -> @downloadingJamTrack = downloading @issueChange() onToggleSessionVideo: () -> if @videoState?.videoEnabled logger.debug("toggle session video") VideoActions.toggleVideo() else context.JK.Banner.showAlert({ title: "Video Is Disabled", html: "To re-enable video, you must go your video settings in your account settings and enable video.", }) onAudioResync: () -> logger.debug("audio resyncing") response = context.jamClient.SessionAudioResync() if response? @app.notify({ "title": "Error", "text": response, "icon_url": "/assets/content/icon_alert_big.png"}) onSyncWithServer: () -> @refreshCurrentSession(true) onWatchedInputs: (inputTracks) -> logger.debug("obtained tracks at start of session") @sessionPageEnterDeferred.resolve(inputTracks); @sessionPageEnterDeferred = null # codeInitiated means the user did not initiate this onCloseMedia: (codeInitiated) -> logger.debug("SessionStore: onCloseMedia") if @helper.recordedTracks() @closeRecording() else if @helper.jamTracks() || @downloadingJamTrack @closeJamTrack() else if @helper.backingTrack() && @helper.backingTrack().path @closeBackingTrack() else if @helper.isMetronomeOpen() @closeMetronomeTrack() else logger.error("don't know how to close open media") unless codeInitiated closeJamTrack: () -> logger.debug("closing jam track"); if @isRecording logger.debug("can't close jamtrack while recording") @app.notify({title: 'Can Not Close JamTrack', text: 'A JamTrack can not be closed while recording.'}) return unless @selfOpenedJamTracks() logger.debug("can't close jamtrack if not the opener") @app.notify({title: 'Can Not Close JamTrack', text: 'Only the person who opened the JamTrack can close it.'}) return rest.closeJamTrack({id: @currentSessionId}) .done(() => @downloadingJamTrack = false @refreshCurrentSession(true) ) .fail((jqXHR) => @app.notify({ "title": "Couldn't Close JamTrack", "text": "Couldn't inform the server to close JamTrack. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }) ) context.jamClient.JamTrackStopPlay() JamTrackActions.close() onOpenBackingTrack: (result) -> unless @inSession() logger.debug("ignoring backing track selected callback (not in session)") return if result.success logger.debug("backing track selected: " + result.file); rest.openBackingTrack({id: @currentSessionId, backing_track_path: result.file}) .done(() => openResult = context.jamClient.SessionOpenBackingTrackFile(result.file, false); if openResult # storing session state in memory, not in response of Session server response. bad. @openBackingTrack = result.file else @app.notify({ "title": "Couldn't Open Backing Track", "text": "Is the file a valid audio file?", "icon_url": "/assets/content/icon_alert_big.png" }); @closeBackingTrack() ) .fail((jqXHR) => @app.notifyServerError(jqXHR, "Unable to Open Backing Track For Playback"); ) closeRecording: () -> logger.debug("closing recording"); rest.stopPlayClaimedRecording({id: @currentSessionId, claimed_recording_id: @currentSession.claimed_recording.id}) .done((response) => #sessionModel.refreshCurrentSession(true); # update session info @onUpdateSession(response) ) .fail((jqXHR) => @app.notify({ "title": "Couldn't Stop Recording Playback", "text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }) ) context.jamClient.CloseRecording() closeMetronomeTrack:() -> logger.debug("SessionStore: closeMetronomeTrack") rest.closeMetronome({id: @currentSessionId}) .done(() => context.jamClient.SessionCloseMetronome() @refreshCurrentSession(true) ) .fail((jqXHR) => @app.notify({ "title": "Couldn't Close MetronomeTrack", "text": "Couldn't inform the server to close MetronomeTrack. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }) ) closeBackingTrack: () -> if @isRecording logger.debug("can't close backing track while recording") return rest.closeBackingTrack({id: @currentSessionId}) .done(() => ) .fail(() => @app.notify({ "title": "Couldn't Close Backing Track", "text": "Couldn't inform the server to close Backing Track. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }); ) # '' closes all open backing tracks context.jamClient.SessionStopPlay(); context.jamClient.SessionCloseBackingTrackFile(''); 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(()=> MixerActions.syncTracks() ) , 100) else if text == 'Midi-Track Update' logger.debug('midi track sync') MixerActions.syncTracks() else if text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl' backingTracks = trackInfo.backingTracks previousBackingTracks = @previousAllTracks.backingTracks metronomeTracks = trackInfo.metronomeTracks 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) MixerActions.syncTracks() else if !(previousMetronomeTracks.length == 0 && metronomeTracks.length == 0) && previousMetronomeTracks != metronomeTracks #logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks) MixerActions.syncTracks() else @refreshCurrentSession(true) @previousAllTracks = trackInfo else if text == 'Global Peer Input Mixer Mode' MixerActions.mixerModeChanged(MIX_MODES.MASTER) else if text == 'Local Peer Stream Mixer Mode' MixerActions.mixerModeChanged(MIX_MODES.PERSONAL) onRecordingChanged: (details) -> logger.debug("SessionStore.onRecordingChanged: " + details.cause) @isRecording = details.isRecording switch details.cause when 'started' if details.reason reason = details.reason; detail = details.detail; title = "Could Not Start Recording"; switch reason when 'client-no-response' @notifyWithUserInfo(title, 'did not respond to the start signal.', detail) when 'empty-recording-id' @app.notifyAlert(title, "No recording ID specified.") when 'missing-client' @notifyWithUserInfo(title, 'could not be signalled to start recording.', detail) when 'already-recording' @app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.') when 'recording-engine-unspecified' @notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail) when 'recording-engine-create-directory' @notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail) when 'recording-engine-create-file' @notifyWithUserInfo(title, 'had a problem creating a recording file.', detail) when 'recording-engine-sample-rate' @notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail) when 'rest' jqXHR = detail[0]; @app.notifyServerError(jqXHR); else @notifyWithUserInfo(title, 'Error Reason: ' + reason) else @displayWhoCreatedRecording(details.clientId) when 'stopped' if @selfOpenedJamTracks() timeline = context.jamClient.GetJamTrackTimeline(); rest.addRecordingTimeline(details.recordingId, timeline) .fail(()=> @app.notify({ title: "Unable to Add JamTrack Volume Data", text: "The volume of the JamTrack will not be correct in the recorded mix." }, null, true) ) if details.reason logger.warn("Recording Discarded: ", details) reason = details.reason detail = details.detail title = "Recording Discarded" switch reason when 'client-no-response' @notifyWithUserInfo(title, 'did not respond to the stop signal.', detail) when 'missing-client' @notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail) when 'empty-recording-id' @app.notifyAlert(title, "No recording ID specified.") when 'wrong-recording-id' @app.notifyAlert(title, "Wrong recording ID specified.") when 'not-recording' @app.notifyAlert(title, "Not currently recording.") when 'already-stopping' @app.notifyAlert(title, "Already stopping the current recording.") when 'start-before-stop' @notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail) else @app.notifyAlert(title, "Error reason: " + reason) else @promptUserToSave(details.recordingId, timeline); when 'abortedRecording' reason = details.reason detail = details.detail title = "Recording Cancelled" switch reason when 'client-no-response' @notifyWithUserInfo(title, 'did not respond to the start signal.', detail) when 'missing-client' @notifyWithUserInfo(title, 'could not be signalled to start recording.', detail) when 'populate-recording-info' @notifyWithUserInfo(title, 'could not synchronize with the server.', detail) when 'recording-engine-unspecified' @notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail) when 'recording-engine-create-directory' @notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail) when 'recording-engine-create-file' @notifyWithUserInfo(title, 'had a problem creating a recording file.', detail) when 'recording-engine-sample-rate' @notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail) else @app.notifyAlert(title, "Error reason: " + reason) @issueChange() notifyWithUserInfo: (title , text, clientId) -> @findUserBy({clientId: clientId}) .done((user)=> @app.notify({ "title": title, "text": user.name + " " + text, "icon_url": context.JK.resolveAvatarUrl(user.photo_url) }); ) .fail(()=> @app.notify({ "title": title, "text": 'Someone ' + text, "icon_url": "/assets/content/icon_alert_big.png" }) ) findUserBy: (finder) -> if finder.clientId foundParticipant = null for participant in @participants() if participant.client_id == finder.clientId foundParticipant = participant break if foundParticipant return $.Deferred().resolve(foundParticipant.user).promise(); # TODO: find it via some REST API if not found? return $.Deferred().reject().promise(); displayWhoCreatedRecording: (clientId) -> if @app.clientId != clientId # don't show to creator @findUserBy({clientId: clientId}) .done((user) => @app.notify({ "title": "Recording Started", "text": user.name + " started a recording", "icon_url": context.JK.resolveAvatarUrl(user.photo_url) }) ) .fail(() => @app.notify({ "title": "Recording Started", "text": "Oops! Can't determine who started this recording", "icon_url": "/assets/content/icon_alert_big.png" }) ) promptUserToSave: (recordingId, timeline) -> rest.getRecording( {id: recordingId} ) .done((recording) => if timeline recording.timeline = timeline.global context.JK.recordingFinishedDialog.setRecording(recording) @app.layout.showDialog('recordingFinished').one(EVENTS.DIALOG_CLOSED, (e, data) => if data.result && data.result.keep context.JK.prodBubble($('#recording-manager-viewer'), 'file-manager-poke', {}, {positions:['top', 'left', 'right', 'bottom'], offsetParent: $('#session-screen').parent()}) ) ) .fail(@app.ajaxError) onJoinSession: (sessionId) -> # poke ShareDialog shareDialog = new JK.ShareDialog(@app, sessionId, "session"); shareDialog.initialize(context.JK.FacebookHelperInstance); # initialize webcamViewer VideoActions.stopVideo(); # 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(true) # 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; # old client protection if !context.jamClient.getClientParentChildRole? clientRole = CLIENT_ROLE.PARENT else clientRole = context.jamClient.getClientParentChildRole() if clientRole == CLIENT_ROLE.CHILD logger.debug("client is configured to act as child. skipping all checks. assuming 0 tracks") @userTracks = [] @joinSession() return @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(@currentSessionId) return @updateSessionInfo(response, true) @issueChange() 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 @participants().length == 1 context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create); else context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join); @recordingModel.reset(@currentSessionId); context.jamClient.JoinSession({sessionID: @currentSessionId}); #@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() @watchBackendStats() ConfigureTracksActions.reset(true) @delayEnableVst() ) .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'); ) delayEnableVst: () -> if @enableVstTimeout? clearTimeout(@enableVstTimeout) @enableVstTimeout = null isVstLoaded = context.jamClient.IsVstLoaded() hasVstAssignment = context.jamClient.hasVstAssignment() if hasVstAssignment && !isVstLoaded @enableVstTimeout = setTimeout((() => @enableVst() ), 5000) @issueChange() enableVst: () -> @enableVstTimeout = null if @inSession() ConfigureTracksActions.enableVst() else logger.debug("no longer in session; not enabling VSTs at this time") @issueChange() watchBackendStats: () -> @backendStatsInterval = window.setInterval((() => (@updateBackendStats())), 1000) updateBackendStats: () -> connectionStats = window.jamClient.getConnectionDetail('') SessionStatsActions.pushStats(connectionStats) 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: @currentSessionId, jam_track_id: jamTrack.id}) .done((response) => logger.debug("jamtrack opened") # now actually load the jamtrack context.SessionActions.updateSession.trigger(response); # context.JK.CurrentSessionModel.updateSession(response); # loadJamTrack(jamTrack); JamTrackActions.open(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) => try @updateSessionInfo(response, force) catch e logger.error("unable to updateSessionInfo in session refresh", e) #setTimeout(() => # @updateSessionInfo(response, force) #, 5000) ) .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) ) onUpdateSession: (session) -> @updateSessionInfo(session, true) 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 of @participantsEverSeen) @participantsEverSeen[client_id] = participant; if client_id of 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 of 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 #logger.debug("session changed") @issueChange() 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 if behavior.hash window.location.hash = behavior.hash else logger.warn("no location specified in leaveSession action", behavior) window.location = '/client#/home' #VideoActions.stopVideo() @leaveSession() @sessionUtils.SessionPageLeave() leaveSession: () -> if !@joinDeferred? || @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() @issueChange() selfOpenedJamTracks: () -> @currentSession && (@currentSession.jam_track_initiator_id == context.JK.currentUserId) sessionEnded: (onJoin) -> # 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 @backendStatsInterval? window.clearInterval(@backendStatsInterval) @backendStatsInterval = null if @joinDeferred?.state() == 'resolved' $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}}) @currentTrackChanges = 0 @currentSession = null @joinDeferred = null @isRecording = false @currentSessionId = null @currentParticipants = {} @previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []} @openBackingTrack = null @shownAudioMediaMixerHelp = false @controlsLockedForJamTrackRecording = false @openBackingTrack = null @downloadingJamTrack = false @sessionUtils.setAutoOpenJamTrack(null) unless onJoin JamTrackActions.close() NotificationActions.sessionEnded() $(context.AppStore).triggerHandler('SessionEnded') id: () -> @currentSessionId getCurrentOrLastSession: () -> @currentOrLastSession } )