$ = 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 sessionRules: null subscriptionRules: 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 isJoinDeferredResolved: false; 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?, @sessionRules, @subscriptionRules, @recordingOwnerId) # onSessionJoinedByOther: (payload) -> # clientId = payload.client_id # #parentClientId = context.jamClient.getParentClientId() # #if parentClientId? && parentClientId != '' # #if parentClientId == clientId # # auto nav to session # if context.jamClient.getClientParentChildRole && context.jamClient.getClientParentChildRole() == CLIENT_ROLE.CHILD && payload.source_user_id == context.JK.currentUserId # logger.debug("autonav to session #{payload.session_id}") # context.SessionActions.navToSession(payload.session_id) onSessionJoinedByOther: `async function(payload) { const clientId = payload.client_id; //parentClientId = context.jamClient.getParentClientId() //if parentClientId? && parentClientId != '' //if parentClientId == clientId // auto nav to session if (await context.jamClient.getClientParentChildRole() === CLIENT_ROLE.CHILD && (payload.source_user_id === context.JK.currentUserId)) { 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?, @sessionRules, @subscriptionRules, @recordingOwnerId) 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() onShowNativeMetronomeGui: `async function() { await 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); onMetronomeCricketChange: `async function(isCricket) { await 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 unstableNTPClocks: `async function() { const unstable = []; // This should be handled in the below loop, actually: const myState = await context.jamClient.getMyNetworkState(); let map = null; for (let participant of Array.from(this.participants())) { var isStable; const isSelf = participant.client_id === this.app.clientId; if (isSelf) { isStable = myState.ntp_stable; } else { map = await context.jamClient.getPeerState(participant.client_id); isStable = map.ntp_stable; } if (!isStable) { let { name } = participant.user; if (isSelf) { name += " (this computer)"; } unstable.push(name); } } return 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"}) onAudioResync: `async function() { logger.debug("audio resyncing"); try{ const response = await context.jamClient.SessionAudioResync(); if(!response.process_status === 'Success') { this.app.notify({ "title": "Error", "text": 'Error in audio resyncing', "icon_url": "/assets/content/icon_alert_big.png"}); } }catch(e){ this.app.notify({ "title": "Error", "text": e.message, "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 onLog: (detail) -> logger.debug("SessionStore: OnLog", detail) # codeInitiated means the user did not initiate this # onCloseMedia: (codeInitiated) -> # logger.debug("SessionStore: onCloseMedia", codeInitiated) # 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", @helper) unless codeInitiated onCloseMedia: `async function(codeInitiated) { logger.debug("SessionStore: onCloseMedia", codeInitiated); if (this.helper.recordedTracks()) { await this.closeRecording(); } else if (this.helper.jamTracks() || this.downloadingJamTrack) { await this.closeJamTrack(); } else if (this.helper.backingTrack() && this.helper.backingTrack().path) { await this.closeBackingTrack(); } else if (this.helper.isMetronomeOpen()) { await this.closeMetronomeTrack(); } else { if (!codeInitiated) { return logger.error("don't know how to close open media", this.helper); } } }` # 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() closeJamTrack: `async function() { logger.debug("closing jam track"); if (this.isRecording) { logger.debug("can't close jamtrack while recording"); this.app.notify({title: 'Can Not Close JamTrack', text: 'A JamTrack can not be closed while recording.'}); } if (!this.selfOpenedJamTracks()) { logger.debug("can't close jamtrack if not the opener"); this.app.notify({title: 'Can Not Close JamTrack', text: 'Only the person who opened the JamTrack can close it.'}); } rest.closeJamTrack({id: this.currentSessionId}) .done(() => { this.downloadingJamTrack = false; return this.refreshCurrentSession(true); }) .fail(jqXHR => { return this.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" }); }); await 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"); # ) onOpenBackingTrack: `async function(result) { console.log('onOpenBackingTrack', result) if (!this.inSession()) { logger.debug("ignoring backing track selected callback (not in session)"); return; } if (result.success) { logger.debug("backing track selected: " + result.file); try{ await rest.openBackingTrack({id: this.currentSessionId, backing_track_path: result.file}); const openResult = await context.jamClient.SessionOpenBackingTrackFile(result.file, false); console.log('openResult', openResult) if (openResult) { // storing session state in memory, not in response of Session server response. bad. this.openBackingTrack = result.file; } else { this.app.notify({ "title": "Couldn't Open Backing Track", "text": "Is the file a valid audio file?", "icon_url": "/assets/content/icon_alert_big.png" }); await this.closeBackingTrack(); } }catch(jqXHR){ this.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() closeRecording: `async function() { logger.debug("closing recording"); rest.stopPlayClaimedRecording({id: this.currentSessionId, claimed_recording_id: this.currentSession.claimed_recording.id}) .done(response => { //sessionModel.refreshCurrentSession(true); // update session info return this.onUpdateSession(response); }) .fail(jqXHR => { return this.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" }); }); await 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" # }) # ) closeMetronomeTrack: `async function() { logger.debug("SessionStore: closeMetronomeTrack"); try{ await rest.closeMetronome({id: this.currentSessionId}) await context.jamClient.SessionCloseMetronome(); this.refreshCurrentSession(true); }catch(jqXHR){ this.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(''); closeBackingTrack: `async function() { if (this.isRecording) { logger.debug("can't close backing track while recording"); return; } rest.closeBackingTrack({id: this.currentSessionId}) .done(() => { }) .fail(() => { return this.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 await context.jamClient.SessionStopPlay(); await 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 .then(()=> 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() # onRecordingChanged: `async function(details) { # let detail, reason, timeline, title; # logger.debug("SessionStore.onRecordingChanged: " + details.cause); # console.log("_DEBUG_ SessionStore.onRecordingChanged: " + JSON.stringify(details)); # this.isRecording = details.isRecording; # this.recordingClinetId = details.clientId; # switch (details.cause) { # case 'started': # if (details.reason) { # ({ # reason # } = details); # ({ # detail # } = details); # title = "Could Not Start Recording"; # switch (reason) { # case 'client-no-response': # this.notifyWithUserInfo(title, 'did not respond to the start signal.', detail); # break; # case 'empty-recording-id': # this.app.notifyAlert(title, "No recording ID specified."); # break; # case 'missing-client': # this.notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); # break; # case 'already-recording': # this.app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.'); # break; # case 'recording-engine-unspecified': # this.notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); # break; # case 'recording-engine-create-directory': # this.notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); # break; # case 'recording-engine-create-file': # this.notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); # break; # case 'recording-engine-sample-rate': # this.notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); # break; # case 'rest': # var jqXHR = detail[0]; # this.app.notifyServerError(jqXHR); # break; # default: # this.notifyWithUserInfo(title, 'Error Reason: ' + reason); # } # } else { # this.displayWhoCreatedRecording(details.clientId); # } # break; # case 'stopped': # if (this.selfOpenedJamTracks()) { # timeline = await context.jamClient.GetJamTrackTimeline(); # rest.addRecordingTimeline(details.recordingId, timeline) # .fail(()=> { # return this.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); # ({ # detail # } = details); # title = "Recording Discarded"; # switch (reason) { # case 'client-no-response': # this.notifyWithUserInfo(title, 'did not respond to the stop signal.', detail); # break; # case 'missing-client': # this.notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail); # break; # case 'empty-recording-id': # this.app.notifyAlert(title, "No recording ID specified."); # break; # case 'wrong-recording-id': # this.app.notifyAlert(title, "Wrong recording ID specified."); # break; # case 'not-recording': # this.app.notifyAlert(title, "Not currently recording."); # break; # case 'already-stopping': # this.app.notifyAlert(title, "Already stopping the current recording."); # break; # case 'start-before-stop': # this.notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail); # break; # default: # this.app.notifyAlert(title, "Error reason: " + reason); # } # } else { # logger.debug('backend opens recording files directory.') # } # break; # case 'abortedRecording': # ({ # reason # } = details); # ({ # detail # } = details); # title = "Recording Cancelled"; # switch (reason) { # case 'client-no-response': # this.notifyWithUserInfo(title, 'did not respond to the start signal.', detail); # break; # case 'missing-client': # this.notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); # break; # case 'populate-recording-info': # this.notifyWithUserInfo(title, 'could not synchronize with the server.', detail); # break; # case 'recording-engine-unspecified': # this.notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); # break; # case 'recording-engine-create-directory': # this.notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); # break; # case 'recording-engine-create-file': # this.notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); # break; # case 'recording-engine-sample-rate': # this.notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); # break; # default: # this.app.notifyAlert(title, "Error reason: " + reason); # } # break; # } # return this.issueChange(); # }` onRecordingChanged: `async function(details) { let detail, reason, timeline, title; logger.debug("SessionStore.onRecordingChanged: " + details.cause); //console.log("_DEBUG_* SessionStore.onRecordingChanged: " + JSON.stringify(details)); //this.isRecording = details.isRecording; this.recordingClinetId = details.clientId; let recordingState = await this.recordingModel.getCurrentRecordingState(); this.recordingOwnerId = recordingState.recordingOwnerId; this.isRecording = recordingState.isRecording; switch (details.cause) { case 'started': if (details.reason) { ({ reason } = details); ({ detail } = details); title = "Could Not Start Recording"; switch (reason) { case 'client-no-response': this.notifyWithUserInfo(title, 'did not respond to the start signal.', detail); break; case 'empty-recording-id': this.app.notifyAlert(title, "No recording ID specified."); break; case 'missing-client': this.notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); break; case 'already-recording': this.app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.'); break; case 'recording-engine-unspecified': this.notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); break; case 'recording-engine-create-directory': this.notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); break; case 'recording-engine-create-file': this.notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); break; case 'recording-engine-sample-rate': this.notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); break; case 'rest': var jqXHR = detail[0]; this.app.notifyServerError(jqXHR); break; default: this.notifyWithUserInfo(title, 'Error Reason: ' + reason); } } else { this.displayWhoCreatedRecording(details.clientId); } break; case 'stopped': if (this.selfOpenedJamTracks()) { timeline = await context.jamClient.GetJamTrackTimeline(); rest.addRecordingTimeline(details.recordingId, timeline) .fail(()=> { return this.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); ({ detail } = details); title = "Recording Discarded"; switch (reason) { case 'client-no-response': this.notifyWithUserInfo(title, 'did not respond to the stop signal.', detail); break; case 'missing-client': this.notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail); break; case 'empty-recording-id': this.app.notifyAlert(title, "No recording ID specified."); break; case 'wrong-recording-id': this.app.notifyAlert(title, "Wrong recording ID specified."); break; case 'not-recording': this.app.notifyAlert(title, "Not currently recording."); break; case 'already-stopping': this.app.notifyAlert(title, "Already stopping the current recording."); break; case 'start-before-stop': this.notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail); break; default: this.app.notifyAlert(title, "Error reason: " + reason); } } else { logger.debug('backend opens recording files directory.') } break; case 'abortedRecording': ({ reason } = details); ({ detail } = details); title = "Recording Cancelled"; switch (reason) { case 'client-no-response': this.notifyWithUserInfo(title, 'did not respond to the start signal.', detail); break; case 'missing-client': this.notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); break; case 'populate-recording-info': this.notifyWithUserInfo(title, 'could not synchronize with the server.', detail); break; case 'recording-engine-unspecified': this.notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); break; case 'recording-engine-create-directory': this.notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); break; case 'recording-engine-create-file': this.notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); break; case 'recording-engine-sample-rate': this.notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); break; default: this.app.notifyAlert(title, "Error reason: " + reason); } break; } return this.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(); findParticipantByUserId: (userId) -> foundParticipant = null for participant in @participants() if participant.user.id == userId foundParticipant = participant break foundParticipant 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) onEnterSession: (sessionId) -> if !context.JK.guardAgainstBrowser(@app) return false; window.location.href = '/client#/session/' + sessionId onJoinSession: `async function(sessionId) { if(!context.JK.guardAgainstBrowser(this.app)) return false; const shareDialog = new JK.ShareDialog(this.app, sessionId, "session"); shareDialog.initialize(context.JK.FacebookHelperInstance); VideoActions.stopVideo(); if (!this.ensureConnected()) { return; } this.sessionEnded(true); this.updateCurrentSession(null); this.currentSessionId = sessionId; this.startTime = new Date().getTime(); try{ const musicSession = await rest.getSessionHistoryPromise(this.currentSessionId) await this.onJoinSessionDone(musicSession) }catch(e){ return logger.error("unable to fetch session history", e); } }` onJoinSessionDone: `async function(musicSession){ const musicianAccessOnJoin = musicSession.musician_access; const shouldVerifyNetwork = musicSession.musician_access; let clientRole = CLIENT_ROLE.PARENT; clientRole = await context.jamClient.getClientParentChildRole(); if (clientRole === CLIENT_ROLE.CHILD) { logger.debug("client is configured to act as child. skipping all checks. assuming 0 tracks"); this.userTracks = []; await this.joinSession(); } try{ await this.gearUtils.guardAgainstInvalidConfiguration(this.app, shouldVerifyNetwork) const result = await this.sessionUtils.SessionPageEnter(); try{ await this.gearUtils.guardAgainstActiveProfileMissing(this.app, result) try{ this.userTracks = await this.waitForSessionPageEnterDone() try{ await this.ensureAppropriateProfile(musicianAccessOnJoin) logger.debug("user has passed all session guards") await this.joinSession() }catch(error){ if (!error.controlled_location) { SessionActions.leaveSession.trigger({location: "/client#/home"}); } } }catch(error){ if (error === "timeout") { context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.'); } else if (error === '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: ' + error); } SessionActions.leaveSession.trigger({location: '/client#/home'}); } }catch(error){ const leaveBehavior = {}; if (error && (error.reason === 'handled')) { if (error.nav === 'BACK') { leaveBehavior.location = -1; } else { leaveBehavior.location = error.nav; } } else { leaveBehavior.location = '/client#/home'; } SessionActions.leaveSession.trigger(leaveBehavior); } }catch(error){ SessionActions.leaveSession.trigger({location: '/client#/home'}); } }` # 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 # waitForSessionPageEnterDone: `function() { # const gearUtils = this.gearUtils; # return new Promise(async function(resolve, reject) { # //see if we already have tracks; if so, we need to run with these # const inputTracks = await context.JK.TrackHelpers.getUserTracks(context.jamClient); # logger.debug("isNoInputProfile", await gearUtils.isNoInputProfile()); # if ((inputTracks.length > 0) || await gearUtils.isNoInputProfile()) { # logger.debug("on page enter, tracks are already available"); # resolve(inputTracks); # }else{ # this.sessionPageEnterTimeout = setTimeout(()=> { # if (this.sessionPageEnterTimeout) { # reject('timeout'); # this.sessionPageEnterTimeout = null; # } # }, 5000); # } # }) # }` waitForSessionPageEnterDone: `async function () { 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", this.gearUtils.isNoInputProfile()) if(inputTracks.length > 0 || this.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; }` # ensureAppropriateProfile: (musicianAccess) -> # deferred = new $.Deferred(); # if musicianAccess # deferred = context.JK.guardAgainstSinglePlayerProfile(@app) # else # deferred.resolve(); # deferred ensureAppropriateProfile: `function(musicianAccess) { const app = this.app return new Promise(async function(resolve, reject){ if (musicianAccess) { try{ await context.JK.guardAgainstSinglePlayerProfile(app); resolve(); }catch(error){ reject(error) } } else { resolve(); } }) }` openBrowserToPayment: () -> context.JK.popExternalLink("/client#/account/subscription", true) openBrowserToPlanComparison: () -> context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-") return 'noclose' # joinSession: () -> # context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); # context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); # context.jamClient.SessionSetConnectionStatusRefreshRate(1000); # clientRole = context.jamClient.getClientParentChildRole(); # parentClientId = context.jamClient.getParentClientId(); # logger.debug("role when joining session: #{clientRole}, parent client id #{parentClientId}") # #context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen) # if clientRole == 0 # clientRole = 'child' # else if clientRole == 1 # clientRole = 'parent' # if clientRole == '' || !clientRole # clientRole = null # # 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, # client_role: clientRole, # parent_client_id: parentClientId # 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); # joinSessionMsg = {sessionID: @currentSessionId, music_session_id_int: response.music_session_id_int} # context.jamClient.JoinSession(joinSessionMsg); # #@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, lesson_session: response.lesson_session}}) 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 if response["errors"] && response["errors"]["remaining_session_play_time"] # leaveBehavior = # location: "/client#/findSession" # buttons = [] # buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}) # buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (@openBrowserToPlanComparison()))}) # buttons.push({ # name: 'UPGRADE PLAN', # buttonStyle: 'button-orange', # click: (() => (@openBrowserToPayment())) # }) # context.JK.Banner.show({ # title: "Out of Time For This Session", # html: context._.template($('#template-no-remaining-session-play-time').html(), {}, { variable: 'data' }), # buttons: buttons}); # SessionActions.leaveSession.trigger(leaveBehavior) # else if response["errors"] && response["errors"]["remaining_month_play_time"] # leaveBehavior = # location: "/client#/findSession" # buttons = [] # buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}) # buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (@openBrowserToPlanComparison()))}) # buttons.push({ # name: 'UPGRADE PLAN', # buttonStyle: 'button-orange', # click: (() => (@openBrowserToPayment())) # }) # context.JK.Banner.show({ # title: "Out of Time for the Month", # html: context._.template($('#template-no-remaining-month-play-time').html(), {}, { variable: 'data' }), # buttons: buttons}); # SessionActions.leaveSession.trigger(leaveBehavior) # else # @app.notifyServerError(xhr, 'Unable to Join Session'); # else # @app.notifyServerError(xhr, 'Unable to Join Session'); # ) joinSession: `async function() { await context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); await context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); await context.jamClient.SessionSetConnectionStatusRefreshRate(1000); let clientRole = await context.jamClient.getClientParentChildRole(); const parentClientId = await context.jamClient.getParentClientId(); logger.debug('role when joining session: '+clientRole+', parent client id '+parentClientId); //context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen) if (clientRole === 0) { clientRole = 'child'; } else if (clientRole === 1) { clientRole = 'parent'; } if ((clientRole === '') || !clientRole) { clientRole = null; } // subscribe to events from the recording model this.recordingRegistration(); // tell the server we want to join this.joinDeferred = rest.joinSessionPromise({ client_id: this.app.clientId, ip_address: context.JK.JamServer.publicIP, as_musician: true, tracks: this.userTracks, session_id: this.currentSessionId, client_role: clientRole, parent_client_id: parentClientId, audio_latency: await context.jamClient.FTUEGetExpectedLatency().latency }).then(async (response) => { this.isJoinDeferredResolved = true if (!this.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"); this.leaveSessionRest(this.currentSessionId); } this.updateSessionInfo(response, true); this.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 if (!this.alreadyInSession()) { if (this.participants().length === 1) { context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create); } else { context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join); } } this.recordingModel.reset(this.currentSessionId); const joinSessionMsg = {sessionID: this.currentSessionId, music_session_id_int: response.music_session_id_int}; await context.jamClient.JoinSession(joinSessionMsg); //@refreshCurrentSession(true); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, this.trackChanges); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, this.trackChanges); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, this.trackChanges); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, this.trackChanges); if (document) { $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: this.currentSessionId, lesson_session: response.lesson_session}}); } this.handleAutoOpenJamTrack(); this.watchBackendStats(); ConfigureTracksActions.reset(true); this.delayEnableVst(); logger.debug("completed session join") }).catch((xhr) => { let leaveBehavior; this.updateCurrentSession(null); if (xhr.status === 404) { // we tried to join the session, but it is 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) { let buttons; const response = JSON.parse(xhr.responseText); if (response["errors"] && response["errors"]["tracks"] && (response["errors"]["tracks"][0] === "Please select at least one track")) { return this.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 if (response["errors"] && response["errors"]["remaining_session_play_time"]) { leaveBehavior = {location: "/client#/findSession"}; buttons = []; buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}); buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (this.openBrowserToPlanComparison()))}); buttons.push({ name: 'UPGRADE PLAN', buttonStyle: 'button-orange', click: (() => (this.openBrowserToPayment())) }); context.JK.Banner.show({ title: "Out of Time For This Session", html: context._.template($('#template-no-remaining-session-play-time').html(), {}, { variable: 'data' }), buttons}); SessionActions.leaveSession.trigger(leaveBehavior); } else if (response["errors"] && response["errors"]["remaining_month_play_time"]) { leaveBehavior = {location: "/client#/findSession"}; buttons = []; buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}); buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (this.openBrowserToPlanComparison()))}); buttons.push({ name: 'UPGRADE PLAN', buttonStyle: 'button-orange', click: (() => (this.openBrowserToPayment())) }); context.JK.Banner.show({ title: "Out of Time for the Month", html: context._.template($('#template-no-remaining-month-play-time').html(), {}, { variable: 'data' }), buttons}); SessionActions.leaveSession.trigger(leaveBehavior); } else { this.app.notifyServerError(xhr, 'Unable to Join Session'); } } else { this.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() delayEnableVst: `async function() { if (this.enableVstTimeout != null) { clearTimeout(this.enableVstTimeout); this.enableVstTimeout = null; } const isVstLoaded = await context.jamClient.IsVstLoaded(); const hasVstAssignment = await context.jamClient.hasVstAssignment(); if (hasVstAssignment && !isVstLoaded) { this.enableVstTimeout = setTimeout((() => { return this.enableVst(); } ), 5000); this.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('', false) # parentConnectionStats = window.jamClient.getConnectionDetail('', true) # #console.log("CONNECTION STATES", connectionStats) # #console.log("PARENT STATES", parentConnectionStats) # SessionStatsActions.pushStats(connectionStats, parentConnectionStats) updateBackendStats: `async function() { //const clientId = await context.jamClient.clientID(); //const parentClientId = await context.jamClient.getParentClientId(); const clientId = context.JK.app.clientId const connectionStats = await window.jamClient.getConnectionDetail('', false); const parentConnectionStats = await window.jamClient.getConnectionDetail('', true); SessionStatsActions.pushStats(connectionStats, parentConnectionStats); }` 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) handleAutoOpenJamTrack: `async function() { const jamTrack = this.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: this.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 => { this.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) -> # console.log("_DEBUG_* SessionStore#updateSessionInfo", session) # 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) # console.log("_DEBUG_* recordingState", recordingState); # logger.debug('update current session') # @updateCurrentSession(session); # #if(callback != null) { # # callback(); # #} # else # logger.info("ignoring refresh because we already have current: " + @currentTrackChanges + ", seen: " + session.track_changes_counter); # updateSessionInfo: `function(session, force) { # console.log("_DEBUG_* SessionStore#updateSessionInfo", session); # if ((force === true) || (this.currentTrackChanges < session.track_changes_counter)) { # logger.debug("updating current track changes from %o to %o", this.currentTrackChanges, session.track_changes_counter); # this.currentTrackChanges = session.track_changes_counter; # this.sendClientParticipantChanges(this.currentSession, session); # this.recordingModel.getRecordingState().then( # (recordingState) => { # if (recordingState) { # //merge the recording state with the session # console.log("_DEBUG_* SessionStore getRecordingState -> recordingState", recordingState); # session = {...session, ...recordingState}; # } # } # ).finally(() => { # console.log("_DEBUG_* SessionStore final -> recordingState", session); # logger.debug('update current session'); # this.updateCurrentSession(session); # //if(callback != null) { # // callback(); # //} # }) # } else { # return logger.info("ignoring refresh because we already have current: " + this.currentTrackChanges + ", seen: " + session.track_changes_counter); # } # }` updateSessionInfo: `function (session, force) { if ((force === true) || (this.currentTrackChanges < session.track_changes_counter)) { logger.debug("updating current track changes from %o to %o", this.currentTrackChanges, session.track_changes_counter); this.currentTrackChanges = session.track_changes_counter; this.sendClientParticipantChanges(this.currentSession, session); this.recordingModel.getCurrentRecordingState().then((recordingState) => { session = { ...session, ...recordingState }; logger.debug('update current session'); }).finally(() => { //console.log("_DEBUG_* SessionStore#updateSessionInfo sessionState", session); this.updateCurrentSession(session); }); } else { return logger.info("ignoring refresh because we already have current: " + this.currentTrackChanges + ", seen: " + session.track_changes_counter); } }` leaveSessionRest: () -> #alert('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}} participantJoined: `async function(newSession, participant) { logger.debug("jamClient.ParticipantJoined", participant.client_id); await context.jamClient.ParticipantJoined(newSession, this.toJamClientParticipant(participant)); this.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] participantLeft: `async function(newSession, participant) { logger.debug("jamClient.ParticipantLeft", participant.client_id); await context.jamClient.ParticipantLeft(newSession, this.toJamClientParticipant(participant)); delete this.currentParticipants[participant.client_id]; }` toJamClientParticipant: (participant) -> { userID: "", clientID: participant.client_id, client_id_int: participant.client_id_int, 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 # if sessionData.session_rules # @sessionRules = sessionData.session_rules # # TESTING: # #@sessionRules.remaining_session_play_time = 60 * 15 + 15 # 15 minutes and 15 seconds # # compute timestamp due time # if @sessionRules.remaining_session_play_time? # until_time = new Date() # until_time = new Date(until_time.getTime() + @sessionRules.remaining_session_play_time * 1000) # console.log("subscription: session has remaining play time", until_time) # @sessionRules.remaining_session_until = until_time # if sessionData.subscription # # for the backend - it looks here # #sessionData.subscription = sessionData.subscription_rules # # let the backend know # #context.jamClient.applySubscriptionPolicy() # @subscriptionRules = sessionData.subscription # # TESTING: # #@subscriptionRules.remaining_month_play_time = 60 * 15 + 15 # 15 minutes and 15 seconds # if @subscriptionRules.remaining_month_play_time? # until_time = new Date() # until_time = new Date(until_time.getTime() + @subscriptionRules.remaining_month_play_time * 1000) # #until_time.setSeconds(until_time.getSeconds() + @subscriptionRules.remaining_month_play_time) # console.log("subscription: month has remaining play time", until_time) # @subscriptionRules.remaining_month_until = until_time # @currentSession = sessionData # if context.jamClient.UpdateSessionInfo? # if @currentSession? # context.jamClient.UpdateSessionInfo(@currentSession) # else # context.jamClient.UpdateSessionInfo({}) # #logger.debug("session changed") # logger.debug("issue change") # @issueChange() updateCurrentSession: `async function(sessionData) { //console.log("_DEBUG_* SessionStore#updateCurrentSession", sessionData) if (sessionData !== null) { let until_time; this.currentOrLastSession = sessionData; if (sessionData.session_rules) { this.sessionRules = sessionData.session_rules; // TESTING: //@sessionRules.remaining_session_play_time = 60 * 15 + 15 # 15 minutes and 15 seconds // compute timestamp due time if (this.sessionRules.remaining_session_play_time != null) { until_time = new Date(); until_time = new Date(until_time.getTime() + (this.sessionRules.remaining_session_play_time * 1000)); console.log("subscription: session has remaining play time", until_time); this.sessionRules.remaining_session_until = until_time; } } if (sessionData.subscription) { // for the backend - it looks here //sessionData.subscription = sessionData.subscription_rules // let the backend know //context.jamClient.applySubscriptionPolicy() this.subscriptionRules = sessionData.subscription; // TESTING: //@subscriptionRules.remaining_month_play_time = 60 * 15 + 15 # 15 minutes and 15 seconds if (this.subscriptionRules.remaining_month_play_time != null) { until_time = new Date(); until_time = new Date(until_time.getTime() + (this.subscriptionRules.remaining_month_play_time * 1000)); //until_time.setSeconds(until_time.getSeconds() + @subscriptionRules.remaining_month_play_time) console.log("subscription: month has remaining play time", until_time); this.subscriptionRules.remaining_month_until = until_time; } } } this.currentSession = sessionData; //if (context.jamClient.UpdateSessionInfo != null) { if (this.currentSession != null) { await context.jamClient.UpdateSessionInfo(this.currentSession); } else { await context.jamClient.UpdateSessionInfo({}); } //} //logger.debug("session changed") logger.debug("issue change"); return this.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() # if @currentSession?.lesson_session? # isTeacher = context.JK.currentUserId == @currentSession.lesson_session.teacher_id # tempSession = @currentSession # rest.ratingDecision({ # as_student: !isTeacher, # teacher_id: @currentSession.lesson_session.teacher_id, # student_id: @currentSession.lesson_session.teacher_id # }).done(((decision) => # showDialog = !decision.rating || showDialog = decision.lesson_count % 6 == 0 # if showDialog # if isTeacher # @app.layout.showDialog('rate-user-dialog', {d1: 'student_' + tempSession.lesson_session.student_id}) # else # @app.layout.showDialog('rate-user-dialog', {d1: 'teacher_' + tempSession.lesson_session.teacher_id}) # else # unless @rateSessionDialog? # @rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); # @rateSessionDialog.initialize(); # @rateSessionDialog.showDialog(); # )) # else # unless @rateSessionDialog? # @rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); # @rateSessionDialog.initialize(); # @rateSessionDialog.showDialog(); # alert('calling leaveSession') # @leaveSession() # @sessionUtils.SessionPageLeave() onLeaveSession: `async function(behavior) { logger.debug("attempting to leave session", behavior); if (behavior.notify) { this.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() if ((this.currentSession != null ? this.currentSession.lesson_session : undefined) != null) { const isTeacher = context.JK.currentUserId === this.currentSession.lesson_session.teacher_id; const tempSession = this.currentSession; rest.ratingDecision({ as_student: !isTeacher, teacher_id: this.currentSession.lesson_session.teacher_id, student_id: this.currentSession.lesson_session.teacher_id }).done((decision => { var showDialog = !decision.rating || (showDialog = (decision.lesson_count % 6) === 0); if (showDialog) { if (isTeacher) { return this.app.layout.showDialog('rate-user-dialog', {d1: 'student_' + tempSession.lesson_session.student_id}); } else { return this.app.layout.showDialog('rate-user-dialog', {d1: 'teacher_' + tempSession.lesson_session.teacher_id}); } } else { if (this.rateSessionDialog == null) { this.rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); this.rateSessionDialog.initialize(); return this.rateSessionDialog.showDialog(); } } })); } else { if (this.rateSessionDialog == null) { this.rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); this.rateSessionDialog.initialize(); } this.rateSessionDialog.showDialog(); } await this.leaveSession(); await this.sessionUtils.SessionPageLeave(); }` # leaveSession: () -> # #if !@joinDeferred? || @joinDeferred?.state() == 'resolved' # if !@joinDeferred? # deferred = new $.Deferred() # @recordingModel.stopRecordingIfNeeded() # .always(()=> # @performLeaveSession(deferred) # ) leaveSession: `async function() { if ((this.joinDeferred == null) || this.isJoinDeferredResolved) { const deferred = new $.Deferred(); const self = this; /*Promise.resolve(this.recordingModel.stopRecordingIfNeeded()) .then(async function() { await self.performLeaveSession(deferred) });*/ await self.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() performLeaveSession: `async function(deferred) { logger.debug("SessionModel.leaveCurrentSession()"); logger.debug("performLeaveSession: calling jamClient.LeaveSession for clientId=" + this.app.clientId); logger.debug('before context.jamClient.LeaveSession') await context.jamClient.LeaveSession({ sessionID: this.currentSessionId }); logger.debug('before leaveSessionRest') this.leaveSessionRest(this.currentSessionId) .done(function() { session = this.getCurrentOrLastSession(); context.SessionStore.updateSessionInfo(session, true) logger.debug('leaveSessionRest done') deferred.resolve(arguments[0], arguments[1], arguments[2]);}.bind(this)) .fail(function() { logger.debug('leaveSessionRest fail') deferred.reject(arguments[0], arguments[1], arguments[2]); }.bind(this)); // 'unregister' for callbacks await context.jamClient.SessionRegisterCallback(""); //context.jamClient.SessionSetAlertCallback(""); await context.jamClient.SessionSetConnectionStatusRefreshRate(0); this.sessionEnded(); this.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' if @joinDeferred? $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}}) @currentTrackChanges = 0 @currentSession = null @joinDeferred = null @isJoinDeferredResolved = false @isRecording = false @currentSessionId = null @sessionRules = null @subscriptionRules = 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 canRecord: () -> if @subscriptionRules? console.log("can record? rules:", @subscriptionRules.can_record_audio ) return @subscriptionRules.can_record_audio else console.log("can record? no rules; allow") return true canVideo: () -> if @subscriptionRules? console.log("can video? rules:", @subscriptionRules.can_use_video) return @subscriptionRules.can_use_video else console.log("can video? no rules; allow") return true getCurrentOrLastSession: () -> @currentOrLastSession handleJoinLeaveRequestCallback: (data) -> op = data["op"] logger.debug("client asks #{op} for #{data["id"]}") if op == "join" sessionId = data["id"] if sessionId != @currentSessionId window.location = "/client#/session/" + sessionId else logger.debug("dropped #{op} because sessionId #{sessionId} matches currentSessionId") else if op == "leave" sessionId = data["id"] if sessionId == @currentSessionId @onLeaveSession({location: '/client#/home'}) else logger.debug("dropped #{op} because sessionId #{sessionId} does not match currentSessionId #{@currentSessionId}") } )