wip session screen

This commit is contained in:
Nuwan 2025-10-10 12:42:26 +05:30
parent daf530fae6
commit 5eceb287c0
19 changed files with 1252 additions and 388 deletions

View File

@ -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", $('<span>You will need to reconfigure your audio device.</span>'));
// // } else if (response["errors"] && response["errors"]["music_session"] && (response["errors"]["music_session"][0] === ["is currently recording"])) {
// // leaveBehavior = {
// // location: "/client#/findSession",
// // notify: {
// // title: "Unable to Join Session",
// // text: "The session is currently recording."
// // }
// // };
// // SessionActions.leaveSession.trigger(leaveBehavior);
// // } else 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 (
<Card>
{!isConnected && <div className='d-flex align-items-center'>Connecting to backend...</div>}
<FalconCardHeader title={`Session ${sessionId}`} titleClass="font-weight-bold">
<FalconCardHeader title={`Session ${currentSessionIdRef.current}`} titleClass="font-weight-bold">
</FalconCardHeader>
<CardHeader className="bg-light border-bottom border-top py-2 border-3">

View File

@ -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 (
<CurrentSessionContext.Provider value={{ currentSession, setCurrentSession, inSession }}>
<CurrentSessionContext.Provider value={{
currentSession,
setCurrentSession,
inSession,
setCurrentSessionId,
currentSessionIdRef,
}}
>
{children}
</CurrentSessionContext.Provider>
);

View File

@ -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 (
<JamClientContext.Provider value={proxyRef.current}>
{children}
@ -18,4 +40,3 @@ export const JamClientProvider = ({ children }) => {
};
export const useJamClient = () => useContext(JamClientContext);

View File

@ -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 (
<JamKazamAppContext.Provider value={value}>
{children}
</JamKazamAppContext.Provider>
);
};
export const useJamKazamApp = () => useContext(JamKazamAppContext);

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
});

View File

@ -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);
}
}

View File

@ -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",

View File

@ -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));
});
}
}

View File

@ -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();
}

View File

View File

@ -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;

View File

@ -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,

View File

@ -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
};
}

View File

@ -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']);

View File

@ -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 }) => {
<UserAuth path={location.pathname}>
<AppRoutesProvider>
<AppDataProvider>
<BrowserQueryProvider>
<JamClientProvider>
<CurrentSessionProvider>
<ClientRoutes />
</CurrentSessionProvider>
</JamClientProvider>
</BrowserQueryProvider>
<JamKazamAppProvider>
<BrowserQueryProvider>
<JamClientProvider>
<CurrentSessionProvider>
<ClientRoutes />
</CurrentSessionProvider>
</JamClientProvider>
</BrowserQueryProvider>
</JamKazamAppProvider>
</AppDataProvider>
</AppRoutesProvider>
</UserAuth>

View File

@ -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']);

View File

@ -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<rpse.trck_chnge_uter
unction updelegges.dnnug("updfting ourrretneraclbcaang,srfromc%oeto %o",=currt|tTr ckChunget, reschas .trock_chsngea_cnugtes)nter) {
logger.ducurr(npTrngeChangest=TrespkCsa.grack_ch ngs__csu_ter; sendClientParticipantChanges(currentSession, response);
updateCuetSndClseotP(rrie;Cngs(cr elSesson, rspos
callback();pdCrreS(sps
if(callback != null) {
callback();
}
}
}abck(;
}}
}
else {
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
}