const $ = jQuery; const context = window; const { logger } = context.JK; const rest = context.JK.Rest(); const { EVENTS } = context.JK; const { MIX_MODES } = context.JK; const { CLIENT_ROLE } = context.JK; const { JamTrackActions } = context; const { SessionActions } = context; const { RecordingActions } = context; const { NotificationActions } = context; const { VideoActions } = context; const { ConfigureTracksActions } = context; const shouldInstallModernSessionStore = !!context.__JK_SKIP_LEGACY_SESSION_STORE__; context.__JK_SESSION_STORE_MODERN_LOAD_COUNT = (context.__JK_SESSION_STORE_MODERN_LOAD_COUNT || 0) + 1; if (shouldInstallModernSessionStore) { // Marker used by legacy SessionStore.js.coffee to avoid registering a second // Reflux store in modern bundle builds. context.__JK_USE_MODERN_SESSION_STORE__ = true; logger.warn("[session-store] modern-load", { count: context.__JK_SESSION_STORE_MODERN_LOAD_COUNT, legacy_load_count: context.__JK_SESSION_STORE_LEGACY_LOAD_COUNT || 0 }); if (context.JK.DebugLogCollector && context.JK.DebugLogCollector.push) { context.JK.DebugLogCollector.push("join-source.session-store.modern-load", { count: context.__JK_SESSION_STORE_MODERN_LOAD_COUNT, legacy_load_count: context.__JK_SESSION_STORE_LEGACY_LOAD_COUNT || 0 }); } context.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, recordingModel: null, currentTrackChanges: 0, isRecording: false, previousAllTracks: {userTracks: [], backingTracks: [], metronomeTracks: []}, webcamViewer: null, openBackingTrack: null, helper: null, downloadingJamTrack: false, init() { logger.warn("[session-store] modern-init"); if (context.JK.DebugLogCollector && context.JK.DebugLogCollector.push) { context.JK.DebugLogCollector.push("join-source.session-store.modern-init", {}); } // Register with the app store to get @app this.listenTo(context.AppStore, this.onAppInit); this.listenTo(context.RecordingStore, this.onRecordingChanged); return this.listenTo(context.VideoStore, this.onVideoChanged); }, onAppInit(app) { this.app = app; this.gearUtils = context.JK.GearUtilsInstance; this.sessionUtils = context.JK.SessionUtils; this.recordingModel = new context.JK.RecordingModel(this.app, rest, context.jamClientAdapter); RecordingActions.initModel(this.recordingModel); return this.helper = new context.SessionHelper(this.app, this.currentSession, this.participantsEverSeen, this.isRecording, this.downloadingJamTrack, (this.enableVstTimeout != null), this.sessionRules, this.subscriptionRules); }, onSessionJoinedByOther(payload) { const clientId = payload.client_id; //parentClientId = context.jamClientAdapter.getParentClientId() //if parentClientId? && parentClientId != '' //if parentClientId == clientId // auto nav to session if (context.jamClientAdapter.getClientParentChildRole && (context.jamClientAdapter.getClientParentChildRole() === CLIENT_ROLE.CHILD) && (payload.source_user_id === context.JK.currentUserId)) { logger.debug(`autonav to session ${payload.session_id}`); return context.SessionActions.navToSession(payload.session_id); } }, onNavToSession(sessionId) { return context.location = '/client#/session/' + sessionId; }, onMixdownActive(mixdown) { if ((this.currentSession != null ? this.currentSession.jam_track : undefined) != null) { this.currentSession.jam_track.mixdown = mixdown; return this.issueChange(); } }, onVideoChanged(videoState) { this.videoState = videoState; }, issueChange() { this.helper = new context.SessionHelper(this.app, this.currentSession, this.participantsEverSeen, this.isRecording, this.downloadingJamTrack, (this.enableVstTimeout != null), this.sessionRules, this.subscriptionRules); return this.trigger(this.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') //) if (!this.inSession()) { return; } // 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"); return SessionActions.leaveSession({location: '/client#/home'}); }, onBroadcastFailure(text) { logger.debug("SESSION_LIVEBROADCAST_FAIL alert. reason:" + text); if ((this.currentSession != null) && (this.currentSession.mount != null)) { return rest.createSourceChange({ mount_id: this.currentSession.mount.id, source_direction: true, success: false, reason: text, client_id: this.app.clientId }); } else { return logger.debug("unable to report source change because no mount seen on session"); } }, onBroadcastSuccess(text) { logger.debug("SESSION_LIVEBROADCAST_ACTIVE alert. reason:" + text); if ((this.currentSession != null) && (this.currentSession.mount != null)) { return rest.createSourceChange({ mount_id: this.currentSession.mount.id, source_direction: true, success: true, reason: text, client_id: this.app.clientId }); } else { return logger.debug("unable to report source change because no mount seen on session"); } }, onBroadcastStopped(text) { logger.debug("SESSION_LIVEBROADCAST_STOPPED alert. reason:" + text); if ((this.currentSession != null) && (this.currentSession.mount != null)) { return rest.createSourceChange({ mount_id: this.currentSession.mount.id, source_direction: false, success: true, reason: text, client_id: this.app.clientId }); } else { return logger.debug("unable to report source change because no mount seen on session"); } }, onShowNativeMetronomeGui() { return context.jamClientAdapter.SessionShowMetronomeGui(); }, onOpenMetronome() { const unstable = this.unstableNTPClocks(); if ((this.participants().length > 1) && (unstable.length > 0)) { const names = unstable.join(", "); logger.debug("Unstable clocks: ", names, unstable); return context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names}, { variable: 'data' })); } else { const data = { value: 1, session_size: this.participants().length, user_id: context.JK.currentUserId, user_name: context.JK.currentUserName }; context.stats.write('web.metronome.open', data); return rest.openMetronome({id: this.currentSessionId}) .done(response => { MixerActions.openMetronome(); return this.updateSessionInfo(response, true); }) .fail(jqXHR => { return this.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) { return context.jamClientAdapter.setMetronomeCricketTestState(isCricket); }, unstableNTPClocks() { const unstable = []; // This should be handled in the below loop, actually: const myState = context.jamClientAdapter.getMyNetworkState(); let map = null; for (var participant of Array.from(this.participants())) { var isStable; var isSelf = participant.client_id === this.app.clientId; if (isSelf) { isStable = myState.ntp_stable; } else { map = context.jamClientAdapter.getPeerState(participant.client_id); isStable = map.ntp_stable; } if (!isStable) { var { name } = participant.user; if (isSelf) { name += " (this computer)"; } unstable.push(name); } } return unstable; }, onDownloadingJamTrack(downloading) { this.downloadingJamTrack = downloading; return this.issueChange(); }, onToggleSessionVideo() { if (this.videoState != null ? this.videoState.videoEnabled : undefined) { logger.debug("toggle session video"); return VideoActions.toggleVideo(); } else { return 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"); const response = context.jamClientAdapter.SessionAudioResync(); if (response != null) { return this.app.notify({ "title": "Error", "text": response, "icon_url": "/assets/content/icon_alert_big.png"}); } }, onSyncWithServer() { return this.refreshCurrentSession(true); }, onWatchedInputs(inputTracks) { logger.debug("obtained tracks at start of session"); this.sessionPageEnterDeferred.resolve(inputTracks); return this.sessionPageEnterDeferred = null; }, onLog(detail) { return logger.debug("SessionStore: OnLog", detail); }, // codeInitiated means the user did not initiate this onCloseMedia(codeInitiated) { logger.debug("SessionStore: onCloseMedia", codeInitiated); if (this.helper.recordedTracks()) { return this.closeRecording(); } else if (this.helper.jamTracks() || this.downloadingJamTrack) { return this.closeJamTrack(); } else if (this.helper.backingTrack() && this.helper.backingTrack().path) { return this.closeBackingTrack(); } else if (this.helper.isMetronomeOpen()) { return 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 (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.'}); return; } 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.'}); return; } 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" }); }); context.jamClientAdapter.JamTrackStopPlay(); return JamTrackActions.close(); }, 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); return rest.openBackingTrack({id: this.currentSessionId, backing_track_path: result.file}) .done(() => { const openResult = context.jamClientAdapter.SessionOpenBackingTrackFile(result.file, false); if (openResult) { // storing session state in memory, not in response of Session server response. bad. return 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" }); return this.closeBackingTrack(); } }) .fail(jqXHR => { return this.app.notifyServerError(jqXHR, "Unable to Open Backing Track For Playback"); }); } }, closeRecording() { 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" }); }); return context.jamClientAdapter.CloseRecording(); }, closeMetronomeTrack() { logger.debug("SessionStore: closeMetronomeTrack"); return rest.closeMetronome({id: this.currentSessionId}) .done(() => { context.jamClientAdapter.SessionCloseMetronome(); return this.refreshCurrentSession(true); }) .fail(jqXHR => { return 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 (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 context.jamClientAdapter.SessionStopPlay(); return context.jamClientAdapter.SessionCloseBackingTrackFile(''); }, onMixersChanged(type, text, trackInfo) { if (!this.inSession()) { return; } if (text === 'RebuildAudioIoControl') { if (this.backendMixerAlertThrottleTimer) { clearTimeout(this.backendMixerAlertThrottleTimer); } return this.backendMixerAlertThrottleTimer = setTimeout(() => { this.backendMixerAlertThrottleTimer = null; if (this.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"); this.sessionPageEnterDeferred.resolve(trackInfo.userTracks); this.sessionPageEnterDeferred = null; } return; } // wait until we are fully in session before trying to sync tracks to server if (this.joinDeferred) { return this.joinDeferred .done(()=> { return MixerActions.syncTracks(); }); } } , 100); } else if (text === 'Midi-Track Update') { logger.debug('midi track sync'); return MixerActions.syncTracks(); } else if ((text === 'RebuildMediaControl') || (text === 'RebuildRemoteUserControl')) { const { backingTracks } = trackInfo; const previousBackingTracks = this.previousAllTracks.backingTracks; const { metronomeTracks } = trackInfo; const previousMetronomeTracks = this.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 { this.refreshCurrentSession(true); } return this.previousAllTracks = trackInfo; } else if (text === 'Global Peer Input Mixer Mode') { return MixerActions.mixerModeChanged(MIX_MODES.MASTER); } else if (text === 'Local Peer Stream Mixer Mode') { return MixerActions.mixerModeChanged(MIX_MODES.PERSONAL); } }, onRecordingChanged(details) { let detail, reason, timeline, title; logger.debug("SessionStore.onRecordingChanged: " + details.cause); this.isRecording = details.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 = context.jamClientAdapter.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 { this.promptUserToSave(details.recordingId, timeline); } 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) { return this.findUserBy({clientId}) .done(user=> { return this.app.notify({ "title": title, "text": user.name + " " + text, "icon_url": context.JK.resolveAvatarUrl(user.photo_url) }); }) .fail(()=> { return this.app.notify({ "title": title, "text": 'Someone ' + text, "icon_url": "/assets/content/icon_alert_big.png" }); }); }, findUserBy(finder) { if (finder.clientId) { let foundParticipant = null; for (var participant of Array.from(this.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) { let foundParticipant = null; for (var participant of Array.from(this.participants())) { if (participant.user.id === userId) { foundParticipant = participant; break; } } return foundParticipant; }, displayWhoCreatedRecording(clientId) { if (this.app.clientId !== clientId) { // don't show to creator return this.findUserBy({clientId}) .done(user => { return this.app.notify({ "title": "Recording Started", "text": user.name + " started a recording", "icon_url": context.JK.resolveAvatarUrl(user.photo_url) }); }) .fail(() => { return this.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) { return rest.getRecording( {id: recordingId} ) .done(recording => { if (timeline) { recording.timeline = timeline.global; } context.JK.recordingFinishedDialog.setRecording(recording); return this.app.layout.showDialog('recordingFinished').one(EVENTS.DIALOG_CLOSED, (e, data) => { if (data.result && data.result.keep) { return context.JK.prodBubble($('#recording-manager-viewer'), 'file-manager-poke', {}, {positions:['top', 'left', 'right', 'bottom'], offsetParent: $('#session-screen').parent()}); } }); }) .fail(this.app.ajaxError); }, onEnterSession(sessionId) { if (!context.JK.guardAgainstBrowser(this.app)) { return false; } return window.location.href = '/client#/session/' + sessionId; }, async onJoinSession(sessionId) { logger.warn("[session-store] modern-onJoinSession", {session_id: sessionId}); // poke ShareDialog const shareDialog = new JK.ShareDialog(this.app, sessionId, "session"); shareDialog.initialize(context.JK.FacebookHelperInstance); // initialize webcamViewer VideoActions.stopVideo(); // double-check that we are connected to the server via websocket if (!this.ensureConnected()) { return; } // just make double sure a previous session state is cleared out this.sessionEnded(true); // update the session data to be empty this.updateCurrentSession(null); // start setting data for this new session this.currentSessionId = sessionId; this.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 try { const musicSession = await rest.getSessionHistory(this.currentSessionId); return await this.onJoinSessionDone(musicSession); } catch (e) { return logger.error("unable to fetch session history", e); } }, async onJoinSessionDone(musicSession) { const pushJoinAbort = (reason, detail) => { context.__jkJoinAborts = context.__jkJoinAborts || []; context.__jkJoinAborts.push({ ts: (new Date()).toISOString(), reason, detail, current_session_id: this.currentSessionId }); if (context.JK && context.JK.DebugLogCollector && context.JK.DebugLogCollector.push) { context.JK.DebugLogCollector.push("join-source.session-store.modern-abort", { reason, detail, current_session_id: this.currentSessionId, stack: (new Error("SessionStoreModern.onJoinSessionDone.abort")).stack }); } }; const musicianAccessOnJoin = musicSession.musician_access; const shouldVerifyNetwork = musicSession.musician_access; let clientRole = CLIENT_ROLE.PARENT; if (context.jamClientAdapter.getClientParentChildRole != null) { clientRole = await context.jamClientAdapter.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(); return; } try { await this.gearUtils.guardAgainstInvalidConfiguration(this.app, shouldVerifyNetwork); } catch (e) { pushJoinAbort("guardAgainstInvalidConfiguration", {error: String(e)}); SessionActions.leaveSession.trigger({location: '/client#/home'}); return; } const result = await this.sessionUtils.SessionPageEnter(); try { await this.gearUtils.guardAgainstActiveProfileMissing(this.app, result); } catch (data) { const leaveBehavior = {}; if (data && (data.reason === 'handled')) { if (data.nav === 'BACK') { leaveBehavior.location = -1; } else { leaveBehavior.location = data.nav; } } else { leaveBehavior.location = '/client#/home'; } pushJoinAbort("guardAgainstActiveProfileMissing", data); SessionActions.leaveSession.trigger(leaveBehavior); return; } try { this.userTracks = await this.waitForSessionPageEnterDone(); } catch (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); } pushJoinAbort("waitForSessionPageEnterDone", {error: data}); SessionActions.leaveSession.trigger({location: '/client#/home'}); return; } try { await this.ensureAppropriateProfile(musicianAccessOnJoin); logger.debug("user has passed all session guards"); await this.joinSession(); } catch (result2) { if (!result2 || !result2.controlled_location) { SessionActions.leaveSession.trigger({location: "/client#/home"}); } } }, async waitForSessionPageEnterDone() { this.sessionPageEnterDeferred = $.Deferred(); // see if we already have tracks; if so, we need to run with these const inputTracks = await context.JK.TrackHelpers.getUserTracks(context.jamClientAdapter); const isNoInputProfile = await this.gearUtils.isNoInputProfile(); logger.debug("isNoInputProfile", isNoInputProfile); if ((inputTracks.length > 0) || isNoInputProfile) { logger.debug("on page enter, tracks are already available"); this.sessionPageEnterDeferred.resolve(inputTracks); const deferred = this.sessionPageEnterDeferred; this.sessionPageEnterDeferred = null; return await deferred; } this.sessionPageEnterTimeout = setTimeout(()=> { if (this.sessionPageEnterTimeout) { if (this.sessionPageEnterDeferred) { this.sessionPageEnterDeferred.reject('timeout'); this.sessionPageEnterDeferred = null; } return this.sessionPageEnterTimeout = null; } } , 5000); return await this.sessionPageEnterDeferred; }, ensureAppropriateProfile(musicianAccess) { let deferred = new $.Deferred(); if (musicianAccess) { deferred = context.JK.guardAgainstSinglePlayerProfile(this.app); } else { deferred.resolve(); } return deferred; }, openBrowserToPayment() { return 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'; }, async joinSession() { await context.jamClientAdapter.SessionRegisterCallback("JK.HandleBridgeCallback2"); await context.jamClientAdapter.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); await context.jamClientAdapter.SessionSetConnectionStatusRefreshRate(1000); let clientRole = await context.jamClientAdapter.getClientParentChildRole(); const parentClientId = await context.jamClientAdapter.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 const expectedLatency = await context.jamClientAdapter.FTUEGetExpectedLatency(); this.joinDeferred = rest.joinSession({ 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: expectedLatency.latency }); try { const response = await this.joinDeferred; 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); return; } 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.jamClientAdapter.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}}); } await this.handleAutoOpenJamTrack(); this.watchBackendStats(); ConfigureTracksActions.reset(true); return await this.delayEnableVst(); } catch (xhr) { let leaveBehavior; this.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." } }; return 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." } }; return 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}); return 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}); return SessionActions.leaveSession.trigger(leaveBehavior); } else { return this.app.notifyServerError(xhr, 'Unable to Join Session'); } } else { return this.app.notifyServerError(xhr, 'Unable to Join Session'); } } }, delayEnableVst() { if (this.enableVstTimeout != null) { clearTimeout(this.enableVstTimeout); this.enableVstTimeout = null; } const isVstLoaded = context.jamClientAdapter.IsVstLoaded(); const hasVstAssignment = context.jamClientAdapter.hasVstAssignment(); if (hasVstAssignment && !isVstLoaded) { this.enableVstTimeout = setTimeout((() => { return this.enableVst(); } ), 5000); return this.issueChange(); } }, enableVst() { this.enableVstTimeout = null; if (this.inSession()) { ConfigureTracksActions.enableVst(); } else { logger.debug("no longer in session; not enabling VSTs at this time"); } return this.issueChange(); }, watchBackendStats() { return this.backendStatsInterval = window.setInterval((() => (this.updateBackendStats())), 1000); }, updateBackendStats() { const connectionStats = window.jamClientAdapter.getConnectionDetail('', false); const parentConnectionStats = window.jamClientAdapter.getConnectionDetail('', true); //console.log("CONNECTION STATES", connectionStats) //console.log("PARENT STATES", parentConnectionStats) return SessionStatsActions.pushStats(connectionStats, parentConnectionStats); }, trackChanges(header, payload) { if (this.currentTrackChanges < payload.track_changes_counter) { // we don't have the latest info. try and go get it logger.debug("track_changes_counter = stale. refreshing..."); return this.refreshCurrentSession(); } else { if (header.type !== 'HEARTBEAT_ACK') { // don't log if HEARTBEAT_ACK, or you will see this log all the time return logger.info("track_changes_counter = fresh. skipping refresh...", header, payload); } } }, handleAutoOpenJamTrack() { const jamTrack = this.sessionUtils.grabAutoOpenJamTrack(); if (jamTrack) { // give the session to settle just a little (call a timeout of 1 second) return setTimeout(()=> { // tell the server we are about to open a jamtrack return 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); return JamTrackActions.open(jamTrack); }) .fail(jqXHR => { return this.app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback"); }); } , 1000); } }, inSession() { return !!this.currentSessionId; }, alreadyInSession() { let inSession = false; return (() => { const result = []; for (var participant of Array.from(this.participants())) { if (participant.user.id === context.JK.currentUserId) { inSession = true; break; } else { result.push(undefined); } } return result; })(); }, participants() { if (this.currentSession) { return this.currentSession.participants; } else { return []; } }, refreshCurrentSession(force) { if (force) { logger.debug("refreshCurrentSession(force=true)"); } return this.refreshCurrentSessionRest(force); }, refreshCurrentSessionRest(force) { if (!this.inSession()) { logger.debug("refreshCurrentSession skipped: "); return; } if (this.requestingSessionRefresh) { // if someone asks for a refresh while one is going on, we ask for another to queue up logger.debug("queueing refresh"); return this.pendingSessionRefresh = true; } else { this.requestingSessionRefresh = true; return rest.getSession(this.currentSessionId) .done(response => { try { return this.updateSessionInfo(response, force); } catch (e) { return logger.error("unable to updateSessionInfo in session refresh", e); } //setTimeout(() => // @updateSessionInfo(response, force) //, 5000) }) .fail(jqXHR => { if (jqXHR.status !== 404) { return this.app.notifyServerError(jqXHR, "Unable to refresh session data"); } else { return logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone"); } }) .always(() => { this.requestingSessionRefresh = false; if (this.pendingSessionRefresh) { // and when the request is done, if we have a pending, fire it off again this.pendingSessionRefresh = false; return this.refreshCurrentSessionRest(force); } }); } }, onUpdateSession(session) { return this.updateSessionInfo(session, true); }, updateSessionInfo(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); logger.debug('update current session'); return 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); } }, leaveSessionRest() { if (context.JK && context.JK.DebugLogCollector && context.JK.DebugLogCollector.push) { context.JK.DebugLogCollector.push("leave-source.session-store.modern-leaveSessionRest", { current_session_id: this.currentSessionId, client_id: this.app && this.app.clientId, stack: (new Error("SessionStoreModern.leaveSessionRest")).stack }); } return rest.deleteParticipant(this.app.clientId); }, sendClientParticipantChanges(oldSession, newSession) { let client_id, i, participant, v; const joins = []; const leaves = []; const leaveJoins = []; // Will hold JamClientParticipants const oldParticipants = []; // will be set to session.participants if session const oldParticipantIds = {}; const newParticipants = []; const newParticipantIds = {}; if (oldSession && oldSession.participants) { for (var oldParticipant of Array.from(oldSession.participants)) { oldParticipantIds[oldParticipant.client_id] = oldParticipant; } } if (newSession && newSession.participants) { for (var newParticipant of Array.from(newSession.participants)) { newParticipantIds[newParticipant.client_id] = newParticipant; } } for (client_id in newParticipantIds) { // grow the 'all participants seen' list participant = newParticipantIds[client_id]; if (!(client_id in this.participantsEverSeen)) { this.participantsEverSeen[client_id] = participant; } if (client_id in oldParticipantIds) { // if the participant is here now, and here before, there is still a chance we missed a // very fast leave/join. So check if joined_session_at is different if (oldParticipantIds[client_id].joined_session_at !== participant.joined_session_at) { leaveJoins.push(participant); } } else { // new participant id that's not in old participant ids: Join joins.push(participant); } } for (client_id in oldParticipantIds) { participant = oldParticipantIds[client_id]; if (!(client_id in newParticipantIds)) { // old participant id that's not in new participant ids: Leave leaves.push(participant); } } for (i in joins) { v = joins[i]; if (v.client_id !== this.app.clientId) { this.participantJoined(newSession, v); } } for (i in leaves) { v = leaves[i]; if (v.client_id !== this.app.clientId) { this.participantLeft(newSession, v); } } return (() => { const result = []; for (i in leaveJoins) { v = leaveJoins[i]; if (v.client_id !== this.app.clientId) { logger.debug("participant had a rapid leave/join"); this.participantLeft(newSession, v); result.push(this.participantJoined(newSession, v)); } else { result.push(undefined); } } return result; })(); }, participantJoined(newSession, participant) { logger.debug("jamClient.ParticipantJoined", participant.client_id); context.jamClientAdapter.ParticipantJoined(newSession, this.toJamClientParticipant(participant)); return this.currentParticipants[participant.client_id] = {server: participant, client: {audio_established: null}}; }, participantLeft(newSession, participant) { logger.debug("jamClient.ParticipantLeft", participant.client_id); context.jamClientAdapter.ParticipantLeft(newSession, this.toJamClientParticipant(participant)); return delete this.currentParticipants[participant.client_id]; }, toJamClientParticipant(participant) { return { 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() { return logger.debug("recording registration not hooked up yet"); }, 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.jamClientAdapter.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.jamClientAdapter.UpdateSessionInfo != null) { if (this.currentSession != null) { context.jamClientAdapter.UpdateSessionInfo(this.currentSession); } else { context.jamClientAdapter.UpdateSessionInfo({}); } } //logger.debug("session changed") logger.debug("issue change"); return this.issueChange(); }, ensureConnected() { if (!context.JK.JamServer.connected) { const 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); } return 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 (context.JK && context.JK.DebugLogCollector && context.JK.DebugLogCollector.push) { context.JK.DebugLogCollector.push("leave-source.session-store.modern-onLeaveSession", { behavior: behavior, current_session_id: this.currentSessionId, stack: (new Error("SessionStoreModern.onLeaveSession")).stack }); } 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(); } this.leaveSession(); return this.sessionUtils.SessionPageLeave(); }, leaveSession() { if ((this.joinDeferred == null) || ((this.joinDeferred != null ? this.joinDeferred.state() : undefined) === 'resolved')) { const deferred = new $.Deferred(); return this.recordingModel.stopRecordingIfNeeded() .always(()=> { return this.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=" + this.app.clientId); context.jamClientAdapter.LeaveSession({ sessionID: this.currentSessionId }); this.leaveSessionRest(this.currentSessionId) .done(function() { return deferred.resolve(arguments[0], arguments[1], arguments[2]);}.bind(this)) .fail(function() { return deferred.reject(arguments[0], arguments[1], arguments[2]); }.bind(this)); // 'unregister' for callbacks context.jamClientAdapter.SessionRegisterCallback(""); //context.jamClientAdapter.SessionSetAlertCallback(""); context.jamClientAdapter.SessionSetConnectionStatusRefreshRate(0); this.sessionEnded(); return this.issueChange(); }, selfOpenedJamTracks() { return this.currentSession && (this.currentSession.jam_track_initiator_id === context.JK.currentUserId); }, sessionEnded(onJoin) { // cleanup context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, this.trackChanges); context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, this.trackChanges); context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, this.trackChanges); context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, this.trackChanges); if (this.sessionPageEnterDeferred != null) { this.sessionPageEnterDeferred.reject('session_over'); this.sessionPageEnterDeferred = null; } if (this.backendMixerAlertThrottleTimer) { clearTimeout(this.backendMixerAlertThrottleTimer); this.backendMixerAlertThrottleTimer = null; } this.userTracks = null; this.startTime = null; if (this.backendStatsInterval != null) { window.clearInterval(this.backendStatsInterval); this.backendStatsInterval = null; } if ((this.joinDeferred != null ? this.joinDeferred.state() : undefined) === 'resolved') { $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: this.currentSessionId}}); } this.currentTrackChanges = 0; this.currentSession = null; this.joinDeferred = null; this.isRecording = false; this.currentSessionId = null; this.sessionRules = null; this.subscriptionRules = null; this.currentParticipants = {}; this.previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []}; this.openBackingTrack = null; this.shownAudioMediaMixerHelp = false; this.controlsLockedForJamTrackRecording = false; this.openBackingTrack = null; this.downloadingJamTrack = false; if (!onJoin) { this.sessionUtils.setAutoOpenJamTrack(null); } JamTrackActions.close(); NotificationActions.sessionEnded(); return $(context.AppStore).triggerHandler('SessionEnded'); }, id() { return this.currentSessionId; }, canRecord() { if (this.subscriptionRules != null) { console.log("can record? rules:", this.subscriptionRules.can_record_audio ); return this.subscriptionRules.can_record_audio; } else { console.log("can record? no rules; allow"); return true; } }, canVideo() { if (this.subscriptionRules != null) { console.log("can video? rules:", this.subscriptionRules.can_use_video); return this.subscriptionRules.can_use_video; } else { console.log("can video? no rules; allow"); return true; } }, getCurrentOrLastSession() { return this.currentOrLastSession; }, handleJoinLeaveRequestCallback(data) { let sessionId; const op = data["op"]; logger.debug(`client asks ${op} for ${data["id"]}`); if (op === "join") { sessionId = data["id"]; if (sessionId !== this.currentSessionId) { return window.location = "/client#/session/" + sessionId; } else { return logger.debug(`dropped ${op} because sessionId ${sessionId} matches currentSessionId`); } } else if (op === "leave") { sessionId = data["id"]; if (sessionId === this.currentSessionId) { return this.onLeaveSession({location: '/client#/home'}); } else { return logger.debug(`dropped ${op} because sessionId ${sessionId} does not match currentSessionId ${this.currentSessionId}`); } } } } ); } else if (context.JK.DebugLogCollector && context.JK.DebugLogCollector.push) { context.JK.DebugLogCollector.push("join-source.session-store.modern-skipped", { reason: "legacy-bundle" }); }