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

583 lines
22 KiB
CoffeeScript

$ = jQuery
context = window
logger = context.JK.logger
rest = context.JK.Rest()
EVENTS = context.JK.EVENTS
MIX_MODES = context.JK.MIX_MODES
SessionActions = @SessionActions
@SessionStore = Reflux.createStore(
{
listenables: SessionActions
userTracks: null # comes from the backend
currentSessionId: null
currentSession: null
currentOrLastSession: null
startTime: null
currentParticipants: {}
participantsEverSeen: {}
users: {} # // User info for session participants
requestingSessionRefresh: false
pendingSessionRefresh: false
sessionPageEnterTimeout: null
sessionPageEnterDeferred: null
gearUtils: null
sessionUtils: null
joinDeffered: null
recordingModel: null
currentTrackChanges: 0
previousAllTracks: {userTracks: [], backingTracks: [], metronomeTracks: []}
init: ->
# Register with the app store to get @app
this.listenTo(context.AppStore, this.onAppInit)
onAppInit: (@app) ->
@gearUtils = context.JK.GearUtilsInstance
@sessionUtils = context.JK.SessionUtils
@recordingModel = new context.JK.RecordingModel(@app, this, rest, context.jamClient);
onWatchedInputs: (inputTracks) ->
logger.debug("obtained tracks at start of session")
@sessionPageEnterDeferred.resolve(inputTracks);
@sessionPageEnterDeferred = null
onMixersChanged: (type, text, trackInfo) ->
return unless @inSession()
if text == 'RebuildAudioIoControl'
if @backendMixerAlertThrottleTimer
clearTimeout(@backendMixerAlertThrottleTimer)
@backendMixerAlertThrottleTimer =
setTimeout(() =>
@backendMixerAlertThrottleTimer = null
if @sessionPageEnterDeferred
# this means we are still waiting for the BACKEND_MIXER_CHANGE that indicates we have user tracks built-out/ready
# we will get at least one BACKEND_MIXER_CHANGE that corresponds to the backend doing a 'audio pause', which won't matter much
# so we need to check that we actaully have userTracks before considering ourselves done
if trackInfo.userTracks.length > 0
logger.debug("obtained tracks at start of session")
@sessionPageEnterDeferred.resolve(trackInfo.userTracks)
@sessionPageEnterDeferred = null
return
# wait until we are fully in session before trying to sync tracks to server
if @joinDeferred
@joinDeferred
.done(()=>
MixerActions.syncTracks()
)
, 100)
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)
onJoinSession: (sessionId) ->
# initialize webcamViewer
if gon.global.video_available && gon.global.video_available != "none"
@webcamViewer.beforeShow()
# double-check that we are connected to the server via websocket
return unless @ensureConnected()
# just make double sure a previous session state is cleared out
@sessionEnded()
# update the session data to be empty
@updateCurrentSession(null)
# start setting data for this new session
@currentSessionId = sessionId
@startTime = new Date().getTime()
# let's find out the public/private nature of this session,
# so that we can decide whether we need to validate the audio profile more aggressively
rest.getSessionHistory(@currentSessionId)
.done((musicSession)=>
musicianAccessOnJoin = musicSession.musician_access
shouldVerifyNetwork = musicSession.musician_access;
@gearUtils.guardAgainstInvalidConfiguration(@app, shouldVerifyNetwork).fail(() =>
SessionActions.leaveSession.trigger({location: '/client#/home'})
).done(() =>
result = @sessionUtils.SessionPageEnter();
@gearUtils.guardAgainstActiveProfileMissing(@app, result)
.fail((data) =>
leaveBehavior = {}
if data && data.reason == 'handled'
if data.nav == 'BACK'
leaveBehavior.location = -1
else
leaveBehavior.location = data.nav
else
leaveBehavior.location = '/client#/home';
SessionActions.leaveSession.trigger(leaveBehavior)
).done(() =>
@waitForSessionPageEnterDone()
.done((userTracks) =>
@userTracks = userTracks
@ensureAppropriateProfile(musicianAccessOnJoin)
.done(() =>
logger.debug("user has passed all session guards")
@joinSession()
)
.fail((result) =>
unless result.controlled_location
SessionActions.leaveSession.trigger({location: "/client#/home"})
)
).fail((data) =>
if data == "timeout"
context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.')
else if data == 'session_over'
# do nothing; session ended before we got the user track info. just bail
logger.debug("session is over; bailing")
else
context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data)
SessionActions.leaveSession.trigger({location: '/client#/home'})
)
)
)
)
.fail(() =>
logger.error("unable to fetch session history")
)
waitForSessionPageEnterDone: () ->
@sessionPageEnterDeferred = $.Deferred()
# see if we already have tracks; if so, we need to run with these
inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient)
logger.debug("isNoInputProfile", @gearUtils.isNoInputProfile())
if inputTracks.length > 0 || @gearUtils.isNoInputProfile()
logger.debug("on page enter, tracks are already available")
@sessionPageEnterDeferred.resolve(inputTracks)
deferred = @sessionPageEnterDeferred
@sessionPageEnterDeferred = null
return deferred
@sessionPageEnterTimeout = setTimeout(()=>
if @sessionPageEnterTimeout
if @sessionPageEnterDeferred
@sessionPageEnterDeferred.reject('timeout')
@sessionPageEnterDeferred = null
@sessionPageEnterTimeout = null
, 5000)
@sessionPageEnterDeferred
ensureAppropriateProfile: (musicianAccess) ->
deferred = new $.Deferred();
if musicianAccess
deferred = context.JK.guardAgainstSinglePlayerProfile(@app)
else
deferred.resolve();
deferred
joinSession: () ->
context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2");
#context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
context.jamClient.SessionSetConnectionStatusRefreshRate(1000);
#context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen)
# subscribe to events from the recording model
@recordingRegistration()
# tell the server we want to join
@joinDeferred = rest.joinSession({
client_id: @app.clientId,
ip_address: context.JK.JamServer.publicIP,
as_musician: true,
tracks: @userTracks,
session_id: @currentSessionId,
audio_latency: context.jamClient.FTUEGetExpectedLatency().latency
})
.done((response) =>
unless @inSession()
# the user has left the session before they got joined. We need to issue a leave again to the server to make sure they are out
logger.debug("user left before fully joined to session. telling server again that they have left")
@leaveSessionRest(response.id)
return
logger.debug("calling jamClient.JoinSession");
# on temporary disconnect scenarios, a user may already be in a session when they enter this path
# so we avoid double counting
unless @alreadyInSession()
if response.music_session.participant_count == 1
context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create);
else
context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join);
@recordingModel.reset(response.id);
context.jamClient.JoinSession({sessionID: response.id});
@refreshCurrentSession(true);
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges);
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges);
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges);
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges);
$(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId}}) if document
@handleAutoOpenJamTrack()
)
.fail((xhr) =>
@updateCurrentSession(null)
if xhr.status == 404
# we tried to join the session, but it's already gone. kick user back to join session screen
leaveBehavior =
location: "/client#/findSession"
notify:
title: "Unable to Join Session",
text: " The session you attempted to join is over."
SessionActions.leaveSession.trigger(leaveBehavior)
else if xhr.status == 422
response = JSON.parse(xhr.responseText);
if response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track"
@app.notifyAlert("No Inputs Configured", $('<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');
)
trackChanges: (header, payload) ->
if @currentTrackChanges < payload.track_changes_counter
# we don't have the latest info. try and go get it
logger.debug("track_changes_counter = stale. refreshing...")
@refreshCurrentSession();
else
if header.type != 'HEARTBEAT_ACK'
# don't log if HEARTBEAT_ACK, or you will see this log all the time
logger.info("track_changes_counter = fresh. skipping refresh...", header, payload)
handleAutoOpenJamTrack: () ->
jamTrack = @sessionUtils.grabAutoOpenJamTrack();
if jamTrack
# give the session to settle just a little (call a timeout of 1 second)
setTimeout(()=>
# tell the server we are about to open a jamtrack
rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id})
.done((response) =>
logger.debug("jamtrack opened")
# now actually load the jamtrack
# TODO
# context.JK.CurrentSessionModel.updateSession(response);
# loadJamTrack(jamTrack);
)
.fail((jqXHR) =>
@app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback")
)
, 1000)
inSession: () ->
!!@currentSessionId
alreadyInSession: () ->
inSession = false
for participant in @participants()
if participant.user.id == context.JK.currentUserId
inSession = true
break
participants: () ->
if @currentSession
@currentSession.participants;
else
[]
refreshCurrentSession: (force) ->
logger.debug("refreshCurrentSession(force=true)") if force
@refreshCurrentSessionRest(force)
refreshCurrentSessionRest: (force) ->
unless @inSession()
logger.debug("refreshCurrentSession skipped: ")
return
if @requestingSessionRefresh
# if someone asks for a refresh while one is going on, we ask for another to queue up
logger.debug("queueing refresh")
@pendingSessionRefresh = true;
else
@requestingSessionRefresh = true
rest.getSession(@currentSessionId)
.done((response) =>
@updateSessionInfo(response, force)
)
.fail((jqXHR) =>
if jqXHR.status != 404
@app.notifyServerError(jqXHR, "Unable to refresh session data")
else
logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone")
)
.always(() =>
@requestingSessionRefresh = false
if @pendingSessionRefresh
# and when the request is done, if we have a pending, fire it off again
@pendingSessionRefresh = false
@refreshCurrentSessionRest(force)
)
updateSessionInfo: (session, force) ->
if force == true || @currentTrackChanges < session.track_changes_counter
logger.debug("updating current track changes from %o to %o", @currentTrackChanges, session.track_changes_counter)
@currentTrackChanges = session.track_changes_counter;
@sendClientParticipantChanges(@currentSession, session);
@updateCurrentSession(session);
#if(callback != null) {
# callback();
#}
else
logger.info("ignoring refresh because we already have current: " + @currentTrackChanges + ", seen: " + session.track_changes_counter);
leaveSessionRest: () ->
rest.deleteParticipant(@app.clientId);
sendClientParticipantChanges: (oldSession, newSession) ->
joins = []
leaves = []
leaveJoins = []; # Will hold JamClientParticipants
oldParticipants = []; # will be set to session.participants if session
oldParticipantIds = {};
newParticipants = [];
newParticipantIds = {};
if oldSession && oldSession.participants
for oldParticipant in oldSession.participants
oldParticipantIds[oldParticipant.client_id] = oldParticipant
if newSession && newSession.participants
for newParticipant in newSession.participants
newParticipantIds[newParticipant.client_id] = newParticipant
for client_id, participant of newParticipantIds
# grow the 'all participants seen' list
unless (client_id 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
console.log("SESSION CHANGED", sessionData)
this.trigger(new context.SessionHelper(@app, @currentSession))
ensureConnected: () ->
unless context.JK.JamServer.connected
leaveBehavior =
location: '/client#/home'
notify:
title: "Not Connected"
text: 'To create or join a session, you must be connected to the server.'
SessionActions.leaveSession.trigger(leaveBehavior)
context.JK.JamServer.connected
# called by anyone wanting to leave the session with a certain behavior
onLeaveSession: (behavior) ->
logger.debug("attempting to leave session", behavior)
if behavior.notify
@app.layout.notify(behavior.notify)
SessionActions.allowLeaveSession.trigger()
if behavior.location
if jQuery.isNumeric(behavior.location)
window.history.go(behavior.location)
else
window.location = behavior.location
else
logger.warn("no location specified in leaveSession action", behavior)
window.location = '/client#/home'
if gon.global.video_available && gon.global.video_available != "none"
@webcamViewer.setVideoOff()
@leaveSession()
@sessionUtils.SessionPageLeave()
leaveSession: () ->
if @joinDeferred?.state() == 'resolved'
deferred = new $.Deferred()
@recordingModel.stopRecordingIfNeeded()
.always(()=>
@performLeaveSession(deferred)
)
performLeaveSession: (deferred) ->
logger.debug("SessionModel.leaveCurrentSession()")
# TODO - sessionChanged will be called with currentSession = null\
# leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long
# time, for that entire duration you'll still be sending voice data to the other users.
# this may be bad if someone decides to badmouth others in the left-session during this time
logger.debug("performLeaveSession: calling jamClient.LeaveSession for clientId=" + @app.clientId)
context.jamClient.LeaveSession({ sessionID: @currentSessionId })
@leaveSessionRest(@currentSessionId)
.done(=>
deferred.resolve(arguments[0], arguments[1], arguments[2]))
.fail(=>
deferred.reject(arguments[0], arguments[1], arguments[2]);
)
# 'unregister' for callbacks
context.jamClient.SessionRegisterCallback("");
#context.jamClient.SessionSetAlertCallback("");
context.jamClient.SessionSetConnectionStatusRefreshRate(0);
@sessionEnded()
this.trigger(new context.SessionHelper(@app, @currentSession))
sessionEnded: () ->
# cleanup
context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges);
context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges);
context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges);
context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges);
if @sessionPageEnterDeferred?
@sessionPageEnterDeferred.reject('session_over')
@sessionPageEnterDeferred = null
if @backendMixerAlertThrottleTimer
clearTimeout(@backendMixerAlertThrottleTimer)
@backendMixerAlertThrottleTimer = null
@userTracks = null;
@startTime = null;
if @joinDeffered?.state() == 'resolved'
$(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}})
@currentTrackChanges = 0
@currentSession = null
@joinDeferred = null
@currentSessionId = null
@currentParticipants = {}
@previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []}
@openBackingTrack = null
@shownAudioMediaMixerHelp = false
@controlsLockedForJamTrackRecording = false;
}
)