1693 lines
60 KiB
JavaScript
1693 lines
60 KiB
JavaScript
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", $('<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."
|
|
}
|
|
};
|
|
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"
|
|
});
|
|
}
|