wip session screen mixer data and showing vu for mixers

This commit is contained in:
Nuwan 2025-11-06 12:56:25 +05:30
parent aedcc2ed65
commit a5e6a4644d
29 changed files with 3145 additions and 355 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,4 +14,5 @@
@import './custom/audio_player';
@import './custom/landing';
@import './custom/buttons';
@import './custom/session';

View File

@ -0,0 +1,84 @@
#tracks {
margin-top:12px;
overflow:auto;
&.no-local-tracks {
#session-mytracks-notracks {
display:block;
}
#session-mytracks-container {
display:none;
}
#recording-start-stop {
display:none;
}
#session-invite-musicians {
display:none;
}
}
}
.track-empty a {
color:#aaa;
}
table.vu td {
border: 3px solid #770e0e;
}
.track-vu-left {
position:absolute;
bottom:8px;
left:0px;
}
.track-vu-right {
position:absolute;
bottom:8px;
right:0px;
}
.vu-red-off {
background-color:#6c1709;
}
.vu-red-on {
background-color:#e73619;
}
.vu-green-off {
background-color:#244105;
}
.vu-green-on {
background-color:#72a43b;
}
.track-label {
position: absolute;
text-align:center;
width: 55px;
height: 15px;
min-height: 11px;
max-height: 33px;
max-width: 55px;
white-space:normal;
top: 3px;
left: 7px;
font-family: Arial, Helvetica, sans-serif;
font-size: 11px;
font-weight: bold;
text-overflow:ellipsis;
}
.track-close {
position:absolute;
top:3px;
right:3px;
width:12px;
height:12px;
}

View File

@ -0,0 +1,9 @@
import React from 'react'
const DebugPage = () => {
return (
<div>DebugPage</div>
)
}
export default DebugPage

View File

@ -2,7 +2,7 @@
import React, { useEffect, useContext, useState, memo } from 'react'
import { useParams } from 'react-router-dom';
import useJamServer, { ConnectionStatus } from '../../hooks/useJamServer'
//import useJamServer, { ConnectionStatus } from '../../hooks/useJamServer'
import useGearUtils from '../../hooks/useGearUtils'
import useSessionUtils from '../../hooks/useSessionUtils.js';
import useSessionModel from '../../hooks/useSessionModel.js';
@ -10,9 +10,10 @@ import useSessionHelper from '../../hooks/useSessionHelper.js';
import useRecordingHelpers from '../../hooks/useRecordingHelpers.js';
import useMixerStore from '../../hooks/useMixerStore.js';
import { useCurrentSession } from '../../context/CurrentSessionContext.js';
import { useCurrentSessionContext } from '../../context/CurrentSessionContext.js';
import { useJamServerContext } from '../../context/JamServerContext.js';
import { useJamKazamApp } from '../../context/JamKazamAppContext.js';
import { useMixersContext } from '../../context/MixersContext.js';
import { getSessionHistory, getSession, joinSession as joinSessionRest } from '../../helpers/rest';
@ -26,15 +27,6 @@ import SessionTrackVU from './SessionTrackVU.js';
const JKSessionScreen = () => {
const logger = console; // Replace with another logging mechanism if needed
const app = useJamKazamApp();
const {
isConnected,
connectionStatus,
reconnectAttempts,
lastError,
jamClient,
server,
registerMessageCallback,
} = useJamServer(process.env.REACT_APP_WEBSOCKET_GATEWAY_URL);
const {
guardAgainstInvalidConfiguration,
@ -42,19 +34,23 @@ const JKSessionScreen = () => {
guardAgainstSinglePlayerProfile,
} = useGearUtils();
const { initialize: initializeMixer, mixers, onSessionChange } = useMixerStore();
const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSession();
const { initialize: initializeMixer, onSessionChange } = useMixerStore();
const mixerHelper = useMixersContext();
const { isConnected,
ConnectionStatus,
connectionStatus,
reconnectAttempts,
lastError,
jamClient,
server,
registerMessageCallback } = useJamServerContext();
const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSessionContext();
const { getCurrentRecordingState, reset: resetRecordingState } = useRecordingHelpers();
const { SessionPageEnter } = useSessionUtils();
// Use the session model hook
const sessionModel = useSessionModel(app, server, null); // sessionScreen is null for now
const sessionHelper = useSessionHelper();
const { id: sessionId } = useParams();
// State to hold session data
@ -66,9 +62,13 @@ const JKSessionScreen = () => {
const [sessionRules, setSessionRules] = useState(null);
const [subscriptionRules, setSubscriptionRules] = useState(null);
const [currentOrLastSession, setCurrentOrLastSession] = useState(null);
const [sessionGuardsPassed, setSessionGuardsPassed] = useState(false);
const [myTracks, setMyTracks] = useState([]);
const [mixers, setMixers] = useState([]);
useEffect(() => {
if (!isConnected || !jamClient) return;
console.debug("JKSessionScreen: -DEBUG- isConnected changed to true");
guardOnJoinSession();
}, [isConnected, jamClient]); // Added jamClient to dependencies for stability
@ -112,8 +112,7 @@ const JKSessionScreen = () => {
await ensureAppropriateProfile(musicianAccessOnJoin)
logger.log("user has passed all session guards")
// all checks passed; join the session
await joinSession();
setSessionGuardsPassed(true)
} catch (error) {
logger.error("User profile is not appropriate for session:", error);
@ -149,7 +148,13 @@ const JKSessionScreen = () => {
}
};
useEffect(() => {
if (!sessionGuardsPassed || userTracks.length === 0 || hasJoined) { return }
joinSession();
}, [sessionGuardsPassed, userTracks, hasJoined])
const joinSession = async () => {
await jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2");
await jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
@ -159,6 +164,8 @@ const JKSessionScreen = () => {
const parentClientId = await jamClient.getParentClientId();
console.debug('role when joining session: ' + clientRole + ', parentClientId: ' + parentClientId);
if (clientRole === 0) {
clientRole = 'child';
} else if (clientRole === 1) {
@ -174,11 +181,25 @@ const JKSessionScreen = () => {
// 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 clientId = await jamClient.clientID();
const clientId = server.clientId;
console.log("xxx joining session " + sessionId + " as client " + JSON.stringify(clientId) + " with role " + clientRole + " and parent client " + parentClientId);
const latency = await jamClient.FTUEGetExpectedLatency().latency
console.log("joinSession parameters: ", {
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,
});
joinSessionRest({
client_id: clientId,
ip_address: server.publicIP,
@ -189,11 +210,13 @@ const JKSessionScreen = () => {
parent_client_id: parentClientId,
audio_latency: latency,
}).then(async (response) => {
console.debug("join session response: ", response);
console.debug("joinSessionRest response received", response.errors);
if (response.errors) {
throw new Error("Unable to join session: " + JSON.stringify(response.errors));
} else {
const data = await response.json();
console.debug("join session response xxx: ", data);
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
@ -245,6 +268,7 @@ const JKSessionScreen = () => {
}
}).catch((xhr) => {
console.error("joinSessionRest error: ", xhr);
let leaveBehavior;
sessionModel.updateCurrentSession(null);
@ -252,19 +276,19 @@ const JKSessionScreen = () => {
// we tried to join the session, but it is already gone. kick user back to join session screen
} else if (xhr.status === 422) {
const response = JSON.parse(xhr.responseText);
if (response["errors"] && response["errors"]["tracks"] && (response["errors"]["tracks"][0] === "Please select at least one track")) {
// You will need to reconfigure your audio device. show an alert
} else if (response["errors"] && response["errors"]["music_session"] && (response["errors"]["music_session"][0] == ["is currently recording"])) {
//The session is currently recording. You can not join a session that is recording. show an alert
} else if (response["errors"] && response["errors"]["remaining_session_play_time"]) {
//user has no available playtime. upgrade
} else if (response["errors"] && response["errors"]["remaining_month_play_time"]) {
//user has no available playtime. upgrade
} else {
// unknown 422 error. alert unable to join sessio
}
//console.error("unable to join session - 422 error");
// const response = JSON.parse(xhr.responseText);
// if (response["errors"] && response["errors"]["tracks"] && (response["errors"]["tracks"][0] === "Please select at least one track")) {
// // You will need to reconfigure your audio device. show an alert
// } else if (response["errors"] && response["errors"]["music_session"] && (response["errors"]["music_session"][0] == ["is currently recording"])) {
// //The session is currently recording. You can not join a session that is recording. show an alert
// } else if (response["errors"] && response["errors"]["remaining_session_play_time"]) {
// //user has no available playtime. upgrade
// } else if (response["errors"] && response["errors"]["remaining_month_play_time"]) {
// //user has no available playtime. upgrade
// } else {
// // unknown 422 error. alert unable to join sessio
// }
} else {
// unable to join session
}
@ -273,13 +297,10 @@ const JKSessionScreen = () => {
}
useEffect(() => {
if (!isConnected) return;
if (!sessionModel) return;
if (!hasJoined) return;
console.debug("JKSessionScreen: -DEBUG-", sessionHelper);
console.log("isConnected or hasJoined changed:", { isConnected, hasJoined });
if (!isConnected || !hasJoined) return;
onSessionChange(sessionHelper);
}, [isConnected, hasJoined, sessionHelper]); // Added sessionModel to dependencies for stability
}, [isConnected, hasJoined]); // Added sessionModel to dependencies for stability
const ensureAppropriateProfile = async (musicianAccess) => {
return new Promise(async function (resolve, reject) {
@ -301,9 +322,6 @@ const JKSessionScreen = () => {
const refreshCurrentSession = sessionModel.refreshCurrentSession;
const updateSession = sessionModel.updateSession;
// useEffect(() => {
// if (!isConnected) return;
// // validate session by fetching the session from the server
@ -323,8 +341,6 @@ const JKSessionScreen = () => {
// }
// };
// Monitor connection status changes
useEffect(() => {
if (connectionStatus === ConnectionStatus.DISCONNECTED ||
@ -335,19 +351,7 @@ const JKSessionScreen = () => {
}
}, [connectionStatus]);
const loadSessionData = async () => {
try {
const audioConfigs = await jamClient.FTUEGetAllAudioConfigurations();
const controlState = await jamClient.SessionGetAllControlState(true);
const sampleRate = await jamClient.GetSampleRate();
logger.log('Session data loaded:', { audioConfigs, controlState, sampleRate });
} catch (error) {
logger.error('Error loading session data:', error);
}
};
// Handlers for recording and playback
// const handleStartRecording = () => {
// jamClient.StartRecording({ recordingId: `rec_${Date.now()}` });
// };
@ -394,6 +398,17 @@ const JKSessionScreen = () => {
// // Handle VU meter updates
// };
useEffect(() => {
setMyTracks(mixerHelper.myTracks);
console.log("My Tracks changing", mixerHelper.myTracks)
// Update mixers based on myTracks
const updatedMixers = mixerHelper.myTracks.map(track => {
return track.mixers;
}).filter(mixer => mixer !== undefined);
console.log("Updated Mixers:", updatedMixers);
setMixers(updatedMixers);
}, [mixerHelper.myTracks])
return (
<Card>
{!isConnected && <div className='d-flex align-items-center'>Connecting to backend...</div>}
@ -420,38 +435,33 @@ const JKSessionScreen = () => {
<div className='audioInputs'>
<h5>Audio Inputs</h5>
<div className='d-flex' style={{ gap: '0.5rem', borderRight: '1px #ddd solid', paddingRight: '1rem' }}>
{/* {participants.map((participant, index) => (
<div key={participant.clientId} className='shadow-sm' style={{ border: '1px #ddd solid', width: '100px', height: '600px', backgroundColor: 'white' }}>
<div className='d-flex flex-column' style={{ height: '100%' }}>
<div className='p-2'>{participant.name}</div>
<div className='p-2'>Instrument: {participant.instrument}</div>
<div className='p-2'>Latency: {participant.latency}ms</div>
<div className='p-2'>Volume Slider</div>
<div className='p-2'>Controls</div>
</div>
</div>
))} */}
{
JSON.stringify(mixers)
}
<SessionTrackVU lightCount={12} orientation="vertical" lightWidth={5} lightHeight={20} side="left" ptr="STV1" mixers={mixers} />
<div>{
myTracks.length === 0 ? (
<div>No mixers available</div>
) : (
myTracks.map((track, index) => (
<div key={track.id || index}>{
<SessionTrackVU
lightCount={13}
orientation="vertical"
lightWidth={5}
lightHeight={7}
side="best"
ptr={`track_vu_${track.id || index}`}
mixers={track.mixers}
/>
}</div>
))
)
}</div>
</div>
</div>
<div className='sessionMix'>
<h5>Session Mix</h5>
<div className='d-flex' style={{ gap: '0.5rem' }}>
{/* {mixers.slice(0, 4).map((mixer, index) => (
<div key={mixer.id} className='shadow-sm' style={{ border: '1px #ddd solid', width: '100px', height: '600px', backgroundColor: 'white' }}>
<div className='d-flex flex-column align-items-center' style={{ height: '100%', padding: '10px' }}>
<div className='mb-2'>Mixer {index + 1}</div>
<div className='mb-2'>ID: {mixer.id}</div>
<div className='mb-2'>Volume: {mixer.volume_left}dB</div>
<div className='mb-2'>Mute: {mixer.mute ? 'Yes' : 'No'}</div>
<div className='mb-2'>Type: {mixer.group_id}</div>
</div>
</div>
))} */}
</div>
</div>

View File

@ -1,47 +1,43 @@
import React, { useEffect, useRef, useState } from 'react';
import useVuHelpers from '../../hooks/useVuHelpers';
import React, { useEffect, useRef } from 'react';
import { useVuContext } from '../../context/VuContext';
function SessionTrackVU({ lightCount, orientation, lightWidth, lightHeight, side, ptr, mixers }) {
const { renderVU, registerVU, unregisterVU } = useVuHelpers();
const [registered, setRegistered] = useState(null);
const { VuMeter, updateVuState } = useVuContext();
const ptrRef = useRef(ptr || `STV${Date.now()}`);
const containerRef = useRef(null);
const mixerIdRef = useRef(null);
useEffect(() => {
console.debug("SessionTrackVU: useEffect - registering VU", { mixers, registered, side, orientation, lightCount });
const mixer = mixers?.vuMixer;
if (!mixer) return;
let mixerChanged = false;
if (registered && mixer) {
if (registered.mixer.id !== mixer.id) {
unregisterVU(registered.mixer, registered.ptr);
mixerChanged = true;
}
if (!mixer) {
mixerIdRef.current = null;
return;
}
if (!mixerChanged && (registered || !mixer)) return;
// Create a unique ID for this VU meter
const mixerId = `${mixer.mode ? 'M' : 'P'}${mixer.id}`;
mixerIdRef.current = mixerId;
const horizontal = orientation === 'horizontal';
const lights = containerRef.current.querySelectorAll('td');
registerVU(side, mixer, ptrRef.current, horizontal, lightCount, lights);
setRegistered({ mixer, ptr: ptrRef.current });
}, [mixers]);
console.debug("SessionTrackVU: VU registered for mixer", mixerId);
useEffect(() => {
return () => {
if (registered) {
unregisterVU(registered.mixer, registered.ptr);
// Cleanup: reset VU state when component unmounts or mixer changes
if (mixerIdRef.current) {
updateVuState(mixerIdRef.current, 0, false);
mixerIdRef.current = null;
}
};
}, [registered, unregisterVU]);
}, [mixers, updateVuState]);
const vuType = orientation === 'horizontal' ? 'horizontal' : 'vertical';
// Use the React component for rendering
return (
<div ref={containerRef}>
{renderVU({ vuType, lightCount, lightWidth, lightHeight })}
</div>
<VuMeter
mixerId={mixerIdRef.current}
orientation={orientation}
lightCount={lightCount}
lightWidth={lightWidth}
lightHeight={lightHeight}
/>
);
}

View File

@ -1,20 +0,0 @@
import useVuHelpers from '../../hooks/useVuHelpers';
function VUMeter() {
const vuRef = React.useRef(null);
const { renderVU, updateVU, registerVU } = useVuHelpers();
React.useEffect(() => {
if (vuRef.current) {
registerVU(vuRef.current);
}
}, [vuRef, registerVU]);
return (
<div ref={vuRef}>
{renderVU({ vuType: 'vertical', lightCount: 12 })}
</div>
);
}

View File

@ -1,4 +1,6 @@
import React, { createContext, useContext, useState, useRef } from 'react';
import React, { createContext, useContext, useState, useRef, useEffect } from 'react';
//import useMixerHelper from '../hooks/useMixerHelper';
import { useJamServerContext } from './JamServerContext';
const CurrentSessionContext = createContext(null);
@ -6,6 +8,16 @@ export const CurrentSessionProvider = ({ children }) => {
const [currentSession, setCurrentSession] = useState({});
const currentSessionIdRef = useRef(null);
//const mixerHelper = useMixerHelper();
// const { isConnected,
// connectionStatus,
// reconnectAttempts,
// lastError,
// jamClient,
// server,
// registerMessageCallback,
// ConnectionStatus } = useJamServerContext();
const inSession = () => {
return currentSessionIdRef.current !== null;
};
@ -29,4 +41,4 @@ export const CurrentSessionProvider = ({ children }) => {
);
};
export const useCurrentSession = () => useContext(CurrentSessionContext);
export const useCurrentSessionContext = () => useContext(CurrentSessionContext);

View File

@ -0,0 +1,32 @@
import React, { createContext, useContext } from 'react';
import useJamServer, { ConnectionStatus } from '../hooks/useJamServer';
const JamServerContext = createContext(null);
export const JamServerProvider = ({ children }) => {
const { isConnected,
connectionStatus,
reconnectAttempts,
lastError,
jamClient,
server,
registerMessageCallback } = useJamServer(process.env.REACT_APP_WEBSOCKET_GATEWAY_URL);
return (
<JamServerContext.Provider value={{
ConnectionStatus,
connectionStatus,
isConnected,
jamClient,
server,
reconnectAttempts,
lastError,
registerMessageCallback,
}}
>
{children}
</JamServerContext.Provider>
);
};
export const useJamServerContext = () => useContext(JamServerContext);

View File

@ -0,0 +1,24 @@
import React, { createContext, useContext } from 'react';
import useMixerHelper from '../hooks/useMixerHelper.js';
const MixersContext = createContext();
export const MixersProvider = ({ children }) => {
const mixerHelper = useMixerHelper();
return (
<MixersContext.Provider value={mixerHelper}>
{children}
</MixersContext.Provider>
);
};
export const useMixersContext = () => {
const context = useContext(MixersContext);
if (!context) {
throw new Error('useMixersContext must be used within a MixersProvider');
}
return context;
};
export default MixersContext;

View File

@ -0,0 +1,24 @@
import React, { createContext, useContext } from 'react';
import useVuHelpers from '../hooks/useVuHelpers.js';
const VuContext = createContext();
export const VuProvider = ({ children }) => {
const vuHelpers = useVuHelpers();
return (
<VuContext.Provider value={vuHelpers}>
{children}
</VuContext.Provider>
);
};
export const useVuContext = () => {
const context = useContext(VuContext);
if (!context) {
throw new Error('useVuContext must be used within a VuProvider');
}
return context;
};
export default VuContext;

View File

@ -536,3 +536,8 @@ export const EVENTS = {
export const SYSTEM_DEFAULT_PLAYBACK_ONLY = 'System Default (Playback Only)';
export const JAMKAZAM_VIRTUAL_INPUT = "JamKazam Virtual Input";
export const SKIPPED_NETWORK_TEST = -1;
export const getAvatarUrl = (photo_url) => {
return photo_url ? photo_url : "/assets/shared/avatar_generic.png";
};

View File

@ -342,6 +342,7 @@ const useGearUtils = () => {
const guardAgainstBadNetworkScore = useCallback((app) => {
return new Promise(async (resolve, reject) => {
resolve();
if (!(await validNetworkScore())) {
reject();
} else {

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { generateUUID, getCookieValue, createCookie } from '../helpers/utils';
import { messageFactory, MessageType } from '../helpers/MessageFactory';
import { useJamClient } from '../context/JamClientContext';
@ -66,11 +66,15 @@ export default function useJamServer(url) {
try {
const clientType = 'client'; //TODO: make dynamic
const clientId = await getClientId(); //get from cookie or jamClient
//const clientId = await jamClient.clientID(); #This currently retuens empty string. need to add this as a method to c++ client. for now use hardcoded uuid
const clientId = generateUUID(); //TODO: replace with jamClient.clientID() when available
console.log("_DEBUG_ clientId", clientId);
server.current.clientId = clientId;
// Gather necessary parameters before establishing the connection
// Use Promise.all to wait for all async calls to complete
const operatingModePromise = await jamClient.getOperatingMode();
console.log("_DEBUG_ operatingModePromise", operatingModePromise);
const macHashPromise = await jamClient.SessionGetMacHash();
const osStringPromise = await jamClient.GetDetailedOS();
@ -81,6 +85,7 @@ export default function useJamServer(url) {
];
Promise.all(allPromises).then(async ([operatingMode, machine, osString]) => {
console.log("_DEBUG_ ", operatingMode, machine, osString)
const channelId = generateUUID();
const rememberToken = getCookieValue('remember_token');
@ -224,7 +229,7 @@ export default function useJamServer(url) {
}, [url, reconnectAttempts]);
const getClientId = (async () => {
return getCookieValue('client_id') || await jamClient.GetClientID() || '';
return getCookieValue('client_id') || await jamClient.clientID() || '';
});
const rememberLogin = useCallback((rememberToken, clientId) => {
@ -731,8 +736,9 @@ export default function useJamServer(url) {
};
}, []);
// Legacy compatibility - keep isConnected for existing code
const isConnected = connectionStatus === ConnectionStatus.CONNECTED;
const isConnected = useMemo(() => {
return connectionStatus === ConnectionStatus.CONNECTED;
}, [connectionStatus]);
return {
// Legacy properties for backward compatibility

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,30 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { throttle } from 'lodash'; // Add lodash for throttling
import { useJamClient } from '../context/JamClientContext';
//import { useJamClient } from '../context/JamClientContext';
import { MIX_MODES } from '../helpers/globals';
import { putTrackSyncChange } from '../helpers/rest';
import useVuHelpers from './useVuHelpers';
import { useCurrentSession } from '../context/CurrentSessionContext';
import MixerHelper from '../helpers/MixerHelper';
//import { useMixerHelper } from './useMixerHelper';
import { useCurrentSessionContext } from '../context/CurrentSessionContext';
import { useJamServerContext } from '../context/JamServerContext';
import { useMixersContext } from '../context/MixersContext';
/**
* Custom hook for managing mixer store state and actions.
* Converted from the original CoffeeScript Reflux store to modern React.
*/
export default function useMixerStore() {
const jamClient = useJamClient();
//const jamClient = useJamClient();
const { currentSession, inSession, isConnected } = useCurrentSessionContext();
const {
jamClient,
} = useJamServerContext();
const mixerHelper = useMixersContext();
const logger = console; // Replace with your logging mechanism if needed
const { updateVU } = useVuHelpers();
const { currentSession, inSession } = useCurrentSession();
//const { updateVU } = useVuHelpers();
// State management
const [metro, setMetro] = useState({ tempo: 120, cricket: false, sound: "Beep" });
@ -27,9 +34,9 @@ export default function useMixerStore() {
const [clientsWithAudioOverride, setClientsWithAudioOverride] = useState({});
const [vuMeterUpdatePrefMap, setVuMeterUpdatePrefMap] = useState({ count: 0 });
const [session, setSession] = useState(null);
const [masterMixers, setMasterMixers] = useState(null);
const [personalMixers, setPersonalMixers] = useState(null);
const [mixers, setMixers] = useState([]);
//const [masterMixers, setMasterMixers] = useState(null);
//const [personalMixers, setPersonalMixers] = useState(null);
//const [mixers, setMixers] = useState(null); //holds MixerHelper instance
// Refs for persistent values
const appRef = useRef(null);
@ -51,7 +58,7 @@ export default function useMixerStore() {
// Initialize global callbacks
useEffect(() => {
if (!window.JK) window.JK = {};
console.debug("MixerHelpers: setting up global callbacks");
console.debug("MixerStore: setting up global callbacks", mixerHelper.mixers.current);
//window.JK.HandleVolumeChangeCallback2 = handleVolumeChangeCallback;
//window.JK.HandleMetronomeCallback2 = handleMetronomeCallback;
window.JK.HandleBridgeCallback2 = handleBridgeCallback;
@ -84,12 +91,6 @@ export default function useMixerStore() {
// }
}, [jamClient]);
// Issue change - trigger re-render/update
const issueChange = useCallback(() => {
// In React, state updates will trigger re-renders
// This is equivalent to the original trigger method
}, [session]);
// // Callback handlers
// const handleVolumeChangeCallback = useCallback((mixerId, isLeft, value, isMuted) => {
// // TODO: Visually update mixer
@ -115,58 +116,59 @@ export default function useMixerStore() {
// issueChange();
// }, [session, masterMixers, personalMixers, noAudioUsers, clientsWithAudioOverride, issueChange]);
const handleBridgeCallback = useCallback(throttle((...args) => {
const vuMeterUpdateRate = localStorage.getItem('vuMeterUpdateRate') || 'fast';
setVuMeterUpdatePrefMap(prev => {
const newCount = (prev.count || 0) + 1;
if (vuMeterUpdateRate === 'medium' && newCount % 3 !== 0) {
return { count: newCount };
}
if (vuMeterUpdateRate === 'slow' && newCount % 9 !== 0) {
return { count: newCount };
}
const handleBridgeCallback = useCallback((vuData) => {
let eventName = null;
let mixerId = null;
const value = null;
let vuInfo = null;
// Reset count and process VU data
setVuMeterUpdatePrefMap({ count: 0 });
// const vuMeterUpdateRate = localStorage.getItem('vuMeterUpdateRate') || 'fast';
// this.vuMeterUpdatePrefMap['count'] = (this.vuMeterUpdatePrefMap['count'] || 0) + 1;
// Parse arguments into vuData format: groups of 3 (eventName, mixerId, value)
const vuData = [];
for (let i = 0; i < args.length; i += 3) {
if (args[i] === "vu" && i + 2 < args.length) {
const eventName = args[i];
const mixerId = args[i + 1];
const value = args[i + 2];
// Format: [eventName, mixerId, mode, leftValue, leftClipping, rightValue, rightClipping]
// For fake client, mode=0, clipping=false, left=right=value
vuData.push([eventName, mixerId, 0, value, false, value, false]);
// if ((vuMeterUpdateRate === 'medium') && ((this.vuMeterUpdatePrefMap['count'] % 3) !== 0)) {
// // skip this update
// return;
// }
// if ((vuMeterUpdateRate === 'slow') && ((this.vuMeterUpdatePrefMap['count'] % 9) !== 0)) {
// // skip this update
// return;
// }
//console.log('vuMeterUpdateRate', vuMeterUpdateRate, @vuMeterUpdatePrefMap['count'])
//this.vuMeterUpdatePrefMap['count'] = 0;
//console.log("_DEBUG_ XXX: ", vuData);
//const result = [];
for (vuInfo of vuData) {
eventName = vuInfo[0];
const vuVal = 0.0;
if (eventName === "vu") {
mixerId = vuInfo[1];
const mode = vuInfo[2];
const leftValue = vuInfo[3];
const leftClipping = vuInfo[4];
const rightValue = vuInfo[5];
const rightClipping = vuInfo[6];
// TODO - no guarantee range will be -80 to 20. Get from the
// GetControlState for this mixer which returns min/max
// value is a DB value from -80 to 20. Convert to float from 0.0-1.0
//console.log('handleBridgeCallback@mixers',@mixers)
console.log("mixerHelper.isReady", mixerHelper.isReady.current);
if (mixerHelper.isReady.current) {
console.log("mixerHelper handleBridgeCallback: ", mixerId, mode, leftValue, rightValue, leftClipping, rightClipping);
mixerHelper.updateVU(mixerId, mode, (leftValue + 80) / 80, leftClipping, (rightValue + 80) / 80, rightClipping);
}
}
}
for (const vuInfo of vuData) {
const eventName = vuInfo[0];
if (eventName === "vu") {
const mixerId = vuInfo[1];
const mode = vuInfo[2];
const leftValue = vuInfo[3];
const leftClipping = vuInfo[4];
const rightValue = vuInfo[5];
const rightClipping = vuInfo[6];
console.debug("_DEBUG_ handleBridgeCallback: ", mixerId, mode, leftValue, rightValue, leftClipping, rightClipping);
}, []);
setMixers(prevMixers => {
if (!prevMixers) return prevMixers;
updateVU(mixerId, mode, (leftValue + 80) / 80, leftClipping, (rightValue + 80) / 80, rightClipping);
return { ...prevMixers }; // Trigger re-render
});
}
}
return { count: 0 };
});
}, 100), []); // Throttle to max once per 100ms
// const handleBackingTrackSelectedCallback = useCallback(() => {
// console.debug("backing track selected");
@ -197,14 +199,16 @@ export default function useMixerStore() {
const newMasterMixers = await jamClient.SessionGetAllControlState(true);
const newPersonalMixers = await jamClient.SessionGetAllControlState(false);
setMasterMixers(newMasterMixers);
setPersonalMixers(newPersonalMixers);
// Initialize the mixerHelper with the constructor parameters
console.debug("MixerStore: onSessionChange: initializing mixerHelper", { newSession, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride });
const mixerHelper = new MixerHelper(newSession, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride, mixers?.mixMode || MIX_MODES.PERSONAL);
console.debug("MixerStore: onSessionChange - new mixers", mixerHelper.allMixers);
setMixers(mixerHelper.allMixers);
mixerHelper.updateMixerData(newSession, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride, MIX_MODES.PERSONAL);
// const m = useMixerHelper(newSession, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride, mixers?.mixMode || MIX_MODES.PERSONAL);
// console.debug("MixerStore: onSessionChange: ", m);
// setMixers(m);
}
}, []);
}, [jamClient, mixerHelper, metro, noAudioUsers, clientsWithAudioOverride, inSession, sessionEnded]);
// // Mixer actions
// const onMute = useCallback(async (mixersToMute, muting) => {
@ -470,7 +474,6 @@ export default function useMixerStore() {
return {
// State
// session,
mixers,
// metro,
// noAudioUsers,
// clientsWithAudioOverride,

View File

@ -1,11 +1,11 @@
import { useState, useCallback, useRef, useEffect, useContext } from 'react';
import { useCurrentSession } from '../context/CurrentSessionContext';
import { useCurrentSessionContext } 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();
const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSessionContext();
// State variables from original RecordingModel
const [currentRecording, setCurrentRecording] = useState(null);

View File

@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useCurrentSession } from '../context/CurrentSessionContext';
import { useCurrentSessionContext } from '../context/CurrentSessionContext';
const useSessionHelper = () => {
const { currentSession, inSession } = useCurrentSession();
const { currentSession, inSession } = useCurrentSessionContext();
const sessionHelper = useMemo(() => {
const inSessionCheck = () => {

View File

@ -1,7 +1,7 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { debounce } from 'lodash'; // Add lodash for debouncing
import { useJamClient } from '../context/JamClientContext';
import { useCurrentSession } from '../context/CurrentSessionContext';
import { useCurrentSessionContext } from '../context/CurrentSessionContext';
import useGearUtils from './useGearUtils';
import useTrackHelpers from './useTrackHelpers';
import useRecordingHelpers from './useRecordingHelpers';
@ -33,7 +33,7 @@ export default function useSessionModel(app, server, sessionScreen) {
const { isNoInputProfile } = useGearUtils();
const { getTrackInfo, getUserTracks } = useTrackHelpers();
const { getCurrentRecordingState, reset: resetRecordingState } = useRecordingHelpers();
const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSession();
const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSessionContext();
// State variables from original SessionModel
const [userTracks, setUserTracks] = useState(null);

View File

@ -1,14 +1,17 @@
import React, { useCallback, useRef } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
const logger = console;
export default function useVuHelpers() {
const registeredMixers = useRef({});
const [vuStates, setVuStates] = useState({});
const createQualifiedId = useCallback(mixer => {
return (mixer.mode ? 'M' : 'P') + mixer.id;
}, []);
// Legacy renderVU for backward compatibility
const renderVU = useCallback((userOptions = {}) => {
const renderVUDefaults = {
vuType: 'vertical',
@ -25,7 +28,7 @@ export default function useVuHelpers() {
lights.push(
<td
key={i}
className="vulight vu-green-off"
className="vulight vu-off"
style={{
width: vuType === 'horizontal' ? lightWidth : 'auto',
height: vuType === 'horizontal' ? 'auto' : lightHeight
@ -54,6 +57,7 @@ export default function useVuHelpers() {
);
}, []);
// Legacy updateVU for backward compatibility
const updateVU = useCallback((containerRef, value) => {
if (!containerRef?.current) return;
@ -70,7 +74,7 @@ export default function useVuHelpers() {
// Remove all light classes
$lights.forEach($light => {
$light.classList.remove('vu-green-off', 'vu-green-on', 'vu-red-off', 'vu-red-on');
$light.classList.remove('vu-green-off', 'vu-green-on', 'vu-red-off', 'vu-red-on', 'vu-off');
});
// Set the lights
@ -91,129 +95,103 @@ export default function useVuHelpers() {
}
}, []);
const registerVU = useCallback(
(type, mixer, someFunction, horizontal, lightCount, lights) => {
const fqId = createQualifiedId(mixer);
let registrations = registeredMixers.current[fqId];
if (!registrations) {
registrations = [];
registeredMixers.current[fqId] = registrations;
}
if (type === 'best') {
registrations.push({ type, ptr: someFunction, ptrCount: 1, horizontal, lightCount, lights });
} else {
// find the right registration and add left lights or right lights to it
let found = null;
found = registrations.find(registration => registration.ptr === someFunction) || null;
if (!found) {
found = { type, ptr: someFunction, ptrCount: 1, horizontal, lightCount };
registrations.push(found);
} else {
found.ptrCount++;
}
if (type === 'left') {
found.leftLights = lights;
} else {
found.rightLights = lights;
}
}
},
[createQualifiedId]
);
const unregisterVU = useCallback(
(mixer, someFunction) => {
const fqId = createQualifiedId(mixer);
let registrations = registeredMixers.current[fqId];
if (!registrations || registrations.length === 0) {
logger.debug('no registration found for:' + fqId, registrations, registeredMixers.current);
return;
}
registrations = registrations.filter(element => {
const isMatch = element.ptr === someFunction;
if (isMatch) {
element.ptrCount--;
const keepRegistration = element.ptrCount > 0;
if (!keepRegistration) {
logger.debug('getting rid of the registration; no more ptrs');
}
return keepRegistration;
} else {
return true;
}
});
registeredMixers.current[fqId] = registrations;
},
[createQualifiedId]
);
const updateSingleVU = useCallback((horizontal, lightCount, $lights, value, isClipping) => {
const lights = Math.round(value * lightCount);
const redSwitch = Math.round(lightCount * 0.6666667);
// Remove all light classes from all lights
$lights.forEach($light => {
$light.classList.remove('vu-green-off', 'vu-green-on', 'vu-red-off', 'vu-red-on');
});
// Set the lights
for (let i = 0; i < lightCount; i++) {
let colorClass = 'vu-green-';
let state = 'on';
if (i >= redSwitch) {
colorClass = 'vu-red-';
}
if (i >= lights) {
state = 'off';
}
const lightIndex = horizontal ? i : lightCount - i - 1;
if ($lights[lightIndex]) {
$lights[lightIndex].classList.add(colorClass + state);
}
}
// New React-like VU update function
const updateVuState = useCallback((mixerId, level, clipping = false) => {
setVuStates(prev => ({
...prev,
[mixerId]: { level, clipping }
}));
}, []);
const updateVU3 = useCallback(
(mixer, leftValue, leftClipping, rightValue, rightClipping) => {
const fqId = createQualifiedId(mixer);
logger.debug('useVuHelpers: updateVU3', { fqId, mixer, leftValue, rightValue });
const registrations = registeredMixers.current[fqId];
if (registrations) {
registrations.forEach(registration => {
const { horizontal, lightCount } = registration;
if (registration.type === 'best') {
const $lights = registration.lights;
updateSingleVU(horizontal, lightCount, $lights, leftValue, leftClipping);
} else {
if (mixer.stereo) {
updateSingleVU(horizontal, lightCount, registration.leftLights, leftValue, leftClipping);
updateSingleVU(horizontal, lightCount, registration.rightLights, rightValue, rightClipping);
} else {
updateSingleVU(horizontal, lightCount, registration.leftLights, leftValue, leftClipping);
updateSingleVU(horizontal, lightCount, registration.rightLights, leftValue, leftClipping);
}
}
});
}
// Update React state for declarative rendering
updateVuState(fqId, leftValue, leftClipping);
},
[createQualifiedId, updateSingleVU]
[createQualifiedId, updateVuState]
);
useEffect(() => {
return () => {
// Cleanup on unmount
setVuStates({});
};
}, []);
// Create a proper React component that can be used outside the hook
function VuMeterComponent({ mixerId, orientation = 'vertical', lightCount = 12, lightWidth = 3, lightHeight = 17 }) {
const vuState = vuStates[mixerId] || { level: 0, clipping: false };
const { level, clipping } = vuState;
const lights = [];
for (let i = 0; i < lightCount; i++) {
// Calculate if this light should be on based on the level
const lightThreshold = (i + 1) / lightCount;
const isOn = level >= lightThreshold;
let lightClass = 'vulight ';
if (isOn) {
if (clipping) {
lightClass += 'vu-red-on';
} else if (i >= Math.floor(lightCount * 0.75)) { // Red zone (top 25%)
lightClass += 'vu-red-on';
} else if (i >= Math.floor(lightCount * 0.5)) { // Yellow zone (middle 25%)
lightClass += 'vu-yellow-on';
} else { // Green zone (bottom 50%)
lightClass += 'vu-green-on';
}
} else {
lightClass += 'vu-off';
}
console.debug('VuMeterComponent render', { i, lightThreshold, isOn, lightClass });
lights.push(
<td
key={i}
className={lightClass}
style={{
width: orientation === 'horizontal' ? lightWidth : 'auto',
height: orientation === 'horizontal' ? 'auto' : lightHeight
}}
/>
);
}
const tableClass = `vu ${orientation}`;
const tableStyle = orientation === 'horizontal' ? { display: 'inline-block' } : {};
return (
<table className={tableClass} style={tableStyle}>
{orientation === 'vertical' ? (
<tbody>
{lights.map((light, index) => (
<tr key={index}>{light}</tr>
))}
</tbody>
) : (
<tbody>
<tr>{lights}</tr>
</tbody>
)}
</table>
);
}
return {
// React-like components and functions
VuMeter: VuMeterComponent,
updateVuState,
vuStates,
// Legacy functions for backward compatibility
renderVU,
updateVU,
registerVU,
unregisterVU,
updateVU3,
updateSingleVU
updateVU3
};
}

View File

@ -2,6 +2,7 @@
// Ported from web/app/assets/javascripts/jamClientProxy.js
import { QWebChannel } from 'qwebchannel'
// import useJamServer from './hooks/useJamServer';
class Deferred {
constructor(request_id) {
@ -24,10 +25,10 @@ class JamClientProxy {
this.request_id = 1;
this.skipLogMethods = [];
this.displayLogMethod = [];
}
// const server = useJamServer();
// this.JamServer = server;
get JKFrontendMethods() {
return Object.freeze({
this.JKFrontendMethods = Object.freeze({
UnknownJKAppMessage: this.enumAppCounter++,
AbortRecording: this.enumAppCounter++,
addUserBackingTracksToJamkazamAsset: this.enumAppCounter++,
@ -455,7 +456,7 @@ class JamClientProxy {
if (deferred) deferred.resolve(null);
} else {
let msg = JSON.parse(message);
console.log("[jamClientProxy] Message received via QWebChannel: ", msg);
//console.log("[jamClientProxy] Message received via QWebChannel: ", msg);
let req_id = msg.request_id;
let response = msg.response;
let evt_id = msg.event_id;
@ -467,7 +468,7 @@ class JamClientProxy {
if (this.skipLogMethods.length > 0 && this.skipLogMethods.includes(methodName)) {
// Skip logging
} else if (this.displayLogMethod.includes(methodName)) {
this.logger.log('[jamClientProxy] Message received via QWebChannel: ', msg);
//this.logger.log('[jamClientProxy] Message received via QWebChannel: ', msg);
}
deferred.resolve(response);
@ -496,21 +497,11 @@ class JamClientProxy {
socket.onerror = error => { }; // Handle error if needed
}
handleEvent(evt_id, response) {
// const method = Object.keys(response)[0]; // Available if needed for debugging
switch (evt_id.toString()) {
case '3006': // execute_script
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']);
@ -534,7 +525,7 @@ class JamClientProxy {
break;
case '3010': // JKVideoSession
this.logger.log(`[jamClientProxy] 3010 JKVideoSession: ${response['JKVideoSession']['connect']}`);
//this.logger.log(`[jamClientProxy] 3010 JKVideoSession: ${response['JKVideoSession']['connect']}`);
const vidConnect = response['JKVideoSession']['connect'];
if (this.context.ExternalVideoActions) {
this.context.ExternalVideoActions.setVideoEnabled(vidConnect);
@ -545,7 +536,7 @@ class JamClientProxy {
break;
case '3011': // AudioFormatChangeEvent
this.logger.log(`[jamClientProxy] 3011 AudioFormatChangeEvent: ${response['AudioFormat']}`);
//this.logger.log(`[jamClientProxy] 3011 AudioFormatChangeEvent: ${response['AudioFormat']}`);
const audioFormat = response['AudioFormat'];
if (this.context.RecordingActions) {
this.context.RecordingActions.audioRecordingFormatChanged(`.${audioFormat}`);
@ -616,12 +607,15 @@ class JamClientProxy {
}
sendMessage(prop, args) {
let appMessage = {
request_id: ++this.request_id,
arguments: Array.from(args) || [],
method: this.JKFrontendMethods[prop]
};
//console.log('_DEBUG_ appMessage', prop, appMessage);
let deferred = new Deferred(appMessage.request_id);
if (this.skipLogMethods.length > 0 && this.skipLogMethods.includes(prop)) {

View File

@ -7,7 +7,10 @@ import { JamKazamAppProvider } from '../context/JamKazamAppContext';
import { AppDataProvider } from '../context/AppDataContext';
import { AppRoutesProvider } from '../context/AppRoutesContext';
import { JamClientProvider } from '../context/JamClientContext';
import { JamServerProvider } from '../context/JamServerContext';
import { CurrentSessionProvider } from '../context/CurrentSessionContext';
import { MixersProvider } from '../context/MixersContext';
import { VuProvider } from '../context/VuContext';
const JKClientLayout = ({ location }) => {
@ -19,9 +22,15 @@ const JKClientLayout = ({ location }) => {
<JamKazamAppProvider>
<BrowserQueryProvider>
<JamClientProvider>
<CurrentSessionProvider>
<ClientRoutes />
</CurrentSessionProvider>
<JamServerProvider>
<CurrentSessionProvider>
<VuProvider>
<MixersProvider>
<ClientRoutes />
</MixersProvider>
</VuProvider>
</CurrentSessionProvider>
</JamServerProvider>
</JamClientProvider>
</BrowserQueryProvider>
</JamKazamAppProvider>

View File

@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
import NavbarTop from '../components/navbar/JKNavbarTop';
import NavbarVertical from '../components/navbar/JKNavbarVertical';
import Footer from '../components/footer/JKFooter';
import DebugPage from '../components/client/DebugPage';
// Import your page components here
import JKSessionScreen from '../components/client/JKSessionScreen';
@ -29,6 +30,7 @@ const JKClientRoutes = ({ match: { url } }) => {
<NavbarTop logoWidth={240} />
<Switch>
<PrivateRoute exact path={`${url}/s/:id`} component={JKSessionScreenWithErrorBoundary} />
{/* <PrivateRoute exact path={`${url}/s/:id`} component={DebugPage} /> */}
{/*Redirect*/}
<Redirect to="/errors/404" />
</Switch>