From 5eceb287c0e7ffca21c7d86cc401890fb8ac6dbb Mon Sep 17 00:00:00 2001 From: Nuwan Date: Fri, 10 Oct 2025 12:42:26 +0530 Subject: [PATCH] wip session screen --- .../src/components/client/JKSessionScreen.js | 516 +++++++++--------- jam-ui/src/context/CurrentSessionContext.js | 19 +- jam-ui/src/context/JamClientContext.js | 33 +- jam-ui/src/context/JamKazamAppContext.js | 14 + jam-ui/src/fakeJamClient.js | 8 + jam-ui/src/fakeJamClientMessages.js | 71 +++ jam-ui/src/fakeJamClientProxy.js | 7 +- jam-ui/src/fakeJamClientRecordings.js | 215 ++++++++ jam-ui/src/helpers/MessageFactory.js | 2 - jam-ui/src/helpers/rest.js | 44 +- jam-ui/src/hooks/useGearUtils.js | 60 +- jam-ui/src/hooks/useMixerHelpers.js | 0 jam-ui/src/hooks/useRecordingHelpers.js | 436 +++++++++++++++ jam-ui/src/hooks/useSessionEnter.js | 44 +- jam-ui/src/hooks/useTrackHelpers.js | 103 ++++ jam-ui/src/jamClientProxy.js | 5 + jam-ui/src/layouts/JKClientLayout.js | 17 +- web/app/assets/javascripts/asyncJamClient.js | 6 +- web/app/assets/javascripts/sessionModel.js | 40 +- 19 files changed, 1252 insertions(+), 388 deletions(-) create mode 100644 jam-ui/src/context/JamKazamAppContext.js create mode 100644 jam-ui/src/fakeJamClientMessages.js create mode 100644 jam-ui/src/fakeJamClientRecordings.js create mode 100644 jam-ui/src/hooks/useMixerHelpers.js create mode 100644 jam-ui/src/hooks/useRecordingHelpers.js create mode 100644 jam-ui/src/hooks/useTrackHelpers.js diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js index 102f84540..06898c3b5 100644 --- a/jam-ui/src/components/client/JKSessionScreen.js +++ b/jam-ui/src/components/client/JKSessionScreen.js @@ -8,17 +8,19 @@ import useGearUtils from '../../hooks/useGearUtils' import useSessionUtils from '../../hooks/useSessionUtils.js'; import useSessionEnter from '../../hooks/useSessionEnter.js' import useSessionLeave from '../../hooks/useSessionLeave.js'; - +import useRecordingHelpers from '../../hooks/useRecordingHelpers.js'; import { useCurrentSession } from '../../context/CurrentSessionContext.js'; +import { useJamKazamApp } from '../../context/JamKazamAppContext.js'; -import { getSessionHistory, getSession } from '../../helpers/rest'; +import { getSessionHistory, getSession, joinSession as joinSessionRest } from '../../helpers/rest'; import { useParams } from 'react-router-dom'; import { CLIENT_ROLE } from '../../helpers/globals'; import { MessageType } from '../../helpers/MessageFactory.js'; const JKSessionScreen = () => { - const logger = console; // Replace with your logging mechanism if needed + const logger = console; // Replace with another logging mechanism if needed + const app = useJamKazamApp(); const { isConnected, connectionStatus, @@ -41,97 +43,221 @@ const JKSessionScreen = () => { const { performLeaveSession, leaveSessionRest } = useSessionLeave(); - const { currentSession, setCurrentSession, inSession } = useCurrentSession(); + const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSession(); - //get session ID from URL params - const { id: sessionId } = useParams(); + const { getCurrentRecordingState, reset: resetRecordingState } = useRecordingHelpers(); + + const { id: sessionId } = useParams(); // State to hold session data const [userTracks, setUserTracks] = useState([]); - const [sessionState, setSessionState] = useState({}); const [showConnectionAlert, setShowConnectionAlert] = useState(false); const [hasJoined, setHasJoined] = useState(false); const [requestingSessionRefresh, setRequestingSessionRefresh] = useState(false); const [pendingSessionRefresh, setPendingSessionRefresh] = useState(false); + const [sessionRules, setSessionRules] = useState(null); + const [subscriptionRules, setSubscriptionRules] = useState(null); + const [currentOrLastSession, setCurrentOrLastSession] = useState(null); useEffect(() => { if (!isConnected || !jamClient) return; - guardJoinSession(); + guardOnJoinSession(); }, [isConnected]); - const guardJoinSession = async () => { + const guardOnJoinSession = async () => { try { - const musicSession = await getSessionHistory(sessionId); - if (musicSession) { - const musicianAccessOnJoin = musicSession.musician_access; - const shouldVerifyNetwork = musicSession.musician_access; - const clientRole = await jamClient.getClientParentChildRole(); + + const musicSessionResp = await getSessionHistory(sessionId); + const musicSession = await musicSessionResp.json(); + logger.log("fetched session history: ", musicSession); + setCurrentSessionId(musicSession.id); // use the ref setter to set the current session ID - //store current session in context - setCurrentSession(prev => ({ ...prev, id: musicSession.id })); + const musicianAccessOnJoin = musicSession.musician_access; + const shouldVerifyNetwork = musicSession.musician_access; + const clientRole = await jamClient.getClientParentChildRole(); - if (clientRole === CLIENT_ROLE.CHILD) { - logger.debug("client is configured to act as child. skipping all checks. assuming 0 tracks"); - setUserTracks([]); - await joinSession(); - } + logger.log("musicianAccessOnJoin when joining session: " + musicianAccessOnJoin); + logger.log("clientRole when joining session: " + clientRole); + logger.log("currentSessionId when joining session: " + currentSessionIdRef.current); + + if (clientRole === CLIENT_ROLE.CHILD) { + logger.debug("client is configured to act as child. skipping all checks. assuming 0 tracks"); + setUserTracks([]); + + //skipping all checks. assuming 0 tracks + await joinSession(); + return; + } + + try { + await guardAgainstInvalidConfiguration(app, shouldVerifyNetwork); + const result = await SessionPageEnter(); + logger.log("SessionPageEnter result: ", result); try { - await guardAgainstInvalidConfiguration({}, shouldVerifyNetwork); // TODO: provide proper app object - const result = await SessionPageEnter(); - + await guardAgainstActiveProfileMissing(app, result); + logger.log("user has an active profile"); try { - await guardAgainstActiveProfileMissing({}, result); // TODO: provide proper app object - + const tracks = await waitForSessionPageEnterDone(); + setUserTracks(tracks); + logger.log("userTracks: ", tracks); try { - const tracks = await waitForSessionPageEnterDone(); - setUserTracks(tracks); + await ensureAppropriateProfile(musicianAccessOnJoin) + logger.log("user has passed all session guards") + + // all checks passed; join the session + await joinSession(); - try { - await ensureAppropriateProfile(musicianAccessOnJoin) - logger.debug("user has passed all session guards") - await joinSession() - } catch (error) { - if (!error.controlled_location) { - //SessionActions.leaveSession.trigger({ location: "/client#/home" }); - //TODO: show some error and redirect to home - } - } } catch (error) { - if (error === "timeout") { - //context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.'); - //TODO: show some error - } else if (error === 'session_over') { - // do nothing; session ended before we got the user track info. just bail - logger.debug("session is over; bailing"); - } else { - //context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + error); - //TODO: show some error + logger.error("User profile is not appropriate for session:", error); + if (!error.controlled_location) { } - - //SessionActions.leaveSession.trigger({ location: '/client#/home' }); - await performLeaveSession(); //TODO: handle redirection } } catch (error) { - // Active profile is missing, redirect to home or if the error has a location, redirect there + logger.error("Error: waiting for session page enter to complete:", error); + if (error === "timeout") { + //TODO: show some error + } else if (error === 'session_over') { + // do nothing; session ended before we got the user track info. just bail + logger.debug("Error:: session is over; bailing"); + } else { + //TODO: show some error + } + + await performLeaveSession(); //TODO: handle redirection } } catch (error) { - //SessionActions.leaveSession.trigger({ location: '/client#/home' }); - // Invalid configuration, redirect to home - await performLeaveSession(); //TODO: handle redirection + // Active profile is missing, redirect to home or if the error has a location, redirect there + logger.error("Error: Active profile is missing or invalid:", error); } - } else { - console.error("Invalid session ID or unable to fetch session history"); - //TODO: Show some error + } catch (error) { + // Invalid configuration, redirect to home + await performLeaveSession(); //TODO: handle redirection + logger.error("Error: Invalid configuration:", error); } + } catch (error) { - console.error("Error fetching session history:", error); + logger.error("Error: Error fetching session history:", error); //TODO: Show some error } }; + const joinSession = async () => { + await jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); + // await jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); + + // await jamClient.SessionSetConnectionStatusRefreshRate(1000); + + // let clientRole = await jamClient.getClientParentChildRole(); + // const parentClientId = await jamClient.getParentClientId(); + + // 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(); //TODO: implement recording registration + + // // tell the server we want to join + + // const clientId = await jamClient.GetClientID(); + // logger.debug("joining session " + sessionId + " as client " + clientId + " with role " + clientRole + " and parent client " + parentClientId); + + // const latency = await jamClient.FTUEGetExpectedLatency().latency + + // logger.log("currentSession before join: ", currentSessionIdRef.current); + + // joinSessionRest({ + // client_id: clientId, + // ip_address: server.publicIP, + // as_musician: true, + // tracks: userTracks, + // session_id: currentSessionIdRef.current, + // client_role: clientRole, + // parent_client_id: parentClientId, + // audio_latency: latency, + // }).then(async (response) => { + // setHasJoined(true); + // if (!inSession()) { + // // the user has left the session before they got joined. We need to issue a leave again to the server to make sure they are out + // logger.debug("user left before fully joined to session. telling server again that they have left"); + // leaveSessionRest(currentSessionIdRef.current); + // } + + // updateSessionInfo(response, true); + + // // logger.debug("calling jamClient.JoinSession"); + + // //TODO: revist this logic later + // // // 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); + // // } + // // } + + // resetRecordingState(currentSessionIdRef.current); // reset recording state for this session + + // const joinSessionMsg = { + // sessionID: currentSessionIdRef.current, + // music_session_id_int: response.music_session_id_int + // }; + + // await jamClient.JoinSession(joinSessionMsg); + + // registerMessageCallback(MessageType.SESSION_JOIN, trackChanges); + // registerMessageCallback(MessageType.SESSION_DEPART, trackChanges); + // registerMessageCallback(MessageType.TRACKS_CHANGED, trackChanges); + // registerMessageCallback(MessageType.HEARTBEAT_ACK, trackChanges); + + // //TODO: revist the logic in following commented section + // //if (document) { $(document).trigger(EVENTS.SESSION_STARTED, { session: { id: this.currentSessionId, lesson_session: response.lesson_session } }); } + + // // this.handleAutoOpenJamTrack(); + + // // this.watchBackendStats(); + + // // ConfigureTracksActions.reset(true); + // // this.delayEnableVst(); + // // logger.debug("completed session join") + // }).catch(async (xhr) => { + // logger.error("Failed to join session:", xhr); + // let leaveBehavior; + // await updateCurrentSession(null); + + // if (xhr.status === 404) { + // // we tried to join the session, but it is already gone. kick user back to join session screen + + // } else if (xhr.status === 422) { // unprocessable entity - something was wrong with the join request + // const response = JSON.parse(xhr.responseText); + + // if (response["errors"] && response["errors"]["tracks"] && (response["errors"]["tracks"][0] === "Please select at least one track")) { // No Inputs Configured + + // } else if (response["errors"] && response["errors"]["music_session"] && (response["errors"]["music_session"][0] == ["is currently recording"])) { //The session is currently recording + // } else if (response["errors"] && response["errors"]["remaining_session_play_time"]) { // No Remaining Session Play Time + + // } else if (response["errors"] && response["errors"]["remaining_month_play_time"]) { // No Remaining Month Play Time + + // } else { // Unknown error. Unable to Join Session + + // } + // } else { // Unknown error. Unable to Join Session + + // } + // }) + + } + const ensureAppropriateProfile = async (musicianAccess) => { - const app = {}; // Placeholder for app object if needed return new Promise(async function (resolve, reject) { if (musicianAccess) { try { @@ -169,7 +295,7 @@ const JKSessionScreen = () => { refreshCurrentSessionRest(force); }; - + const refreshCurrentSessionRest = async (force) => { if (!inSession()) { logger.debug("refreshCurrentSession skipped: "); @@ -197,205 +323,82 @@ const JKSessionScreen = () => { } }; - // updateSessionInfo: `function (session, force) { - // if ((force === true) || (this.currentTrackChanges < session.track_changes_counter)) { - // logger.debug("updating current track changes from %o to %o", this.currentTrackChanges, session.track_changes_counter); - // this.currentTrackChanges = session.track_changes_counter; - // this.sendClientParticipantChanges(this.currentSession, session); - - // this.recordingModel.getCurrentRecordingState().then((recordingState) => { - // session = { ...session, ...recordingState }; - - // logger.debug('update current session'); - - // }).finally(() => { - // //console.log("_DEBUG_* SessionStore#updateSessionInfo sessionState", session); - // this.updateCurrentSession(session); - // }); - - // } else { - // return logger.info("ignoring refresh because we already have current: " + this.currentTrackChanges + ", seen: " + session.track_changes_counter); - // } - // }` - const updateSessionInfo = (session, force) => { if ((force === true) || (currentSession.track_changes_counter < session.track_changes_counter)) { logger.debug("updating current track changes from %o to %o", currentSession.track_changes_counter, session.track_changes_counter); + + //TODO: revisit this logic //this.currentTrackChanges = session.track_changes_counter; //this.sendClientParticipantChanges(this.currentSession, session); - setCurrentSession(prev => ({ ...prev, ...session })); - //TODO: handle recording state + getCurrentRecordingState().then((recordingState) => { + session = { ...session, ...recordingState }; + }).finally(async () => { + logger.log("_DEBUG_* JKSessionScreen#updateSessionInfo sessionState", session); + await updateCurrentSession(session); + }); + } else { return logger.info("ignoring refresh because we already have current: " + currentSession.track_changes_counter + ", seen: " + session.track_changes_counter); } }; - const joinSession = async () => { - await jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); - await jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); + const updateCurrentSession = async (sessionData) => { + //logger.log("_DEBUG_* SessionStore#updateCurrentSession", sessionData) + if (sessionData !== null) { + setCurrentOrLastSession(sessionData); - await jamClient.SessionSetConnectionStatusRefreshRate(1000); + if (sessionData.session_rules) { + setSessionRules(sessionData.session_rules); + // TESTING: + //@sessionRules.remaining_session_play_time = 60 * 15 + 15 # 15 minutes and 15 seconds - let clientRole = await jamClient.getClientParentChildRole(); - const parentClientId = await jamClient.getParentClientId(); - console.debug('role when joining session: ' + clientRole + ', parent client id ' + parentClientId); - - 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(); //TODO: implement recording registration - - // tell the server we want to join - - const clientId = await jamClient.GetClientID(); - console.debug("joining session " + sessionId + " as client " + clientId + " with role " + clientRole + " and parent client " + parentClientId); - - const latency = await jamClient.FTUEGetExpectedLatency().latency - - joinSession({ - client_id: clientId, - ip_address: server.publicIP, - as_musician: true, - tracks: userTracks, - session_id: sessionId, - client_role: clientRole, - parent_client_id: parentClientId, - audio_latency: latency, - }).then(async (response) => { - setHasJoined(true); - if (!inSession()) { - // the user has left the session before they got joined. We need to issue a leave again to the server to make sure they are out - logger.debug("user left before fully joined to session. telling server again that they have left"); - leaveSessionRest(currentSession.id); + // compute timestamp due time + if (sessionRules && sessionRules.remaining_session_play_time != null) { + let until_time = new Date(); + until_time = new Date(until_time.getTime() + (sessionRules.remaining_session_play_time * 1000)); + logger.log("subscription: session has remaining play time", until_time); + setSessionRules(prev => ({ ...prev, remaining_session_until: until_time })); + } } - this.updateSessionInfo(response, true); - setCurrentSession(prev => ({ ...prev, ...response })); + if (sessionData.subscription) { + // for the backend - it looks here + //sessionData.subscription = sessionData.subscription_rules + // let the backend know + //context.jamClient.applySubscriptionPolicy() + setSubscriptionRules(sessionData.subscription); + // TESTING: + //@subscriptionRules.remaining_month_play_time = 60 * 15 + 15 # 15 minutes and 15 seconds - logger.debug("calling jamClient.JoinSession"); + if (subscriptionRules && subscriptionRules.remaining_month_play_time != null) { + let until_time = new Date(); + until_time = new Date(until_time.getTime() + (subscriptionRules.remaining_month_play_time * 1000)); + //until_time.setSeconds(until_time.getSeconds() + @subscriptionRules.remaining_month_play_time) + logger.log("subscription: month has remaining play time", until_time); + setSubscriptionRules(prev => ({ ...prev, remaining_month_until: until_time })); + } + } + } - //TODO: revist this logic later - // // 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); - // } - // } + logger.log("*_DEBUG_* JKSessionScreen#updateCurrentSession currentSession", sessionData); + setCurrentSession(sessionData); + //if (jamClient.UpdateSessionInfo != null) { + if (sessionData != null) { + await jamClient.UpdateSessionInfo(sessionData); + } else { + await jamClient.UpdateSessionInfo({}); + } + //} + //logger.debug("session changed") - // this.recordingModel.reset(this.currentSessionId); //TODO: implement recording model + logger.debug("session updated"); + // @issueChange() equivalent - since it's React, state updates will trigger re-render + }; - const joinSessionMsg = { - sessionID: currentSession.id, - music_session_id_int: response.music_session_id_int - }; + - await jamClient.JoinSession(joinSessionMsg); - - //@refreshCurrentSession(true); - - registerMessageCallback(MessageType.SESSION_JOIN, trackChanges); - registerMessageCallback(MessageType.SESSION_DEPART, trackChanges); - registerMessageCallback(MessageType.TRACKS_CHANGED, trackChanges); - registerMessageCallback(MessageType.HEARTBEAT_ACK, trackChanges); - - //TODO: revist the logic in following commented section - //if (document) { $(document).trigger(EVENTS.SESSION_STARTED, { session: { id: this.currentSessionId, lesson_session: response.lesson_session } }); } - - // this.handleAutoOpenJamTrack(); - - // this.watchBackendStats(); - - // ConfigureTracksActions.reset(true); - // this.delayEnableVst(); - // logger.debug("completed session join") - }).catch((xhr) => { - let leaveBehavior; - this.updateCurrentSession(null); - - // // if (xhr.status === 404) { - // // // we tried to join the session, but it is already gone. kick user back to join session screen - // // leaveBehavior = { - // // location: "/client#/findSession", - // // notify: { - // // title: "Unable to Join Session", - // // text: " The session you attempted to join is over." - // // } - // // }; - // // SessionActions.leaveSession.trigger(leaveBehavior); - // // } else if (xhr.status === 422) { - // // let buttons; - // // const response = JSON.parse(xhr.responseText); - // // if (response["errors"] && response["errors"]["tracks"] && (response["errors"]["tracks"][0] === "Please select at least one track")) { - // // return this.app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')); - - // // } else if (response["errors"] && response["errors"]["music_session"] && (response["errors"]["music_session"][0] === ["is currently recording"])) { - // // leaveBehavior = { - // // location: "/client#/findSession", - // // notify: { - // // title: "Unable to Join Session", - // // text: "The session is currently recording." - // // } - // // }; - // // SessionActions.leaveSession.trigger(leaveBehavior); - // // } else if (response["errors"] && response["errors"]["remaining_session_play_time"]) { - // // leaveBehavior = - // // { location: "/client#/findSession" }; - // // buttons = []; - // // buttons.push({ name: 'CLOSE', buttonStyle: 'button-grey' }); - // // buttons.push({ name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (this.openBrowserToPlanComparison())) }); - // // buttons.push({ - // // name: 'UPGRADE PLAN', - // // buttonStyle: 'button-orange', - // // click: (() => (this.openBrowserToPayment())) - // // }); - // // context.JK.Banner.show({ - // // title: "Out of Time For This Session", - // // html: context._.template($('#template-no-remaining-session-play-time').html(), {}, { variable: 'data' }), - // // buttons - // // }); - // // SessionActions.leaveSession.trigger(leaveBehavior); - // // } else if (response["errors"] && response["errors"]["remaining_month_play_time"]) { - // // leaveBehavior = - // // { location: "/client#/findSession" }; - // // buttons = []; - // // buttons.push({ name: 'CLOSE', buttonStyle: 'button-grey' }); - // // buttons.push({ name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (this.openBrowserToPlanComparison())) }); - // // buttons.push({ - // // name: 'UPGRADE PLAN', - // // buttonStyle: 'button-orange', - // // click: (() => (this.openBrowserToPayment())) - // // }); - // // context.JK.Banner.show({ - // // title: "Out of Time for the Month", - // // html: context._.template($('#template-no-remaining-month-play-time').html(), {}, { variable: 'data' }), - // // buttons - // // }); - // // SessionActions.leaveSession.trigger(leaveBehavior); - // // } else { - // // this.app.notifyServerError(xhr, 'Unable to Join Session'); - // // } - // } else { - // this.app.notifyServerError(xhr, 'Unable to Join Session'); - // } - }) - - - - - } // useEffect(() => { // if (!isConnected) return; @@ -411,7 +414,7 @@ const JKSessionScreen = () => { // ...session // })); // } else { - // console.error("Invalid session ID or unable to fetch session"); + // logger.error("Invalid session ID or unable to fetch session"); // //TODO: Handle invalid session (e.g., redirect or show error) // } // }; @@ -428,25 +431,6 @@ const JKSessionScreen = () => { } }, [connectionStatus]); - // useEffect(() => { - // if (!isConnected) return; - - // // Initialize session callbacks - // jamClient.SessionRegisterCallback("HandleSessionCallback"); - // jamClient.RegisterRecordingCallbacks( - // "HandleRecordingStartResult", - // "HandleRecordingStopResult", - // "HandleRecordingStarted", - // "HandleRecordingStopped", - // "HandleRecordingAborted" - // ); - // jamClient.SessionSetConnectionStatusRefreshRate(1000); - // jamClient.RegisterVolChangeCallBack("HandleVolumeChangeCallback"); - // jamClient.setMetronomeOpenCallback("HandleMetronomeCallback"); - - // loadSessionData(); - - // }, [isConnected]); const loadSessionData = async () => { try { @@ -454,9 +438,9 @@ const JKSessionScreen = () => { const controlState = await jamClient.SessionGetAllControlState(true); const sampleRate = await jamClient.GetSampleRate(); - console.log('Session data loaded:', { audioConfigs, controlState, sampleRate }); + logger.log('Session data loaded:', { audioConfigs, controlState, sampleRate }); } catch (error) { - console.error('Error loading session data:', error); + logger.error('Error loading session data:', error); } }; @@ -482,34 +466,34 @@ const JKSessionScreen = () => { // // Callback handlers (these would be implemented to handle WebSocket responses) // const HandleSessionCallback = (data) => { - // console.log('Session callback:', data); + // logger.log('Session callback:', data); // // Handle session events // }; // const HandleRecordingStarted = (data) => { - // console.log('Recording started:', data); + // logger.log('Recording started:', data); // // Update recording state // }; // const HandleRecordingStopped = (data) => { - // console.log('Recording stopped:', data); + // logger.log('Recording stopped:', data); // // Update recording state // }; // const HandleVolumeChangeCallback = (mixerId, isLeft, value, isMuted) => { - // console.log('Volume changed:', { mixerId, isLeft, value, isMuted }); + // logger.log('Volume changed:', { mixerId, isLeft, value, isMuted }); // // Update mixer state // }; // const HandleBridgeCallback = (vuData) => { - // console.log('Bridge callback:', vuData); + // logger.log('Bridge callback:', vuData); // // Handle VU meter updates // }; return ( {!isConnected &&
Connecting to backend...
} - + diff --git a/jam-ui/src/context/CurrentSessionContext.js b/jam-ui/src/context/CurrentSessionContext.js index 5aa4af1a1..26f3668b1 100644 --- a/jam-ui/src/context/CurrentSessionContext.js +++ b/jam-ui/src/context/CurrentSessionContext.js @@ -1,16 +1,29 @@ -import React, { createContext, useContext, useState } from 'react'; +import React, { createContext, useContext, useState, useRef } from 'react'; const CurrentSessionContext = createContext(null); export const CurrentSessionProvider = ({ children }) => { const [currentSession, setCurrentSession] = useState({}); + const currentSessionIdRef = useRef(null); const inSession = () => { - return currentSession && currentSession.id; + return currentSessionIdRef.current !== null; + }; + + const setCurrentSessionId = (id) => { + console.log("Setting current session ID to: ", id); + currentSessionIdRef.current = id; }; return ( - + {children} ); diff --git a/jam-ui/src/context/JamClientContext.js b/jam-ui/src/context/JamClientContext.js index 844f5d5b3..d058d22bf 100644 --- a/jam-ui/src/context/JamClientContext.js +++ b/jam-ui/src/context/JamClientContext.js @@ -1,15 +1,37 @@ import React, { createContext, useContext, useRef } from 'react'; -// Adjust the import path as necessary. fakeJamClientProxy.js vs jamClientProxy.js -import { FakeJamClientProxy as JamClientProxy } from '../fakeJamClientProxy'; -//import JamClientProxy from '../jamClientProxy'; +import JamClientProxy from '../jamClientProxy'; + +import { FakeJamClientProxy } from '../fakeJamClientProxy'; +import { FakeJamClientRecordings } from '../fakeJamClientRecordings'; +import { FakeJamClientMessages } from '../fakeJamClientMessages'; +import { useJamKazamApp } from './JamKazamAppContext'; const JamClientContext = createContext(null); export const JamClientProvider = ({ children }) => { //assign an instance of JamClientProxy to a ref so that it persists across renders + //if development environment, use FakeJamClientProxy + //otherwise use JamClientProxy + //initialize the proxy when the provider is mounted + //and provide it to the context value + + //get the app instance from JamKazamAppContext + const app = useJamKazamApp(); + const proxyRef = useRef(null); - const proxy = new JamClientProxy(null, console); // Pass appropriate parameters - proxyRef.current = proxy.init(); + if (process.env.NODE_ENV === 'development') { + const fakeJamClientMessages = new FakeJamClientMessages(); + const proxy = new FakeJamClientProxy(app, fakeJamClientMessages); // Pass appropriate parameters + proxyRef.current = proxy.init(); + // For testing purposes, we can add some fake recordings + const fakeJamClientRecordings = new FakeJamClientRecordings(app, proxyRef.current, fakeJamClientMessages); + proxyRef.current.SetFakeRecordingImpl(fakeJamClientRecordings); + } else { + if (!proxyRef.current) { + const proxy = new JamClientProxy(app, console); + proxyRef.current = proxy.init(); + } + } return ( {children} @@ -18,4 +40,3 @@ export const JamClientProvider = ({ children }) => { }; export const useJamClient = () => useContext(JamClientContext); - \ No newline at end of file diff --git a/jam-ui/src/context/JamKazamAppContext.js b/jam-ui/src/context/JamKazamAppContext.js new file mode 100644 index 000000000..e89c326bc --- /dev/null +++ b/jam-ui/src/context/JamKazamAppContext.js @@ -0,0 +1,14 @@ +import React, { createContext, useContext, useRef } from 'react'; + +export const JamKazamAppContext = createContext(null); + +export const JamKazamAppProvider = ({ children }) => { + const value = {}; // Add your context value here + return ( + + {children} + + ); +}; + +export const useJamKazamApp = () => useContext(JamKazamAppContext); diff --git a/jam-ui/src/fakeJamClient.js b/jam-ui/src/fakeJamClient.js index 5baf48ad4..bbf3d22f9 100644 --- a/jam-ui/src/fakeJamClient.js +++ b/jam-ui/src/fakeJamClient.js @@ -845,6 +845,7 @@ export class FakeJamClient { } SessionRegisterCallback(callbackName) { + console.log("SessionRegisterCallback: " + callbackName); this.eventCallbackName = callbackName; if (this.callbackTimer) { window.clearInterval(this.callbackTimer); @@ -926,6 +927,7 @@ export class FakeJamClient { SessionSetUserName(client_id, name) {} doCallbacks() { + console.log("[fakeJamClient] doCallbacks - " + this.vuValue); const names = ["vu"]; const ids = ["i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1", "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~2"]; @@ -1239,6 +1241,7 @@ export class FakeJamClient { } LastUsedProfileName() { + console.log("FakeJamClient: LastUsedProfileName"); return 'default'; } @@ -1366,4 +1369,9 @@ export class FakeJamClient { listTrackAssignments() { return {}; } + + UpdateSessionInfo(info) { + this.logger.debug("FakeJamClient: UpdateSessionInfo: %o", info); + //this.sessionInfo = info; + } } diff --git a/jam-ui/src/fakeJamClientMessages.js b/jam-ui/src/fakeJamClientMessages.js new file mode 100644 index 000000000..73c403779 --- /dev/null +++ b/jam-ui/src/fakeJamClientMessages.js @@ -0,0 +1,71 @@ +// Fake Jam Client Messages - P2P message factory for recording operations +import { generateUUID } from './helpers/utils.js'; + +export class FakeJamClientMessages { + constructor() { + this.Types = { + START_RECORDING: 'start_recording', + START_RECORDING_ACK: 'start_recording_ack', + STOP_RECORDING: 'stop_recording', + STOP_RECORDING_ACK: 'stop_recording_ack', + ABORT_RECORDING: 'abort_recording' + }; + } + + startRecording(recordingId) { + const msg = { + type: this.Types.START_RECORDING, + msgId: generateUUID(), + recordingId + }; + return msg; + } + + startRecordingAck(recordingId, success, reason, detail) { + const msg = { + type: this.Types.START_RECORDING_ACK, + msgId: generateUUID(), + recordingId, + success, + reason, + detail + }; + return msg; + } + + stopRecording(recordingId, success = true, reason, detail) { + const msg = { + type: this.Types.STOP_RECORDING, + msgId: generateUUID(), + recordingId, + success, + reason, + detail + }; + return msg; + } + + stopRecordingAck(recordingId, success, reason, detail) { + const msg = { + type: this.Types.STOP_RECORDING_ACK, + msgId: generateUUID(), + recordingId, + success, + reason, + detail + }; + return msg; + } + + abortRecording(recordingId, reason, detail) { + const msg = { + type: this.Types.ABORT_RECORDING, + msgId: generateUUID(), + recordingId, + success: false, + reason, + detail + }; + return msg; + } +} diff --git a/jam-ui/src/fakeJamClientProxy.js b/jam-ui/src/fakeJamClientProxy.js index b5a69a893..62491aa59 100644 --- a/jam-ui/src/fakeJamClientProxy.js +++ b/jam-ui/src/fakeJamClientProxy.js @@ -10,17 +10,18 @@ export class FakeJamClientProxy { return function (...args) { return new Promise((resolve, reject) => { try { - console.log('[fakeJamClient]', target, prop, args); + if(target[prop]){ + console.log('[FakeJamClientProxy]', prop, args); const result = target[prop].apply(target, args); resolve(result); }else{ - console.error('[fakeJamClient] error: No such method in FakeJamClient', prop); + console.error('[FakeJamClientProxy] error: No such method in FakeJamClient', prop); reject(`No such method in FakeJamClient: ${prop}`); } } catch (error) { - console.error('[fakeJamClient] error:', prop, error); + console.error('[FakeJamClientProxy] error:', prop, error); reject(error); } }); diff --git a/jam-ui/src/fakeJamClientRecordings.js b/jam-ui/src/fakeJamClientRecordings.js new file mode 100644 index 000000000..4dd8e8fc3 --- /dev/null +++ b/jam-ui/src/fakeJamClientRecordings.js @@ -0,0 +1,215 @@ +// Fake Jam Client Recordings - simulates recording functionality for testing +export class FakeJamClientRecordings { + constructor(app, fakeJamClient, p2pMessageFactory) { + this.logger = console; + this.app = app; + this.fakeJamClient = fakeJamClient; + this.p2pMessageFactory = p2pMessageFactory; + + this.startRecordingResultCallbackName = null; + this.stopRecordingResultCallbackName = null; + this.startedRecordingResultCallbackName = null; + this.stoppedRecordingEventCallbackName = null; + this.abortedRecordingEventCallbackName = null; + + this.startingSessionState = null; + this.stoppingSessionState = null; + + this.currentRecordingId = null; + this.currentRecordingCreatorClientId = null; + this.currentRecordingClientIds = null; + + // Register P2P callbacks + const callbacks = {}; + callbacks[this.p2pMessageFactory.Types.START_RECORDING] = this.onStartRecording.bind(this); + callbacks[this.p2pMessageFactory.Types.START_RECORDING_ACK] = this.onStartRecordingAck.bind(this); + callbacks[this.p2pMessageFactory.Types.STOP_RECORDING] = this.onStopRecording.bind(this); + callbacks[this.p2pMessageFactory.Types.STOP_RECORDING_ACK] = this.onStopRecordingAck.bind(this); + callbacks[this.p2pMessageFactory.Types.ABORT_RECORDING] = this.onAbortRecording.bind(this); + fakeJamClient.RegisterP2PMessageCallbacks(callbacks); + } + + timeoutStartRecordingTimer() { + eval(this.startRecordingResultCallbackName).call(this, this.startingSessionState.recordingId, { success: false, reason: 'client-no-response', detail: this.startingSessionState.groupedClientTracks[0] }); + this.startingSessionState = null; + } + + timeoutStopRecordingTimer() { + eval(this.stopRecordingResultCallbackName).call(this, this.stoppingSessionState.recordingId, { success: false, reason: 'client-no-response', detail: this.stoppingSessionState.groupedClientTracks[0] }); + } + + StartRecording(recordingId, clients) { + this.startingSessionState = {}; + + // Expect all clients to respond within 1 second to mimic reliable UDP layer + this.startingSessionState.aggregatingStartResultsTimer = setTimeout(() => this.timeoutStartRecordingTimer(), 1000); + this.startingSessionState.recordingId = recordingId; + this.startingSessionState.groupedClientTracks = this.copyClientIds(clients, this.app.clientId); + + // Store current recording data + this.currentRecordingId = recordingId; + this.currentRecordingCreatorClientId = this.app.clientId; + this.currentRecordingClientIds = this.copyClientIds(clients, this.app.clientId); + + if (this.startingSessionState.groupedClientTracks.length === 0) { + // If no clients but 'self', declare successful recording immediately + this.finishSuccessfulStart(recordingId); + } else { + // Signal all other connected clients that recording has started + for (const clientId of this.startingSessionState.groupedClientTracks) { + this.fakeJamClient.SendP2PMessage(clientId, JSON.stringify(this.p2pMessageFactory.startRecording(recordingId))); + } + } + } + + StopRecording(recordingId, clients, result) { + if (this.startingSessionState) { + // Currently starting a session + // TODO + } + + if (!result) { + result = { success: true }; + } + + this.stoppingSessionState = {}; + + // Expect all clients to respond within 1 second + this.stoppingSessionState.aggregatingStopResultsTimer = setTimeout(() => this.timeoutStopRecordingTimer(), 1000); + this.stoppingSessionState.recordingId = recordingId; + this.stoppingSessionState.groupedClientTracks = this.copyClientIds(clients, this.app.clientId); + + if (this.stoppingSessionState.groupedClientTracks.length === 0) { + this.finishSuccessfulStop(recordingId); + } else { + // Signal all other connected clients that recording has stopped + for (const clientId of this.stoppingSessionState.groupedClientTracks) { + this.fakeJamClient.SendP2PMessage(clientId, JSON.stringify(this.p2pMessageFactory.stopRecording(recordingId, result.success, result.reason, result.detail))); + } + } + } + + AbortRecording(recordingId, errorReason, errorDetail) { + // TODO: check recordingId + this.fakeJamClient.SendP2PMessage(this.currentRecordingCreatorClientId, JSON.stringify(this.p2pMessageFactory.abortRecording(recordingId, errorReason, errorDetail))); + } + + onStartRecording(from, payload) { + this.logger.debug("received start recording request from " + from); + if (window.JK?.SessionStore?.isRecording) { + // Reject the request + this.fakeJamClient.SendP2PMessage(from, JSON.stringify(this.p2pMessageFactory.startRecordingAck(payload.recordingId, false, "already-recording", null))); + } else { + // Accept and tell frontend we are recording + this.currentRecordingId = payload.recordingId; + this.currentRecordingCreatorClientId = from; + + this.fakeJamClient.SendP2PMessage(from, JSON.stringify(this.p2pMessageFactory.startRecordingAck(payload.recordingId, true, null, null))); + eval(this.startedRecordingResultCallbackName).call(this, payload.recordingId, { success: true }, from); + } + } + + onStartRecordingAck(from, payload) { + this.logger.debug("received start recording ack from " + from); + + if (this.startingSessionState) { + if (payload.success) { + const index = this.startingSessionState.groupedClientTracks.indexOf(from); + this.startingSessionState.groupedClientTracks.splice(index, 1); + + if (this.startingSessionState.groupedClientTracks.length === 0) { + this.finishSuccessfulStart(payload.recordingId); + } + } else { + // TODO: handle error + this.logger.warn("received unsuccessful start_record_ack from: " + from); + } + } else { + this.logger.warn("received start_record_ack when no recording starting from: " + from); + } + } + + onStopRecording(from, payload) { + this.logger.debug("received stop recording request from " + from); + + // TODO: check recordingId and if currently recording + this.fakeJamClient.SendP2PMessage(from, JSON.stringify(this.p2pMessageFactory.stopRecordingAck(payload.recordingId, true))); + + if (this.stopRecordingResultCallbackName) { + eval(this.stopRecordingResultCallbackName).call(this, payload.recordingId, { success: payload.success, reason: payload.reason, detail: from }); + } + } + + onStopRecordingAck(from, payload) { + this.logger.debug("received stop recording ack from " + from); + + if (this.stoppingSessionState) { + if (payload.success) { + const index = this.stoppingSessionState.groupedClientTracks.indexOf(from); + this.stoppingSessionState.groupedClientTracks.splice(index, 1); + + if (this.stoppingSessionState.groupedClientTracks.length === 0) { + this.finishSuccessfulStop(payload.recordingId); + } + } else { + // TODO: handle error + this.logger.error("client responded with error: ", payload); + } + } else { + // TODO: error case + } + } + + onAbortRecording(from, payload) { + this.logger.debug("received abort recording from " + from); + + // TODO: check if currently recording and matches payload.recordingId + + if (this.app.clientId === this.currentRecordingCreatorClientId) { + // Ask frontend to stop + for (const clientId of this.currentRecordingClientIds) { + this.fakeJamClient.SendP2PMessage(clientId, JSON.stringify(this.p2pMessageFactory.abortRecording(this.currentRecordingId, payload.reason, from))); + } + } else { + this.logger.debug("only creator deals with abort request. sent from:" + from + " reason: " + payload.errorReason); + } + + eval(this.abortedRecordingEventCallbackName).call(this, payload.recordingId, { success: payload.success, reason: payload.reason, detail: from }); + } + + RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, abortedRecordingCallbackName) { + this.startRecordingResultCallbackName = startRecordingCallbackName; + this.stopRecordingResultCallbackName = stopRecordingCallbackName; + this.startedRecordingResultCallbackName = startedRecordingCallbackName; + this.stoppedRecordingEventCallbackName = stoppedRecordingCallbackName; + this.abortedRecordingEventCallbackName = abortedRecordingCallbackName; + } + + copyClientIds(clientIds, myClientId) { + const newClientIds = []; + for (const clientId of clientIds) { + if (clientId !== myClientId) { + newClientIds.push(clientId); + } + } + return newClientIds; + } + + finishSuccessfulStart(recordingId) { + clearTimeout(this.startingSessionState.aggregatingStartResultsTimer); + this.startingSessionState = null; + eval(this.startRecordingResultCallbackName).call(this, recordingId, { success: true }); + } + + finishSuccessfulStop(recordingId, errorReason) { + clearTimeout(this.stoppingSessionState.aggregatingStopResultsTimer); + this.stoppingSessionState = null; + const result = { success: true }; + if (errorReason) { + result.success = false; + result.reason = errorReason; + result.detail = ""; + } + eval(this.stopRecordingResultCallbackName).call(this, recordingId, result); + } +} diff --git a/jam-ui/src/helpers/MessageFactory.js b/jam-ui/src/helpers/MessageFactory.js index 9947c9441..ab1e1197d 100644 --- a/jam-ui/src/helpers/MessageFactory.js +++ b/jam-ui/src/helpers/MessageFactory.js @@ -3,8 +3,6 @@ * Based on the legacy AAB_message_factory.js, updated for ES6+ and React compatibility */ -import { getCookieValue } from './utils.js'; - // Message types for WebSocket communication export const MessageType = { LOGIN: "LOGIN", diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js index 7e916267a..8a45f45bb 100644 --- a/jam-ui/src/helpers/rest.js +++ b/jam-ui/src/helpers/rest.js @@ -230,11 +230,12 @@ export const getSessionHistory = (id, includePending = false) => { }); }; -const joinSession = (options = {}) => { +export const joinSession = (options = {}) => { + const { session_id, ...rest } = options; return new Promise((resolve, reject) => { - apiFetch(`/sessions/${options.session_id}/participants`, { + apiFetch(`/sessions/${session_id}/participants`, { method: 'POST', - body: JSON.stringify(options) + body: JSON.stringify(rest) }) .then(response => resolve(response)) .catch(error => reject(error)); @@ -735,7 +736,40 @@ export const getClientDownloads = () => { .then(response => resolve(response)) .catch(error => reject(error)); }); -} +}; + +// Recording-related functions +export const startRecording = (options) => { + return new Promise((resolve, reject) => { + apiFetch('/recordings/start', { + method: 'POST', + body: JSON.stringify(options) + }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; + +export const stopRecording = (options) => { + const { id, ...rest } = options; + return new Promise((resolve, reject) => { + apiFetch(`/recordings/${id}/stop`, { + method: 'POST', + body: JSON.stringify(rest) + }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; + +export const getRecordingPromise = (options) => { + const { id } = options; + return new Promise((resolve, reject) => { + apiFetch(`/recordings/${id}`) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getObsPluginDownloads = () => { return new Promise((resolve, reject) => { apiFetch(`/artifacts/OBSPlugin`) @@ -881,4 +915,4 @@ export const createDiagnostic = (options = {}) => { .then(response => resolve(response)) .catch(error => reject(error)); }); -} \ No newline at end of file +} diff --git a/jam-ui/src/hooks/useGearUtils.js b/jam-ui/src/hooks/useGearUtils.js index 4bcbf5889..fd63207a1 100644 --- a/jam-ui/src/hooks/useGearUtils.js +++ b/jam-ui/src/hooks/useGearUtils.js @@ -343,13 +343,7 @@ const useGearUtils = () => { const guardAgainstBadNetworkScore = useCallback((app) => { return new Promise(async (resolve, reject) => { if (!(await validNetworkScore())) { - app.layout.showDialog('network-test').one(EVENTS.DIALOG_CLOSED, async () => { - if (await validNetworkScore()) { - resolve(); - } else { - reject(); - } - }); + reject(); } else { resolve(); } @@ -369,13 +363,7 @@ const useGearUtils = () => { const guardAgainstInvalidGearConfiguration = useCallback((app) => { return new Promise(async (resolve, reject) => { if ((await jamClientFTUEGetAllAudioConfigurations()).length === 0) { - app.layout.showDialog('gear-wizard').one(EVENTS.DIALOG_CLOSED, async () => { - if (await hasGoodActiveProfile() && await validNetworkScore()) { - resolve(); - } else { - reject(); - } - }); + reject(); } else { resolve(); } @@ -387,21 +375,9 @@ const useGearUtils = () => { console.log('guardAgainstActiveProfileMissing: backendInfo %o', backendInfo); if (backendInfo.error && backendInfo.reason === 'no_profile' && (await jamClient.FTUEGetAllAudioConfigurations()).length > 0) { reject({ reason: 'handled', nav: '/client#/account/audio' }); - // Assuming Banner is available console.log('No Active Profile', 'We\'ve sent you to the audio profile screen...'); } else if (backendInfo.error && backendInfo.reason === 'device_failure') { - app.layout.showDialog('audio-profile-invalid-dialog').one(EVENTS.DIALOG_CLOSED, (e, data) => { - if (!data.result || data.result === 'cancel') { - reject({ reason: 'handled', nav: 'BACK' }); - } else if (data.result === 'configure_gear') { - reject({ reason: 'handled', nav: '/client#/account/audio' }); - } else if (data.result === 'session') { - resolve(); - } else { - console.error('unknown result condition in audio-profile-invalid-dialog:', data.result); - reject(); - } - }); + reject({ reason: 'handled', nav: '/client#/account/audio' }); } else { resolve(); } @@ -432,7 +408,7 @@ const useGearUtils = () => { }, []); const validNetworkScore = useCallback(async () => { - return !window.gon?.global?.network_test_required || isNetworkTestSkipped() || (await jamClient.GetNetworkTestScore()) >= 2; + return isNetworkTestSkipped() || (await jamClient.GetNetworkTestScore()) >= 2; }, [jamClient, isNetworkTestSkipped]); const isRestartingAudio = useCallback(() => { @@ -503,33 +479,13 @@ const useGearUtils = () => { }, [jamClient]); // Guard against single player profile - simplified, as it depends on app + //TODO: revisit this const guardAgainstSinglePlayerProfile = useCallback((app, beforeCallback) => { return new Promise(async (resolve, reject) => { - const canPlayWithOthers = await canPlayWithOthers(); + const canPlayResult = await canPlayWithOthers(); - if (!canPlayWithOthers.canPlay) { - console.log('guarding against single player profile'); - const $dialog = app.layout.showDialog('single-player-profile-dialog'); - - if (beforeCallback) { - $dialog.one(EVENTS.DIALOG_CLOSED, beforeCallback); - } - - $dialog.one(EVENTS.DIALOG_CLOSED, (e, data) => { - if (data.canceled) { - reject({ reason: 'canceled', controlled_location: false }); - } else { - if (data.result.choice === 'private_session') { - // Handle private session creation - reject({ reason: 'private_session', controlled_location: true }); - } else if (data.result.choice === 'gear_setup') { - reject({ reason: data.result.choice, controlled_location: true }); - } else { - reject({ reason: 'unknown', controlled_location: false }); - console.error('unknown choice:', data.result.choice); - } - } - }); + if (!canPlayResult.canPlay) { + reject(); } else { resolve(); } diff --git a/jam-ui/src/hooks/useMixerHelpers.js b/jam-ui/src/hooks/useMixerHelpers.js new file mode 100644 index 000000000..e69de29bb diff --git a/jam-ui/src/hooks/useRecordingHelpers.js b/jam-ui/src/hooks/useRecordingHelpers.js new file mode 100644 index 000000000..ee7ae6ed1 --- /dev/null +++ b/jam-ui/src/hooks/useRecordingHelpers.js @@ -0,0 +1,436 @@ +import { useState, useCallback, useRef, useEffect, useContext } from 'react'; +import { useCurrentSession } from '../context/CurrentSessionContext'; +import { useJamKazamApp } from '../context/JamKazamAppContext'; +import { startRecording as startRecordingRest, stopRecording as stopRecordingRest, getRecordingPromise } from '../helpers/rest'; + +const useRecordingHelpers = (jamClient) => { + const app = useJamKazamApp(); + const { currentSession } = useCurrentSession(); + + // State variables from original RecordingModel + const [currentRecording, setCurrentRecording] = useState(null); + const [currentOrLastRecordingId, setCurrentOrLastRecordingId] = useState(null); + const [currentRecordingId, setCurrentRecordingId] = useState(null); + const [thisClientStartedRecording, setThisClientStartedRecording] = useState(false); + const [currentlyRecording, setCurrentlyRecording] = useState(false); + const [startingRecording, setStartingRecording] = useState(false); + const [stoppingRecording, setStoppingRecording] = useState(false); + const [waitingOnServerStop, setWaitingOnServerStop] = useState(false); + const [waitingOnClientStop, setWaitingOnClientStop] = useState(false); + + const waitingOnStopTimer = useRef(null); + const sessionId = currentSession?.id; + + // Get state function + const getState = useCallback(() => ({ + waitingOnClientStop, + waitingOnServerStop, + stoppingRecording, + startingRecording + }), [waitingOnClientStop, waitingOnServerStop, stoppingRecording, startingRecording]); + + // Check if recording + const isRecording = useCallback((recordingId) => { + if (recordingId) { + return recordingId === currentRecordingId; + } else { + return currentlyRecording; + } + }, [currentRecordingId, currentlyRecording]); + + // Get this client started recording + const getThisClientStartedRecording = useCallback(() => thisClientStartedRecording, [thisClientStartedRecording]); + + // Reset function + const reset = useCallback((sessionId) => { + console.log("[RecordingState]: reset"); + setCurrentlyRecording(false); + setWaitingOnServerStop(false); + setWaitingOnClientStop(false); + if (waitingOnStopTimer.current) { + clearTimeout(waitingOnStopTimer.current); + waitingOnStopTimer.current = null; + } + setCurrentRecording(null); + setCurrentRecordingId(null); + setStoppingRecording(false); + setThisClientStartedRecording(false); + }, []); + + // Group tracks to client + const groupTracksToClient = useCallback((recording) => { + const groupedTracks = {}; + const recordingTracks = recording.recorded_tracks || []; + for (let i = 0; i < recordingTracks.length; i++) { + const clientId = recordingTracks[i].client_id; + let tracksForClient = groupedTracks[clientId]; + if (!tracksForClient) { + tracksForClient = []; + groupedTracks[clientId] = tracksForClient; + } + tracksForClient.push(recordingTracks[i]); + } + return Object.keys(groupedTracks); + }, []); + + // Start recording + const startRecording = useCallback(async (recordSettings) => { + const recordVideo = recordSettings.recordingType === 'JK.RECORD_TYPE_BOTH'; + + // Trigger startingRecording event + // In React, we might use a callback or context to notify parent components + + setCurrentlyRecording(true); + setStoppingRecording(false); + setThisClientStartedRecording(true); + + // Context RecordingActions.startingRecording would be handled by parent or context + + try { + const recording = await startRecordingRest({ music_session_id: sessionId, record_video: recordVideo }); + setCurrentRecordingId(recording.id); + setCurrentOrLastRecordingId(recording.id); + + const groupedTracks = groupTracksToClient(recording); + console.log("jamClient#StartMediaRecording", recordSettings); + await jamClient.StartMediaRecording(recording.id, groupedTracks, recordSettings); + } catch (jqXHR) { + console.warn("failed to startRecording due to server issue:", jqXHR.responseJSON); + const details = { clientId: app.clientId, reason: 'rest', detail: jqXHR.responseJSON, isRecording: false }; + // Trigger startedRecording event + setCurrentlyRecording(false); + // Context RecordingActions.startedRecording(details); + } + + return true; + }, [sessionId, groupTracksToClient, jamClient, app.clientId]); + + // Transition to stopped + const transitionToStopped = useCallback(() => { + console.log("[RecordingState] transitionToStopped"); + setCurrentlyRecording(false); + setCurrentRecording(null); + setCurrentRecordingId(null); + if (waitingOnStopTimer.current) { + clearTimeout(waitingOnStopTimer.current); + waitingOnStopTimer.current = null; + } + }, []); + + // Attempt transition to stop + const attemptTransitionToStop = useCallback((recordingId, errorReason, errorDetail) => { + if (!waitingOnClientStop && !waitingOnServerStop) { + transitionToStopped(); + const details = { recordingId, reason: errorReason, detail: errorDetail, isRecording: false }; + // Trigger stoppedRecording event + // Context RecordingActions.stoppedRecording(details) + } + }, [waitingOnClientStop, waitingOnServerStop, transitionToStopped]); + + // Timeout transition to stop + const timeoutTransitionToStop = useCallback(() => { + waitingOnStopTimer.current = null; + transitionToStopped(); + // Trigger stoppedRecordingFailed event + }, [transitionToStopped]); + + // Stop recording + const stopRecording = useCallback(async (recordingId, reason, detail) => { + const userInitiated = recordingId == null && reason == null && detail == null; + const recording = await currentRecording; + + console.log(`[RecordingState]: stopRecording userInitiated=${userInitiated} thisClientStartedRecording=${thisClientStartedRecording} reason=${reason} detail=${detail}`); + + if (stoppingRecording) { + console.log("ignoring stopRecording because we are already stopping"); + return; + } + setStoppingRecording(true); + + setWaitingOnServerStop(true); + setWaitingOnClientStop(true); + waitingOnStopTimer.current = setTimeout(timeoutTransitionToStop, 5000); + + // Trigger stoppingRecording event + // Context RecordingActions.stoppingRecording + + try { + const recording = await currentRecording; + const groupedTracks = groupTracksToClient(recording); + + await jamClient.FrontStopRecording(recording.id, groupedTracks); + + if (thisClientStartedRecording) { + try { + await stopRecordingRest({ id: recording.id }); + setWaitingOnServerStop(false); + attemptTransitionToStop(recording.id, reason, detail); + setStoppingRecording(false); + } catch (error) { + setStoppingRecording(false); + if (error.status === 422) { + setWaitingOnServerStop(false); + attemptTransitionToStop(recording.id, reason, detail); + } else { + console.error("unable to stop recording", error); + transitionToStopped(); + const details = { + recordingId: recording.id, + reason: 'rest', + details: error, + isRecording: false + }; + // Trigger stoppedRecording event + // Context RecordingActions.stoppedRecording(details) + setStoppingRecording(false); + } + } + } + } catch (error) { + console.error("Error in stopRecording:", error); + } + + return true; + }, [currentRecording, thisClientStartedRecording, stoppingRecording, groupTracksToClient, jamClient, timeoutTransitionToStop, attemptTransitionToStop, transitionToStopped]); + + // Abort recording + const abortRecording = useCallback(async (recordingId, errorReason, errorDetail) => { + await jamClient.AbortRecording(recordingId, { reason: errorReason, detail: errorDetail, success: false }); + }, [jamClient]); + + // Handle recording start result + const handleRecordingStartResult = useCallback((recordingId, result) => { + console.log("[RecordingState] handleRecordingStartResult", { recordingId, result, currentRecordingId, currentlyRecording }); + + const { success, reason, detail } = result; + + if (success) { + const details = { clientId: app.clientId, isRecording: true }; + // Trigger startedRecording event + // Context RecordingActions.startedRecording(details) + } else { + setCurrentlyRecording(false); + console.error("unable to start the recording", reason, detail); + const details = { clientId: app.clientId, reason, detail, isRecording: false }; + // Trigger startedRecording event + // Context RecordingActions.startedRecording(details) + } + }, [app.clientId, currentRecordingId, currentlyRecording]); + + // Handle recording stop result + const handleRecordingStopResult = useCallback((recordingId, result) => { + console.log("[RecordingState] handleRecordingStopResult", result); + const { success, reason, detail } = result; + + setWaitingOnClientStop(false); + + if (success) { + attemptTransitionToStop(recordingId, reason, detail); + } else { + transitionToStopped(); + console.error("backend unable to stop the recording", reason, detail); + const details = { recordingId, reason, detail, isRecording: false }; + // Trigger stoppedRecording event + // Context RecordingActions.stoppedRecording(details) + } + }, [attemptTransitionToStop]); + + // Handle recording started + const handleRecordingStarted = useCallback(async (recordingId, result, clientId) => { + reset(sessionId); + // Context RecordingActions.resetRecordingState() + + console.log("[RecordingState] handleRecordingStarted called", { recordingId, result, clientId, currentRecordingId, currentlyRecording, sessionId }); + + const { success, reason, detail } = result; + + console.log("[RecordingState] Attempting to fetch recording data from server", { recordingId, sessionId }); + + try { + const recording = await getRecordingPromise({ id: recordingId }); + console.log("[RecordingState] Successfully fetched recording data from server", { recordingId: recording.id, ownerId: recording.owner?.id }); + + postServerRecordingFetch(recording); + } catch (error) { + console.error("[RecordingState] Failed to fetch recording data", { recordingId, error }); + } + }, [sessionId, reset]); + + // Post server recording fetch + const postServerRecordingFetch = useCallback((recording) => { + if (currentRecordingId == null) { + setCurrentRecordingId(recording.id); + setCurrentOrLastRecordingId(recording.id); + + const details = { recordingId: currentRecordingId, isRecording: false }; + // Trigger startingRecording event + // Context RecordingActions.startingRecording(details) + setCurrentlyRecording(true); + + const startedDetails = { clientId: app.clientId, recordingId: currentRecordingId, isRecording: true }; + // Trigger startedRecording event + // Context RecordingActions.startedRecording(startedDetails) + } else if (currentRecordingId === recording.id) { + // noop + } else { + console.error("[RecordingState] we've missed the stop of previous recording", currentRecordingId, recording.id); + // Show alert + } + }, [currentRecordingId, app.clientId]); + + // Handle recording stopped + const handleRecordingStopped = useCallback(async (recordingId, result) => { + console.log("[RecordingState] handleRecordingStopped event_id=" + recordingId + " current_id=" + currentRecordingId, result); + + const { success, reason, detail } = result; + + const stoppingDetails = { recordingId, reason, detail, isRecording: true }; + // Trigger stoppingRecording event + // Context RecordingActions.stoppingRecording(stoppingDetails) + + if (recordingId == null || recordingId === "") { + transitionToStopped(); + const stoppedDetails = { recordingId, reason, detail, isRecording: false }; + // Context RecordingActions.stoppedRecording(stoppedDetails) + return; + } + + try { + await stopRecordingRest({ id: recordingId }); + transitionToStopped(); + const details = { recordingId, reason, detail, isRecording: false }; + // Trigger stoppedRecording event + // Context RecordingActions.stoppedRecording(details) + } catch (error) { + if (error.status === 422) { + console.log("recording already stopped", error); + transitionToStopped(); + const details = { recordingId, reason, detail, isRecording: false }; + // Trigger stoppedRecording event + // Context RecordingActions.stoppedRecording(details) + } else if (error.status === 404) { + console.log("recording is already deleted", error); + transitionToStopped(); + const details = { recordingId, reason, detail, isRecording: false }; + // Trigger stoppedRecording event + // Context RecordingActions.stoppedRecording(details) + } else { + transitionToStopped(); + const details = { recordingId, reason: error.message || 'error', detail: error, isRecording: false }; + // Trigger stoppedRecording event + // Context RecordingActions.stoppedRecording(details) + } + } + }, [currentRecordingId, transitionToStopped]); + + // Handle recording aborted + const handleRecordingAborted = useCallback(async (recordingId, result) => { + console.log("[RecordingState] handleRecordingAborted"); + if (recordingId === "video") { + // Handle video abort + return; + } + + const { success, reason, detail } = result; + + setStoppingRecording(false); + + const details = { recordingId, reason, detail, isRecording: false }; + // Trigger abortedRecording event + // Context RecordingActions.abortedRecording(details) + + try { + await stopRecordingRest({ id: recordingId }); + } catch (error) { + console.error("Error stopping recording in abort:", error); + } finally { + setCurrentlyRecording(false); + } + }, []); + + // Stop recording if needed + const stopRecordingIfNeeded = useCallback(() => { + return new Promise((resolve) => { + if (!currentlyRecording) { + resolve(); + } else { + // In React, we might use a state or callback to wait for stoppedRecording + // For now, assume immediate resolve + resolve(); + } + }); + }, [currentlyRecording]); + + // Get current recording state + const getCurrentRecordingState = useCallback(async () => { + let recording = null; + const session = currentSession; + let recordingId = null; + + if (jamClient) { + try { + recordingId = await jamClient.GetCurrentRecordingId(); + } catch (error) { + console.error("Error getting current recording ID:", error); + } + } + + const isRecording = recordingId && recordingId !== ""; + + if (session && isRecording) { + try { + recording = await getRecordingPromise({ id: recordingId }); + } catch (error) { + if (error.status !== 404) { + console.error("[RecordingState] Failed to fetch server recording state", { recordingId, error }); + } + } + } + + if (!session) { + console.debug("no session, so no recording"); + return { isRecording: false, serverRecording: null, thisClientStartedRecording: false }; + } + + return { + isRecording, + isServerRecording: !!recording, + thisClientStartedRecording + }; + }, [currentSession, jamClient, thisClientStartedRecording]); + + // Initialize + useEffect(() => { + // Register global handlers if needed + if (window) { + window.JK = window.JK || {}; + window.JK.HandleRecordingStartResult = handleRecordingStartResult; + window.JK.HandleRecordingStopResult = handleRecordingStopResult; + window.JK.HandleRecordingStopped = handleRecordingStopped; + window.JK.HandleRecordingStarted = handleRecordingStarted; + window.JK.HandleRecordingAborted = handleRecordingAborted; + } + }, [handleRecordingStartResult, handleRecordingStopResult, handleRecordingStopped, handleRecordingStarted, handleRecordingAborted]); + + return { + // State + currentlyRecording, + startingRecording, + stoppingRecording, + currentRecordingId, + currentOrLastRecordingId, + + // Functions + startRecording, + stopRecording, + abortRecording, + reset, + isRecording, + getThisClientStartedRecording, + stopRecordingIfNeeded, + getCurrentRecordingState, + getState, + }; +}; + +export default useRecordingHelpers; diff --git a/jam-ui/src/hooks/useSessionEnter.js b/jam-ui/src/hooks/useSessionEnter.js index ae2c853c5..efc1a5cb2 100644 --- a/jam-ui/src/hooks/useSessionEnter.js +++ b/jam-ui/src/hooks/useSessionEnter.js @@ -1,22 +1,24 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import { useJamClient } from '../context/JamClientContext'; import useGearUtils from './useGearUtils'; +import useTrackHelpers from './useTrackHelpers'; export default function useSessionEnter() { const jamClient = useJamClient(); const { isNoInputProfile } = useGearUtils(); const logger = console; // Replace with your logging mechanism if needed + const { getUserTracks } = useTrackHelpers(); + // State to hold pending promises and their resolvers const pendingPromisesRef = useRef(new Map()); const resolvePendingPromises = useCallback((inputTracks) => { - logger.debug("obtained tracks at start of session"); for (const { resolve } of pendingPromisesRef.current.values()) { resolve(inputTracks); } pendingPromisesRef.current.clear(); - }, [logger]); + }, []); const rejectPendingPromises = useCallback((reason) => { for (const { reject } of pendingPromisesRef.current.values()) { @@ -28,35 +30,35 @@ export default function useSessionEnter() { // Event handlers const onWatchedInputs = useCallback((inputTracks) => { resolvePendingPromises(inputTracks); - }, [resolvePendingPromises]); + }, []); const onMixersChanged = useCallback((type, text, trackInfo) => { if (text === 'RebuildAudioIoControl' && trackInfo.userTracks.length > 0) { - logger.debug("obtained tracks at start of session"); resolvePendingPromises(trackInfo.userTracks); } - }, [resolvePendingPromises, logger]); + }, []); // Register callbacks with jamClient - useEffect(() => { - if (jamClient && jamClient.registerCallback) { - jamClient.registerCallback('onWatchedInputs', onWatchedInputs); - jamClient.registerCallback('onMixersChanged', onMixersChanged); + // useEffect(() => { + // if (jamClient && jamClient.registerCallback) { + // jamClient.registerCallback('onWatchedInputs', onWatchedInputs); + // jamClient.registerCallback('onMixersChanged', onMixersChanged); - return () => { - jamClient.unregisterCallback('onWatchedInputs', onWatchedInputs); - jamClient.unregisterCallback('onMixersChanged', onMixersChanged); - }; - } - }, [jamClient, onWatchedInputs, onMixersChanged]); + // return () => { + // jamClient.unregisterCallback('onWatchedInputs', onWatchedInputs); + // jamClient.unregisterCallback('onMixersChanged', onMixersChanged); + // }; + // } + // }, [jamClient, onWatchedInputs, onMixersChanged]); const waitForSessionPageEnterDone = useCallback(() => { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { // Check if tracks are already available - const inputTracks = jamClient ? jamClient.getUserTracks() : []; + const inputTracks = await getUserTracks(); - logger.debug("isNoInputProfile", isNoInputProfile()); - if (inputTracks.length > 0 || isNoInputProfile()) { + logger.debug("isNoInputProfile", await isNoInputProfile()); + logger.debug("inputTracks", inputTracks); + if (inputTracks.length > 0 || await isNoInputProfile()) { logger.debug("on page enter, tracks are already available"); resolve(inputTracks); return; @@ -83,14 +85,14 @@ export default function useSessionEnter() { .then(resolve) .catch(reject); }); - }, [jamClient, isNoInputProfile, logger]); + }, []); // Cleanup on unmount useEffect(() => { return () => { rejectPendingPromises('component_unmounted'); }; - }, [rejectPendingPromises]); + }, []); return { waitForSessionPageEnterDone, diff --git a/jam-ui/src/hooks/useTrackHelpers.js b/jam-ui/src/hooks/useTrackHelpers.js new file mode 100644 index 000000000..586d27100 --- /dev/null +++ b/jam-ui/src/hooks/useTrackHelpers.js @@ -0,0 +1,103 @@ +import { useCallback } from 'react'; +import { useJamClient } from '../context/JamClientContext'; +import { + ChannelGroupIds, + server_to_client_instrument_map, + client_to_server_instrument_map +} from '../helpers/globals'; + +const logger = console; + +export default function useTrackHelpers() { + const jamClient = useJamClient(); + + const getTrackInfo = useCallback(async (masterTracks) => { + if (masterTracks === undefined) { + masterTracks = await jamClient.SessionGetAllControlState(true); + } + + const userTracks = await getUserTracks(masterTracks); + const backingTracks = await getBackingTracks(masterTracks); + const metronomeTracks = await getTracks(ChannelGroupIds.MetronomeGroup, masterTracks); + + return { + userTracks, + backingTracks, + metronomeTracks + }; + }, [jamClient]); + + const getTracks = useCallback(async (groupId, allTracks) => { + const tracks = []; + + if (!allTracks) { + allTracks = await jamClient.SessionGetAllControlState(true); + } + + for (const track of allTracks) { + if (track.group_id === groupId) { + tracks.push(track); + } + } + + return tracks; + }, [jamClient]); + + const getBackingTracks = useCallback(async (allTracks) => { + const mediaTracks = await getTracks(ChannelGroupIds.MediaTrackGroup, allTracks); + + const backingTracks = []; + mediaTracks.forEach((mediaTrack) => { + if (mediaTrack.media_type === "BackingTrack" && !mediaTrack.managed) { + const track = {}; + track.client_track_id = mediaTrack.persisted_track_id; + track.client_resource_id = mediaTrack.rid; + track.filename = mediaTrack.filename; + backingTracks.push(track); + } + }); + + return backingTracks; + }, [getTracks]); + + const getUserTracks = useCallback(async (allTracks) => { + const localMusicTracks = []; + const localMidiTracks = []; + + const audioTracks = await getTracks(ChannelGroupIds.AudioInputMusicGroup, allTracks); + const midiTracks = await getTracks(ChannelGroupIds.MidiInputMusicGroup, allTracks); + localMusicTracks.push(...audioTracks, ...midiTracks); + + const trackObjects = []; + + for (const track of localMusicTracks) { + const trackObj = {}; + trackObj.client_track_id = track.id; + trackObj.client_resource_id = track.rid; + + if (track.instrument_id === 0) { + trackObj.instrument_id = server_to_client_instrument_map["Other"].server_id; + } else { + const instrument = client_to_server_instrument_map[track.instrument_id]; + if (instrument) { + trackObj.instrument_id = instrument.server_id; + } else { + logger.debug("backend reported an invalid instrument ID of " + track.instrument_id); + trackObj.instrument_id = 'other'; + } + } + + trackObj.sound = track.stereo ? "stereo" : "mono"; + trackObjects.push(trackObj); + } + + return trackObjects; + }, [getTracks]); + + return { + getTrackInfo, + getTracks, + getBackingTracks, + getUserTracks + }; +} diff --git a/jam-ui/src/jamClientProxy.js b/jam-ui/src/jamClientProxy.js index f23f3be9f..b5259cbb4 100644 --- a/jam-ui/src/jamClientProxy.js +++ b/jam-ui/src/jamClientProxy.js @@ -324,6 +324,11 @@ class JamClientProxy { if (!response['execute_script'].match('HandleBridgeCallback2')) { // this.logger.log(`[jamClientProxy] 3006 execute_script: ${response['execute_script']}`); } + if (response['execute_script'].includes('HandleBridgeCallback2')) { + //log this for now, need to investigate why this is causing issues + this.logger.log(`[jamClientProxy] 3006 execute_script (skipping eval): ${response['execute_script']}`); + break; + } try { // eslint-disable-next-line no-eval eval(response['execute_script']); diff --git a/jam-ui/src/layouts/JKClientLayout.js b/jam-ui/src/layouts/JKClientLayout.js index 6a5fbb38b..76d11f424 100644 --- a/jam-ui/src/layouts/JKClientLayout.js +++ b/jam-ui/src/layouts/JKClientLayout.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import ClientRoutes from './JKClientRoutes'; import UserAuth from '../context/UserAuth'; import { BrowserQueryProvider } from '../context/BrowserQuery'; +import { JamKazamAppProvider } from '../context/JamKazamAppContext'; import { AppDataProvider } from '../context/AppDataContext'; import { AppRoutesProvider } from '../context/AppRoutesContext'; import { JamClientProvider } from '../context/JamClientContext'; @@ -15,13 +16,15 @@ const JKClientLayout = ({ location }) => { - - - - - - - + + + + + + + + + diff --git a/web/app/assets/javascripts/asyncJamClient.js b/web/app/assets/javascripts/asyncJamClient.js index ca3db4b39..163c009a7 100644 --- a/web/app/assets/javascripts/asyncJamClient.js +++ b/web/app/assets/javascripts/asyncJamClient.js @@ -514,7 +514,7 @@ ); } else if (evt_id) { let method = Object.keys(response)[0] - // logger.log("[asyncJamClient] event received:", evt_id.toString(), Object.keys(response)[0]) + logger.log("[asyncJamClient] event received:", evt_id.toString(), Object.keys(response)[0]) // if(evt_id.toString() === '3012'){ // alert(evt_id.toString()) @@ -522,8 +522,8 @@ switch (evt_id.toString()) { case '3006': //execute_script - if(!response['execute_script'].match('HandleBridgeCallback2')){ - //logger.log(`[asyncJamClient] 3006 execute_script: ${response['execute_script']}`); + if(response['execute_script'].match('HandleBridgeCallback2')){ + logger.log(`[asyncJamClient] 3006 execute_script: ${response['execute_script']}`); } try { eval(response['execute_script']); diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index d92e1f065..c2b39a166 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -409,34 +409,34 @@ // you should only update currentSession with this function function updateCurrentSession(sessionData) { - if(sessionData != null) { - currentOrLastSession = sessionData; + currentOrLastSssion = sessionData; + } } - var beforeUpdate = currentSession; +varbeforeUpdate=currentession; - currentSession = sessionData; + csDratSsiData; - // the 'beforeUpdate != null' makes sure we only do a clean up one time internally - if(sessionData == null) { - sessionEnded(beforeUpdate != null); + befthe 'befrreUUdadat!= null' !ak= 'smrakws only do a clean up one time internally + if(sessionata == null)= + sessiofEnded(beforeUpdaote! !=ull } - } +}} - function updateSession(response) { - updateSessionInfo(response, null, true); - } + fc updSe(po { + }daSeInfo(sonsul, ru +} - function updateSessionInfo(response, callback, force) { - if(force === true || currentTrackChanges < response.track_changes_counter) { - logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter) - currentTrackChanges = response.track_changes_counter; - sendClientParticipantChanges(currentSession, response); - updateCurrentSession(response); +funct updSeInfo(po,calback, forc) {teSession(response) { + if(forceu===ptrued||acurrentTrackChanges