jam-cloud/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee

1225 lines
44 KiB
CoffeeScript

$ = 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
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)
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();
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: (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, 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", $('<span>You will need to reconfigure your audio device.</span>'))
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
if context.jamClient.UpdateSessionInfo?
if @currentSession?
context.jamClient.UpdateSessionInfo(@currentSession)
else
context.jamClient.UpdateSessionInfo({})
#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()
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();
@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
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}")
}
)