From c13bc738066a894f792f9bf78798fd1630d62455 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Fri, 6 Mar 2026 09:52:28 +0530 Subject: [PATCH] revert(31,32): restore pre-optimization state to fix VU/slider perf Phase 31 selector optimization and Phase 32 state update changes caused VU meters to animate slowly and volume sliders to be unresponsive. Reverted all affected files to pre-Phase-31 state. Files reverted: - useMixerHelper.js: back to individual selectors (no shallowEqual) - mixersSlice.js: removed composed selectors - JKSessionScreen.js: pre-Phase-32 state - useSessionModel.js: pre-Phase-32 debounce changes Performance issues resolved: - VU meter animations now smooth - Volume sliders respond immediately Co-Authored-By: Claude Opus 4.5 --- .../src/components/client/JKSessionScreen.js | 1016 +++++++------- jam-ui/src/hooks/useMixerHelper.js | 1202 +++++++---------- jam-ui/src/hooks/useSessionModel.js | 868 ++++++------ jam-ui/src/store/features/mixersSlice.js | 68 +- 4 files changed, 1401 insertions(+), 1753 deletions(-) diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js index 26ffd1992..3478cc953 100644 --- a/jam-ui/src/components/client/JKSessionScreen.js +++ b/jam-ui/src/components/client/JKSessionScreen.js @@ -1,10 +1,9 @@ // jam-ui/src/components/client/JKSessionScreen.js -import React, { useEffect, useRef, useState, memo, useMemo, useCallback } from 'react'; +import React, { useEffect, useRef, useState, memo, useMemo, useCallback } from 'react' import { useParams, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; -import { debounce } from 'lodash'; -import useGearUtils from '../../hooks/useGearUtils'; +import useGearUtils from '../../hooks/useGearUtils' import useSessionUtils from '../../hooks/useSessionUtils.js'; import useSessionModel from '../../hooks/useSessionModel.js'; import useSessionHelper from '../../hooks/useSessionHelper.js'; @@ -20,32 +19,11 @@ import useMediaActions from '../../hooks/useMediaActions'; import { dkeys } from '../../helpers/utils.js'; -import { - getSessionHistory, - getSession, - joinSession as joinSessionRest, - updateSessionSettings, - getFriends, - startRecording, - stopRecording, - submitSessionFeedback, - getVideoConferencingRoomUrl, - getJamTrack, - closeJamTrack, - openMetronome -} from '../../helpers/rest'; +import { getSessionHistory, getSession, joinSession as joinSessionRest, updateSessionSettings, getFriends, startRecording, stopRecording, submitSessionFeedback, getVideoConferencingRoomUrl, getJamTrack, closeJamTrack, openMetronome } from '../../helpers/rest'; import { syncTracksToServer } from '../../services/trackSyncService'; // Redux imports -import { - openModal, - closeModal, - toggleModal, - selectModal, - selectIsRecording, - setRecordingStarted, - setRecordingStopped -} from '../../store/features/sessionUISlice'; +import { openModal, closeModal, toggleModal, selectModal, selectIsRecording, setRecordingStarted, setRecordingStopped } from '../../store/features/sessionUISlice'; import { selectMediaSummary, selectMetronomeTrackMixers, selectMixersReady } from '../../store/features/mixersSlice'; import { fetchActiveSession, @@ -76,39 +54,14 @@ import { selectBackingTrackData, selectJamTrackData } from '../../store/features/activeSessionSlice'; -import { - addMessageFromWebSocket, - uploadAttachment, - selectIsUploading, - selectUploadError, - selectUploadFileName, - selectUploadStatus, - clearUploadError, - fetchChatHistory, - clearAllMessages -} from '../../store/features/sessionChatSlice'; +import { addMessageFromWebSocket, uploadAttachment, selectIsUploading, selectUploadError, selectUploadFileName, selectUploadStatus, clearUploadError, fetchChatHistory, clearAllMessages } from '../../store/features/sessionChatSlice'; import { validateFile } from '../../services/attachmentValidation'; import { checkJamTrackSync } from '../../store/features/mediaSlice'; import { CLIENT_ROLE, RECORD_TYPE_AUDIO, RECORD_TYPE_BOTH } from '../../helpers/globals'; import { MessageType } from '../../helpers/MessageFactory.js'; -import { - Alert, - Col, - Row, - Button, - Card, - CardBody, - Modal, - ModalHeader, - ModalBody, - ModalFooter, - CardHeader, - Badge, - UncontrolledTooltip, - Spinner -} from 'reactstrap'; +import { Alert, Col, Row, Button, Card, CardBody, Modal, ModalHeader, ModalBody, ModalFooter, CardHeader, Badge, UncontrolledTooltip, Spinner } from 'reactstrap'; import FalconCardHeader from '../common/FalconCardHeader'; //import SessionTrackVU from './SessionTrackVU.js'; //import JKSessionMyTrack from './JKSessionMyTrack.js'; @@ -131,8 +84,6 @@ import JKSessionMetronomePlayer from './JKSessionMetronomePlayer.js'; import JKSessionChatWindow from './JKSessionChatWindow.js'; import JKSessionChatButton from './JKSessionChatButton.js'; import JKPopupMediaControls from '../popups/JKPopupMediaControls.js'; -import JKResyncButton from './JKResyncButton.js'; -import JKVideoButton from './JKVideoButton.js'; import { SESSION_PRIVACY_MAP } from '../../helpers/globals.js'; import { toast } from 'react-toastify'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -141,22 +92,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import gearIcon from '../../assets/img/client/gear.svg'; import inviteIcon from '../../assets/img/client/invite.svg'; import volumeIcon from '../../assets/img/client/volume.svg'; +import videoIcon from '../../assets/img/client/video.svg'; import recordIcon from '../../assets/img/client/record.svg'; import broadcastIcon from '../../assets/img/client/broadcast.svg'; import openIcon from '../../assets/img/client/open.svg'; import chatIcon from '../../assets/img/client/chat.svg'; import attachIcon from '../../assets/img/client/attach.svg'; +import resyncIcon from '../../assets/img/client/resync.svg'; import helpIcon from '../../assets/img/client/help.svg'; const JKSessionScreen = () => { - // TEMPORARY: Render counter for Phase 32 verification - // Remove after verifying optimization - const renderCountRef = React.useRef(0); - renderCountRef.current += 1; - if (process.env.NODE_ENV === 'development') { - console.log(`[JKSessionScreen] Render #${renderCountRef.current}`); - } - const logger = console; // Replace with another logging mechanism if needed const dispatch = useDispatch(); const app = useJamKazamApp(); @@ -166,13 +111,12 @@ const JKSessionScreen = () => { guardAgainstInvalidConfiguration, guardAgainstActiveProfileMissing, guardAgainstSinglePlayerProfile, - resyncAudio + resyncAudio, } = useGearUtils(); const { initialize: initializeMixer, onSessionChange } = useMixerStore(); const mixerHelper = useMixersContext(); - const { - isConnected, + const { isConnected, ConnectionStatus, connectionStatus, reconnectAttempts, @@ -180,8 +124,7 @@ const JKSessionScreen = () => { jamClient, server, registerMessageCallback, - unregisterMessageCallback - } = useJamServerContext(); + unregisterMessageCallback } = useJamServerContext(); // Phase 4: Replace CurrentSessionContext with Redux const currentSession = useSelector(selectActiveSession); @@ -255,6 +198,12 @@ const JKSessionScreen = () => { const [leaveComments, setLeaveComments] = useState(''); const [leaveLoading, setLeaveLoading] = useState(false); + //state for video button + const [videoLoading, setVideoLoading] = useState(false); + + // State for resync button + const [resyncLoading, setResyncLoading] = useState(false); + // Redux backing track state (modal visibility and data) const backingTrackData = useSelector(selectBackingTrackData); const showBackingTrackPlayer = Boolean(backingTrackData); @@ -405,8 +354,7 @@ const JKSessionScreen = () => { // console.debug("-DEBUG- JKSessionScreen: isConnected changed to true"); guardOnJoinSession(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isConnected, jamClient]); // guardOnJoinSession defined below, accessed via closure + }, [isConnected, jamClient]); // Added jamClient to dependencies for stability const guardOnJoinSession = async () => { // console.log("-DEBUG- JKSessionScreen: guardOnJoinSession") @@ -446,10 +394,11 @@ const JKSessionScreen = () => { dispatch(setUserTracks(tracks)); // logger.log("-DEBUG- JKSessionScreen: userTracks: ", tracks); try { - await ensureAppropriateProfile(musicianAccessOnJoin); + await ensureAppropriateProfile(musicianAccessOnJoin) // logger.log("-DEBUG- JKSessionScreen: user has passed all session guards") - dispatch(setGuardsPassed(true)); + dispatch(setGuardsPassed(true)) + } catch (error) { // logger.error("-DEBUG- JKSessionScreen: User profile is not appropriate for session:", error); // If error doesn't control navigation, show alert (legacy behavior shows dialog) @@ -458,8 +407,8 @@ const JKSessionScreen = () => { // TODO: Replace with proper JKSessionProfileDialog component alert( 'Your current audio profile is not suitable for multi-user sessions.\n\n' + - 'Please configure a proper audio interface in your audio settings, ' + - 'or create a private session for solo practice.' + 'Please configure a proper audio interface in your audio settings, ' + + 'or create a private session for solo practice.' ); await sessionModel.handleLeaveSession(); history.push('/'); // Redirect to dashboard @@ -469,7 +418,7 @@ const JKSessionScreen = () => { } } catch (error) { // logger.error("-DEBUG- JKSessionScreen: Error: waiting for session page enter to complete:", error); - if (error === 'timeout') { + 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 @@ -489,6 +438,7 @@ const JKSessionScreen = () => { await sessionModel.handleLeaveSession(); //TODO: handle redirection // logger.error("-DEBUG- JKSessionScreen: Error: Invalid configuration:", error); } + } catch (error) { // logger.error("-DEBUG- JKSessionScreen: Error: Error fetching session history:", error); //TODO: Show some error @@ -496,37 +446,43 @@ const JKSessionScreen = () => { }; useEffect(() => { - if (!sessionGuardsPassed || userTracks.length === 0 || hasJoined) { - return; - } + if (!sessionGuardsPassed || userTracks.length === 0 || hasJoined) { return } joinSession(); - // eslint-disable-next-line react-hooks/exhaustive-deps -}, [sessionGuardsPassed, userTracks, hasJoined]); // joinSession defined below, accessed via closure + }, [sessionGuardsPassed, userTracks, hasJoined]) - // Create stable debounced sync function - const syncTracksDebounced = useMemo( - () => - debounce((sid, cid, d) => { - d(syncTracksToServer(sid, cid)); - }, 1500), // 1.5s delay - adequate for mixer initialization - [] - ); - - // Track sync: Single debounced call when session joined - // Replaces legacy 3-timer pattern (1s, 1.4s, 6s) with single 1.5s debounced call + // Track sync: Sync tracks to server when session joined (3-call pattern matching legacy) // IMPORTANT: Wait for mixers to be ready before syncing to avoid race condition with mixer initialization useEffect(() => { if (!hasJoined || !sessionId || !server?.clientId || !mixersReady) { return; } - // console.log('[Track Sync] Mixers ready, scheduling single debounced sync'); - syncTracksDebounced(sessionId, server.clientId, dispatch); + // console.log('[Track Sync] Mixers ready, scheduling track sync calls'); - return () => syncTracksDebounced.cancel(); + // First sync: Initial setup (~1s after join) + const timer1 = setTimeout(() => { + dispatch(syncTracksToServer(sessionId, server.clientId)); + }, 1000); + + // Second sync: Refinement (~1.4s after join) + const timer2 = setTimeout(() => { + dispatch(syncTracksToServer(sessionId, server.clientId)); + }, 1400); + + // Third sync: Final config (~6s after join) + const timer3 = setTimeout(() => { + dispatch(syncTracksToServer(sessionId, server.clientId)); + }, 6000); + + // Cleanup timers on unmount or if hasJoined/sessionId changes + return () => { + clearTimeout(timer1); + clearTimeout(timer2); + clearTimeout(timer3); + }; // Note: server intentionally NOT in deps to avoid re-running on server reference changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasJoined, sessionId, mixersReady, dispatch, syncTracksDebounced]); + }, [hasJoined, sessionId, mixersReady, dispatch]) // Fetch chat history when session joins to populate unread badge // This ensures unread count persists across page reloads @@ -535,25 +491,18 @@ const JKSessionScreen = () => { return; } - dispatch( - fetchChatHistory({ - channel: sessionId, - sessionId: sessionId - }) - ); + dispatch(fetchChatHistory({ + channel: sessionId, + sessionId: sessionId + })); }, [hasJoined, sessionId, dispatch]); - const joinSession = useCallback(async () => { + const joinSession = async () => { + await jamClient.SetVURefreshRate(150); - await jamClient.SessionRegisterCallback('JK.HandleBridgeCallback2'); - await jamClient.SessionSetAlertCallback('JK.HandleAlertCallback'); - await jamClient.RegisterRecordingCallbacks( - 'JK.HandleRecordingStartResult', - 'JK.HandleRecordingStopResult', - 'JK.HandleRecordingStarted', - 'JK.HandleRecordingStopped', - 'JK.HandleRecordingAborted' - ); + await jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); + await jamClient.SessionSetAlertCallback("JK.HandleAlertCallback"); + await jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); await jamClient.SessionSetConnectionStatusRefreshRate(1000); @@ -561,13 +510,15 @@ 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) { clientRole = 'parent'; } - if (clientRole === '' || !clientRole) { + if ((clientRole === '') || !clientRole) { clientRole = null; } @@ -576,11 +527,13 @@ const JKSessionScreen = () => { // tell the server we want to join + + //const clientId = await jamClient.clientID(); const clientId = server.clientId; // console.log("joining session " + sessionId + " as client " + JSON.stringify(clientId) + " with role " + clientRole + " and parent client " + parentClientId); - const latency = await jamClient.FTUEGetExpectedLatency().latency; + const latency = await jamClient.FTUEGetExpectedLatency().latency // console.log("joinSession parameters: ", { // client_id: clientId, @@ -601,147 +554,150 @@ const JKSessionScreen = () => { session_id: sessionId, client_role: clientRole, parent_client_id: parentClientId, - audio_latency: latency + audio_latency: latency, + }).then(async (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); + + // Update Redux state - user has successfully joined + dispatch(joinActiveSession.fulfilled(data, '', { sessionId, options: {} })); + + 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"); + sessionModel.leaveSessionRest(); + } + + sessionModel.updateSessionInfo(data, true); + dispatch(updateSessionData(data)); // Phase 4: dispatch to Redux + + //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); + // } + // } + + // this.recordingModel.reset(this.currentSessionId); //TODO: implement recording model + + const joinSessionMsg = { + sessionID: currentSession.id, + music_session_id_int: response.music_session_id_int + }; + + await jamClient.JoinSession(joinSessionMsg); + + //@refreshCurrentSession(true); + + // Chat message handler - receives messages from WebSocket and dispatches to Redux + // Callback signature: (fullMessage, payload) where payload contains the chat_message data + const handleChatMessage = (fullMessage, payload) => { + // Transform Protocol Buffer format to Redux format + // payload contains: {sender_name, sender_id, msg, msg_id, created_at, channel, ...} + const message = { + id: payload.msg_id, + senderId: payload.sender_id, + senderName: payload.sender_name, + message: payload.msg, + createdAt: payload.created_at, + channel: payload.channel || 'session', + sessionId: currentSession.id, + // Attachment fields (null/undefined if not an attachment) + purpose: payload.purpose, // 'Notation File', 'Audio File', or undefined + attachmentId: payload.attachment_id, // MusicNotation UUID + attachmentType: payload.attachment_type, // 'notation' or 'audio' + attachmentName: payload.attachment_name, // filename + attachmentSize: payload.attachment_size // file size in bytes (may be null) + }; + dispatch(addMessageFromWebSocket(message)); + }; + + // Register message callbacks and store references for cleanup + const callbacksToRegister = [ + { type: MessageType.SESSION_JOIN, callback: trackChanges }, + { type: MessageType.SESSION_DEPART, callback: trackChanges }, + { type: MessageType.TRACKS_CHANGED, callback: trackChanges }, + { type: MessageType.HEARTBEAT_ACK, callback: trackChanges }, + { type: MessageType.CHAT_MESSAGE, callback: handleChatMessage } + ]; + + callbacksToRegister.forEach(({ type, callback }) => { + registerMessageCallback(type, callback); + }); + + // Store registered callbacks for cleanup (both state and ref for reliable cleanup) + setRegisteredCallbacks(callbacksToRegister); + registeredCallbacksRef.current = callbacksToRegister; + + //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) => { + // console.error("joinSessionRest error: ", xhr); + let leaveBehavior; + sessionModel.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) { + //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 + } }) - .then(async 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); - // Update Redux state - user has successfully joined - dispatch(joinActiveSession.fulfilled(data, '', { sessionId, options: {} })); - - 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"); - sessionModel.leaveSessionRest(); - } - - sessionModel.updateSessionInfo(data, true); - dispatch(updateSessionData(data)); // Phase 4: dispatch to Redux - - //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); - // } - // } - - // this.recordingModel.reset(this.currentSessionId); //TODO: implement recording model - - const joinSessionMsg = { - sessionID: currentSession.id, - music_session_id_int: response.music_session_id_int - }; - - await jamClient.JoinSession(joinSessionMsg); - - //@refreshCurrentSession(true); - - // Chat message handler - receives messages from WebSocket and dispatches to Redux - // Callback signature: (fullMessage, payload) where payload contains the chat_message data - const handleChatMessage = (fullMessage, payload) => { - // Transform Protocol Buffer format to Redux format - // payload contains: {sender_name, sender_id, msg, msg_id, created_at, channel, ...} - const message = { - id: payload.msg_id, - senderId: payload.sender_id, - senderName: payload.sender_name, - message: payload.msg, - createdAt: payload.created_at, - channel: payload.channel || 'session', - sessionId: currentSession.id, - // Attachment fields (null/undefined if not an attachment) - purpose: payload.purpose, // 'Notation File', 'Audio File', or undefined - attachmentId: payload.attachment_id, // MusicNotation UUID - attachmentType: payload.attachment_type, // 'notation' or 'audio' - attachmentName: payload.attachment_name, // filename - attachmentSize: payload.attachment_size // file size in bytes (may be null) - }; - dispatch(addMessageFromWebSocket(message)); - }; - - // Register message callbacks and store references for cleanup - const callbacksToRegister = [ - { type: MessageType.SESSION_JOIN, callback: trackChanges }, - { type: MessageType.SESSION_DEPART, callback: trackChanges }, - { type: MessageType.TRACKS_CHANGED, callback: trackChanges }, - { type: MessageType.HEARTBEAT_ACK, callback: trackChanges }, - { type: MessageType.CHAT_MESSAGE, callback: handleChatMessage } - ]; - - callbacksToRegister.forEach(({ type, callback }) => { - registerMessageCallback(type, callback); - }); - - // Store registered callbacks for cleanup (both state and ref for reliable cleanup) - setRegisteredCallbacks(callbacksToRegister); - registeredCallbacksRef.current = callbacksToRegister; - - //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 => { - // console.error("joinSessionRest error: ", xhr); - let leaveBehavior; - sessionModel.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) { - //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 - } - }); - }); + } useEffect(() => { if (!isConnected || !hasJoined) return; onSessionChange(sessionHelper); - }, [isConnected, hasJoined, onSessionChange, sessionHelper]); + }, [isConnected, hasJoined, sessionHelper.id()]); - const ensureAppropriateProfile = async musicianAccess => { - return new Promise(async function(resolve, reject) { + const ensureAppropriateProfile = async (musicianAccess) => { + return new Promise(async function (resolve, reject) { if (musicianAccess) { try { await guardAgainstSinglePlayerProfile(app); resolve(); } catch (error) { - reject(error); + reject(error) } } else { resolve(); } - }); + }) }; // Use sessionModel functions @@ -752,7 +708,7 @@ const JKSessionScreen = () => { const musicianAccess = useMemo(() => { if (!currentSession) return null; return sessionModel.getMusicianAccess(); - }, [currentSession, sessionModel]); + }, [currentSession]); // Memoize chat object to prevent unnecessary re-renders const chat = useMemo(() => { @@ -786,21 +742,15 @@ const JKSessionScreen = () => { // Monitor connection status changes useEffect(() => { - if (connectionStatus === ConnectionStatus.DISCONNECTED || connectionStatus === ConnectionStatus.ERROR) { + if (connectionStatus === ConnectionStatus.DISCONNECTED || + connectionStatus === ConnectionStatus.ERROR) { dispatch(setConnectionStatus('disconnected')); } else if (connectionStatus === ConnectionStatus.CONNECTED) { dispatch(setConnectionStatus('connected')); } else if (connectionStatus === ConnectionStatus.RECONNECTING) { dispatch(setConnectionStatus('reconnecting')); } - }, [ - ConnectionStatus.CONNECTED, - ConnectionStatus.DISCONNECTED, - ConnectionStatus.ERROR, - ConnectionStatus.RECONNECTING, - connectionStatus, - dispatch - ]); + }, [connectionStatus, dispatch]); // Handlers for recording and playback // const handleStartRecording = () => { @@ -849,10 +799,11 @@ const JKSessionScreen = () => { // // Handle VU meter updates // }; + + useEffect(() => { fetchFriends(); - // eslint-disable-next-line react-hooks/exhaustive-deps -}, []); // fetchFriends defined below, runs once on mount + }, []); const fetchFriends = () => { if (currentUser) { @@ -868,12 +819,12 @@ const JKSessionScreen = () => { } }; - const handleRecordingSubmit = async settings => { + const handleRecordingSubmit = async (settings) => { settings.volume = getCurrentRecordingState().inputVolumeLevel; try { - localStorage.setItem('recordSettings', JSON.stringify(settings)); + localStorage.setItem("recordSettings", JSON.stringify(settings)); } catch (e) { - logger.info('error while saving recordSettings to localStorage'); + logger.info("error while saving recordSettings to localStorage"); logger.log(e.stack); } @@ -883,7 +834,7 @@ const JKSessionScreen = () => { audioFormat: settings.audioFormat, audioStoreType: settings.audioStoreType, includeChat: settings.includeChat, - volume: settings.volume + volume: settings.volume, }; if (params.recordingType === RECORD_TYPE_BOTH) { @@ -893,28 +844,25 @@ const JKSessionScreen = () => { const obsAvailable = await jamClient.IsOBSAvailable(); if (!obsAvailable) { - toast.warning( - 'OBS Studio is not available. Please ensure OBS Studio is installed and running to record video.' - ); + toast.warning("OBS Studio is not available. Please ensure OBS Studio is installed and running to record video."); return; } if (!globalObject.JK.videoIsOngoing) { - toast.warning( - 'To make a video recording in JamKazam you must have an ongoing video. You can start a video by clicking the Video button on session tool bar.' - ); + toast.warning("To make a video recording in JamKazam you must have an ongoing video. You can start a video by clicking the Video button on session tool bar."); return; } } //this.startStopRecording(params); //TODO: handle startStopRecording doStartRecording(params); - }; + + } function groupTracksToClient(recording) { // group N tracks to the same client Id let groupedTracks = {}; - let recordingTracks = recording['recorded_tracks']; + let recordingTracks = recording["recorded_tracks"]; for (let i = 0; i < recordingTracks.length; i++) { let clientId = recordingTracks[i].client_id; @@ -928,29 +876,27 @@ const JKSessionScreen = () => { return dkeys(groupedTracks); } - const doStartRecording = params => { - startRecording({ music_session_id: currentSession.id, recordVideo: params.recordVideo }) - .then(async recording => { - const currentRecordingId = recording.id; - // console.debug("Recording started with ID: ", currentRecordingId); - const groupedTracks = groupTracksToClient(recording); - try { - await jamClient.StartMediaRecording(currentRecordingId, groupedTracks, params); - } catch (error) { - // console.error("Error starting media recording:", error); - } - }) - .catch(error => { - // console.error("Error starting recording:", error); - }); - }; + const doStartRecording = (params) => { + startRecording({ music_session_id: currentSession.id, recordVideo: params.recordVideo }).then(async (recording) => { + const currentRecordingId = recording.id; + // console.debug("Recording started with ID: ", currentRecordingId); + const groupedTracks = groupTracksToClient(recording); + try { + await jamClient.StartMediaRecording(currentRecordingId, groupedTracks, params); + } catch (error) { + // console.error("Error starting media recording:", error); + } + }).catch((error) => { + // console.error("Error starting recording:", error); + }); + } const handleLeaveSession = () => { // Just show the modal - no leave operations yet dispatch(openModal('leave')); }; - const handleLeaveSubmit = async feedbackData => { + const handleLeaveSubmit = async (feedbackData) => { try { setLeaveLoading(true); @@ -1013,7 +959,7 @@ const JKSessionScreen = () => { // Clear Redux session state on unmount dispatch(clearSession()); }; - }, [metronomeState.isOpen, resetMetronome, dispatch, unregisterMessageCallbacks]); + }, [metronomeState.isOpen, resetMetronome, dispatch]); // Check if user can use video (subscription/permission check) const canVideo = () => { @@ -1022,6 +968,40 @@ const JKSessionScreen = () => { return currentSession?.can_use_video || false; }; + // Open external link in new window/tab + const openExternalLink = (url) => { + window.open(url, '_blank', 'noopener,noreferrer'); + }; + + // Handle video button click - opens new video conferencing server + const handleVideoClick = async () => { + if (!canVideo()) { + // Show upgrade modal/banner + showVideoUpgradePrompt(); + return; + } + + try { + setVideoLoading(true); + + // Get video conferencing room URL from server + const response = await getVideoConferencingRoomUrl(currentSession.id); + const videoUrl = `${response.url}&audiooff=true`; + + // Open video URL in new browser window/tab + // console.debug("JKSessionScreen: Opening video conferencing URL", videoUrl); + openExternalLink(videoUrl); + + } catch (error) { + // console.error('Failed to get video room URL:', error); + // Handle error - could show error message to user + toast.error('Failed to start video session'); + } finally { + // Keep loading state for 10 seconds to prevent multiple clicks + setTimeout(() => setVideoLoading(false), 10000); + } + }; + // Show upgrade prompt for users without video permissions const showVideoUpgradePrompt = () => { // Implementation for showing upgrade modal/banner @@ -1034,19 +1014,16 @@ const JKSessionScreen = () => {
Upgrade Required

The video feature requires a premium subscription. Please upgrade your plan to access video conferencing.

-
); - const handleBroadcast = async e => { + const handleBroadcast = async (e) => { e.preventDefault(); try { await jamClient.LaunchBroadcastSettings(); @@ -1076,6 +1053,27 @@ const JKSessionScreen = () => { } }; + // Handle Resync button click - performs audio resync via native client + const handleResync = useCallback(async (e) => { + e.preventDefault(); + if (resyncLoading) return; + + setResyncLoading(true); + try { + await resyncAudio(); + // Silent success (matches legacy behavior) + } catch (error) { + // console.error('Audio resync failed:', error); + if (error.message === 'timeout') { + toast.error('Audio resync timed out. Please try again.'); + } else { + toast.error('Audio resync failed: ' + (error.message || 'Unknown error')); + } + } finally { + setResyncLoading(false); + } + }, [resyncAudio, resyncLoading]); + // Attach button handlers const handleAttachClick = useCallback(() => { if (attachFileInputRef.current) { @@ -1083,46 +1081,41 @@ const JKSessionScreen = () => { } }, []); - const handleFileSelect = useCallback( - e => { - const file = e.target.files?.[0]; - if (!file) return; + const handleFileSelect = useCallback((e) => { + const file = e.target.files?.[0]; + if (!file) return; - // Reset input for re-selection of same file - e.target.value = ''; + // Reset input for re-selection of same file + e.target.value = ''; - // Validate file before upload - const validation = validateFile(file); + // Validate file before upload + const validation = validateFile(file); - if (!validation.valid) { - toast.error(validation.error); - return; - } + if (!validation.valid) { + toast.error(validation.error); + return; + } - // Show warnings if any (e.g., backend may not fully support this type) - if (validation.warnings) { - validation.warnings.forEach(warning => { - // console.warn('Attachment warning:', warning); - }); - } + // Show warnings if any (e.g., backend may not fully support this type) + if (validation.warnings) { + validation.warnings.forEach(warning => { + // console.warn('Attachment warning:', warning); + }); + } - // Open chat window if not already open - dispatch(openModal('chat')); + // Open chat window if not already open + dispatch(openModal('chat')); - // Dispatch upload with user info for optimistic update - // (sender is excluded from WebSocket broadcast, so we add message locally) - dispatch( - uploadAttachment({ - file, - sessionId, - clientId: server?.clientId, - userId: currentUser?.id, - userName: currentUser?.name - }) - ); - }, - [currentUser.id, currentUser.name, dispatch, server.clientId, sessionId] - ); + // Dispatch upload with user info for optimistic update + // (sender is excluded from WebSocket broadcast, so we add message locally) + dispatch(uploadAttachment({ + file, + sessionId, + clientId: server?.clientId, + userId: currentUser?.id, + userName: currentUser?.name + })); + }, [dispatch, sessionId, server?.clientId, currentUser?.id, currentUser?.name]); // Show error toast when upload fails useEffect(() => { @@ -1141,7 +1134,7 @@ const JKSessionScreen = () => { prevUploadStatusRef.current = uploadStatus; }, [uploadStatus]); - const handleBackingTrackSelected = async result => { + const handleBackingTrackSelected = async (result) => { // console.log('JKSessionScreen: handleBackingTrackSelected called with:', result); // console.log('JKSessionScreen: Current state - showBackingTrackPopup:', showBackingTrackPopup, 'popupGuard:', popupGuard); @@ -1158,13 +1151,11 @@ const JKSessionScreen = () => { // Set up data for the popup (don't store jamClient in Redux - it's not serializable) // console.log('JKSessionScreen: Setting backing track data...'); - dispatch( - setBackingTrackData({ - backingTrack: result.file, - session: currentSession, - currentUser: currentUser - }) - ); + dispatch(setBackingTrackData({ + backingTrack: result.file, + session: currentSession, + currentUser: currentUser + })); // Show the popup // console.log('JKSessionScreen: Setting showBackingTrackPopup to true...'); @@ -1177,7 +1168,7 @@ const JKSessionScreen = () => { } }; - const handleJamTrackSelect = async jamTrack => { + const handleJamTrackSelect = async (jamTrack) => { // console.log('Jam track selected:', jamTrack); try { // Fetch jam track details with stems @@ -1189,25 +1180,21 @@ const JKSessionScreen = () => { // Check sync state BEFORE dispatching UI state // This transitions downloadState from 'idle' to 'synchronized' (if already synced) // or keeps it 'idle' (if download needed) - const syncResult = await dispatch( - checkJamTrackSync({ - jamTrack: jamTrackWithStems, - jamClient - }) - ).unwrap(); + const syncResult = await dispatch(checkJamTrackSync({ + jamTrack: jamTrackWithStems, + jamClient + })).unwrap(); // Set the selected jam track and stems (for display on session screen) dispatch(setSelectedJamTrack(jamTrackWithStems)); dispatch(setJamTrackStems(jamTrackWithStems.tracks || [])); // Open the JamTrack player popup (with full data needed for player) - dispatch( - setJamTrackData({ - jamTrack: jamTrackWithStems, - session: currentSession, - currentUser: currentUser - }) - ); + dispatch(setJamTrackData({ + jamTrack: jamTrackWithStems, + session: currentSession, + currentUser: currentUser + })); // Handle sync result if (syncResult.isSynchronized) { @@ -1237,7 +1224,7 @@ const JKSessionScreen = () => { // Check for unstable NTP clocks (like legacy implementation) const unstableClocks = await checkUnstableClocks(); if (currentSession.participants && currentSession.participants.length > 1 && unstableClocks.length > 0) { - const names = unstableClocks.join(', '); + const names = unstableClocks.join(", "); toast.warning(`Couldn't open metronome due to unstable clocks: ${names}`); return; } @@ -1259,7 +1246,7 @@ const JKSessionScreen = () => { // Default metronome settings for the controls const bpm = 120; const sound = 2; // Beep (numeric value) - const soundName = 'Beep'; // String name for native client + const soundName = "Beep"; // String name for native client const meter = 1; const cricket = false; @@ -1330,36 +1317,32 @@ const JKSessionScreen = () => { //dispatch(openModal('audioConfig')); // console.log('Audio configuration options coming soon'); alert('Audio configuration modal is not implemented yet'); - }; + + } return ( - {!isConnected &&
Connecting to backend...
} + {!isConnected &&
Connecting to backend...
} - +
- - + - + + - getVideoConferencingRoomUrl(currentSession.id)} - onUpgradePrompt={showVideoUpgradePrompt} - /> - + Broadcast dispatch(openModal('jamTrack'))} @@ -1391,35 +1373,31 @@ const JKSessionScreen = () => { accept=".pdf,.xml,.mxl,.txt,.png,.jpg,.jpeg,.gif,.mp3,.wav" onChange={handleFileSelect} /> - - +
-
-
-
- Audio Inputs +
+
+
Audio Inputs + {/* */} - Help - Settings + Help + Settings
{
-
-
- Session Mix - - Help +
+
Session Mix + + Help
-
+
{ isRemote={true} mixType="personal" /> - +
{/* JamTrack Section - Show stems only when synchronized with backend */} - {selectedJamTrack && jamTrackStems.length > 0 && jamTrackDownloadState.state === 'synchronized' && ( - <> -
- - - )} + {selectedJamTrack && jamTrackStems.length > 0 && + (jamTrackDownloadState.state === 'synchronized') && ( + <> +
+ + + )} {/* Backing Track Section - Show track when player is open */} {showBackingTrackPlayer && mixerHelper.backingTracks && mixerHelper.backingTracks.length > 0 && ( <> -
-
+
+
Backing Track { + onClick={(e) => { e.preventDefault(); handleBackingTrackMainClose(); }} @@ -1513,14 +1492,14 @@ const JKSessionScreen = () => { {/* Metronome Section - Show track when metronome is open */} {metronomeState.isOpen && metronomeTrackMixers && metronomeTrackMixers.length > 0 && ( <> -
- {/* Connection Status Alerts */} {showConnectionAlert && ( -
- -
-
+
+ +
+
{connectionStatus === ConnectionStatus.DISCONNECTED && '⚠️'} {connectionStatus === ConnectionStatus.RECONNECTING && '🔄'} {connectionStatus === ConnectionStatus.ERROR && '❌'} {connectionStatus === ConnectionStatus.CONNECTED && '✅'}
- {connectionStatus === ConnectionStatus.DISCONNECTED && Connection Lost} + {connectionStatus === ConnectionStatus.DISCONNECTED && ( + Connection Lost + )} {connectionStatus === ConnectionStatus.RECONNECTING && (
Reconnecting... - {reconnectAttempts > 0 &&
Attempt {reconnectAttempts} of 10
} + {reconnectAttempts > 0 && ( +
+ Attempt {reconnectAttempts} of 10 +
+ )}
)} {connectionStatus === ConnectionStatus.ERROR && (
Connection Failed - {lastError &&
{lastError.message || 'Unknown error'}
} + {lastError && ( +
+ {lastError.message || 'Unknown error'} +
+ )}
)} - {connectionStatus === ConnectionStatus.CONNECTED && Reconnected Successfully} + {connectionStatus === ConnectionStatus.CONNECTED && ( + Reconnected Successfully + )}
@@ -1579,20 +1568,15 @@ const JKSessionScreen = () => { )} {/* Connection Status Badge in Header */} -
+
{connectionStatus === ConnectionStatus.CONNECTED && '🟢 Connected'} {connectionStatus === ConnectionStatus.CONNECTING && '🟡 Connecting...'} @@ -1601,7 +1585,7 @@ const JKSessionScreen = () => { {connectionStatus === ConnectionStatus.ERROR && '🔴 Error'} {reconnectAttempts > 0 && ( - + Attempt {reconnectAttempts}/10 )} @@ -1639,7 +1623,7 @@ const JKSessionScreen = () => { toggle={() => dispatch(toggleModal('settings'))} currentSession={{ ...currentSession, privacy: musicianAccess }} loading={settingsLoading} - onSave={async payload => { + onSave={async (payload) => { // console.log('Session settings :', payload); try { setSettingsLoading(true); @@ -1676,6 +1660,7 @@ const JKSessionScreen = () => { } finally { setSettingsLoading(false); } + }} /> @@ -1687,10 +1672,10 @@ const JKSessionScreen = () => { friends={friends} initialInvitees={sessionInvitees} loading={inviteLoading} - onSubmit={async invitees => { + onSubmit={async (invitees) => { setSessionInvitees(invitees); // console.log('Submitted invitees:', invitees); - const inviteeIds = invitees.map(i => i.id); + const inviteeIds = invitees.map(i => i.id) const payload = { inviteeIds: inviteeIds.join() }; @@ -1714,7 +1699,10 @@ const JKSessionScreen = () => { }} /> - dispatch(toggleModal('volume'))} /> + dispatch(toggleModal('volume'))} + /> {
-

- Set the input level of your Audio Inputs for each of your tracks to a healthy level. It's important to set - your input level correctly. If your level is set too high, you'll get distortion or clipping of your audio. - If set too low, your audio signal will be too weak, which can cause noise and degrade your audio quality - when you and others use the session mix to increase your volume in the mix. -

-

- For instructions on how to set your Audio Input levels, read{' '} - - this help article - - . -

+

Set the input level of your Audio Inputs for each of your tracks to a healthy level. It's important to set your input level correctly. If your level is set too high, you'll get distortion or clipping of your audio. If set too low, your audio signal will be too weak, which can cause noise and degrade your audio quality when you and others use the session mix to increase your volume in the mix.

+

For instructions on how to set your Audio Input levels, read this help article.

-

- Adjust the volume of each audio track (yours and others) in the Session Mix to get the mix where you want it - (i.e. where it sounds good and well balanced to you). Any volume changes you make will affect only what you - hear. They don’t affect the volume of what others hear in the session. Everyone has their own customizable - session mix. -

-

- Note that your session mix is the mix that will be used for any recordings you make and for any broadcasts - you stream. If another musician in your session makes a recording or streams a broadcast, it will use that - musician’s session mix, not yours. -

-

- For instructions on how to set your Session Mix levels,{' '} - - read this help article - - . -

+

Adjust the volume of each audio track (yours and others) in the Session Mix to get the mix where you want it (i.e. where it sounds good and well balanced to you). Any volume changes you make will affect only what you hear. They don’t affect the volume of what others hear in the session. Everyone has their own customizable session mix.

+

Note that your session mix is the mix that will be used for any recordings you make and for any broadcasts you stream. If another musician in your session makes a recording or streams a broadcast, it will use that musician’s session mix, not yours.

+

For instructions on how to set your Session Mix levels, read this help article.

@@ -1842,36 +1800,32 @@ const JKSessionScreen = () => { {/* Media Controls Popup - Only show when explicitly opened */} - {showMediaControlsPopup && - !popupGuard && - (() => { - // console.log('JKSessionScreen: RENDERING Media Controls Popup - showMediaControlsPopup:', showMediaControlsPopup, 'popupGuard:', popupGuard); - setPopupGuard(true); // Set guard immediately to prevent re-renders - return ( - { - // console.log('JKSessionScreen: Media Controls Popup closing'); - dispatch(closeModal('mediaControls')); - setMediaControlsOpened(false); - setPopupGuard(false); // Reset guard when closing - }} - windowFeatures="width=600,height=500,left=250,top=150,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no,addressbar=no" - title="Media Controls" - windowId="media-controls" - > - { - // console.log('JKSessionScreen: JKPopupMediaControls onClose called'); - dispatch(closeModal('mediaControls')); - setMediaControlsOpened(false); - setPopupGuard(false); // Reset guard when closing - }} - /> - - ); - })()} + {showMediaControlsPopup && !popupGuard && (() => { + // console.log('JKSessionScreen: RENDERING Media Controls Popup - showMediaControlsPopup:', showMediaControlsPopup, 'popupGuard:', popupGuard); + setPopupGuard(true); // Set guard immediately to prevent re-renders + return ( + { + // console.log('JKSessionScreen: Media Controls Popup closing'); + dispatch(closeModal('mediaControls')); + setMediaControlsOpened(false); + setPopupGuard(false); // Reset guard when closing + }} + windowFeatures="width=600,height=500,left=250,top=150,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no,addressbar=no" + title="Media Controls" + windowId="media-controls" + > + { + // console.log('JKSessionScreen: JKPopupMediaControls onClose called'); + dispatch(closeModal('mediaControls')); + setMediaControlsOpened(false); + setPopupGuard(false); // Reset guard when closing + }} /> + + ); + })()} - ); -}; + ) +} -export default memo(JKSessionScreen); +export default memo(JKSessionScreen) diff --git a/jam-ui/src/hooks/useMixerHelper.js b/jam-ui/src/hooks/useMixerHelper.js index 0fac675ab..5edcdd2b3 100644 --- a/jam-ui/src/hooks/useMixerHelper.js +++ b/jam-ui/src/hooks/useMixerHelper.js @@ -1,15 +1,28 @@ import { useEffect, useMemo, useCallback, useRef } from 'react'; -import { useSelector, useDispatch, shallowEqual } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { selectActiveSession, selectInSession } from '../store/features/activeSessionSlice'; import { - // Composed selectors (use these instead of individual ones) - selectCoreMixerState, - selectTrackMixerState, - selectMixerLookupTables, - selectMasterPersonalMixers, - selectMixerMetadata, - selectSimulatedCategoryMixers, - // Actions (still needed) + selectChatMixer, + selectBroadcastMixer, + selectRecordingMixer, + selectRecordingTrackMixers, + selectBackingTrackMixers, + selectJamTrackMixers, + selectMetronomeTrackMixers, + selectAdhocTrackMixers, + selectMasterMixers, + selectPersonalMixers, + selectAllMixers, + selectMixersByResourceId, + selectMixersByTrackId, + selectMetronome, + selectMetronomeSettings, + selectMediaSummary, + selectNoAudioUsers, + selectClientsWithAudioOverride, + selectSimulatedMusicCategoryMixers, + selectSimulatedChatCategoryMixers, + selectMixersReady, setMasterMixers, setPersonalMixers, organizeMixers, @@ -28,8 +41,16 @@ import { setMetronomeTrackMixers, setAdhocTrackMixers } from '../store/features/mixersSlice'; -import { selectBackingTracks, selectJamTracks, selectRecordedTracks } from '../store/features/mediaSlice'; -import { selectMixMode, selectCurrentMixerRange, setCurrentMixerRange } from '../store/features/sessionUISlice'; +import { + selectBackingTracks, + selectJamTracks, + selectRecordedTracks +} from '../store/features/mediaSlice'; +import { + selectMixMode, + selectCurrentMixerRange, + setCurrentMixerRange +} from '../store/features/sessionUISlice'; import { ChannelGroupIds, CategoryGroupIds, MIX_MODES, MIDI_TRACK } from '../helpers/globals.js'; import useVuHelpers from './useVuHelpers.js'; import useFaderHelpers from './useFaderHelpers.js'; @@ -40,49 +61,35 @@ import { useGlobalContext } from '../context/GlobalContext.js'; import { useVuContext } from '../context/VuContext.js'; import { getAvatarUrl, getInstrumentIcon45, getInstrumentIcon24 } from '../helpers/utils.js'; + const useMixerHelper = () => { const dispatch = useDispatch(); const allMixersRef = useRef({}); const previousMyTracksRef = useRef([]); const previousMixerIdsRef = useRef(null); - // Track previous category values for comparison - const prevCategoriesRef = useRef({ - metronome: [], - backing: [], - jam: [], - recording: [], - adhoc: [] - }); - - // Composed selectors with shallowEqual - reduces 21 subscriptions to 6 - const coreMixers = useSelector(selectCoreMixerState, shallowEqual); - const trackMixers = useSelector(selectTrackMixerState, shallowEqual); - const lookupTables = useSelector(selectMixerLookupTables, shallowEqual); - const masterPersonal = useSelector(selectMasterPersonalMixers, shallowEqual); - const metadata = useSelector(selectMixerMetadata, shallowEqual); - const simulatedMixers = useSelector(selectSimulatedCategoryMixers, shallowEqual); - - // Destructure for backward compatibility with rest of hook - const { chatMixer, broadcastMixer, recordingMixer } = coreMixers; - const { - recordingTrackMixers, - backingTrackMixers, - jamTrackMixers, - metronomeTrackMixers, - adhocTrackMixers - } = trackMixers; - const { allMixers, mixersByResourceId, mixersByTrackId } = lookupTables; - const { masterMixers, personalMixers } = masterPersonal; - const { - metronome, - metronomeSettings, - mediaSummary, - noAudioUsers, - clientsWithAudioOverride, - isReady: isReadyRedux - } = metadata; - const { simulatedMusicCategoryMixers, simulatedChatCategoryMixers } = simulatedMixers; + // Redux selectors - replace all useState calls + const chatMixer = useSelector(selectChatMixer); + const broadcastMixer = useSelector(selectBroadcastMixer); + const recordingMixer = useSelector(selectRecordingMixer); + const recordingTrackMixers = useSelector(selectRecordingTrackMixers); + const backingTrackMixers = useSelector(selectBackingTrackMixers); + const jamTrackMixers = useSelector(selectJamTrackMixers); + const metronomeTrackMixers = useSelector(selectMetronomeTrackMixers); + const adhocTrackMixers = useSelector(selectAdhocTrackMixers); + const masterMixers = useSelector(selectMasterMixers); + const personalMixers = useSelector(selectPersonalMixers); + const allMixers = useSelector(selectAllMixers); + const mixersByResourceId = useSelector(selectMixersByResourceId); + const mixersByTrackId = useSelector(selectMixersByTrackId); + const metronome = useSelector(selectMetronome); + const metronomeSettings = useSelector(selectMetronomeSettings); + const mediaSummary = useSelector(selectMediaSummary); + const noAudioUsers = useSelector(selectNoAudioUsers); + const clientsWithAudioOverride = useSelector(selectClientsWithAudioOverride); + const simulatedMusicCategoryMixers = useSelector(selectSimulatedMusicCategoryMixers); + const simulatedChatCategoryMixers = useSelector(selectSimulatedChatCategoryMixers); + const isReadyRedux = useSelector(selectMixersReady); // Media data from mediaSlice const backingTracks = useSelector(selectBackingTracks); @@ -93,20 +100,10 @@ const useMixerHelper = () => { const mixMode = useSelector(selectMixMode); const currentMixerRange = useSelector(selectCurrentMixerRange); - const mediaTrackGroups = useMemo( - () => [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], - [] - ); - const muteBothMasterAndPersonalGroups = useMemo( - () => [ - ChannelGroupIds.AudioInputMusicGroup, - ChannelGroupIds.MidiInputMusicGroup, - ChannelGroupIds.MediaTrackGroup, - ChannelGroupIds.JamTrackGroup, - ChannelGroupIds.MetronomeGroup - ], - [] - ); + const mediaTrackGroups = useMemo(() => [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, + ChannelGroupIds.MetronomeGroup], []); + const muteBothMasterAndPersonalGroups = useMemo(() => [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MidiInputMusicGroup, ChannelGroupIds.MediaTrackGroup, + ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], []); // Phase 4: Replace CurrentSessionContext with Redux const currentSession = useSelector(selectActiveSession); @@ -128,120 +125,96 @@ const useMixerHelper = () => { // console.log("_XDEBUG_ useMixerHelper: allMixersRef updated", allMixersRef.current); }, [allMixers]); - const getMixer = useCallback( - (mixerId, mode) => { - // Only default to mixMode if mode is undefined, not if it's explicitly false - if (mode === undefined) { - mode = mixMode; - } - return allMixersRef.current[(mode ? 'M' : 'P') + mixerId]; - }, - [mixMode] - ); + const getMixer = useCallback((mixerId, mode) => { + // Only default to mixMode if mode is undefined, not if it's explicitly false + if (mode === undefined) { + mode = mixMode; + } + return allMixersRef.current[(mode ? 'M' : 'P') + mixerId]; + }, [mixMode]); - const fillTrackVolumeObject = useCallback( - (mixerId, mode, currentAllMixers, broadcast = true) => { - const mixer = getMixer(mixerId, mode, currentAllMixers); - if (mixer == null) { - return null; - } + const fillTrackVolumeObject = useCallback((mixerId, mode, currentAllMixers, broadcast = true) => { + const mixer = getMixer(mixerId, mode, currentAllMixers); + if (mixer == null) { + return null; + } - // Build volume object directly and return it (don't rely on async state update) - const volumeObj = { - id: mixer.id, - _id: mixer._id, - groupID: mixer.group_id, - clientID: mixer.client_id, - broadcast: broadcast, - master: mixer.master, - monitor: mixer.monitor, - mute: mixer.mute, - name: mixer.name, - record: mixer.record, - volL: mixer.volume_left, - pan: mixer.pan, - mediaType: mixer.media_type, - isJamTrack: mixer.is_jam_track, - isMetronome: mixer.is_metronome, - volR: mixer.volume_left, - loop: mixer.loop + // Build volume object directly and return it (don't rely on async state update) + const volumeObj = { + id: mixer.id, + _id: mixer._id, + groupID: mixer.group_id, + clientID: mixer.client_id, + broadcast: broadcast, + master: mixer.master, + monitor: mixer.monitor, + mute: mixer.mute, + name: mixer.name, + record: mixer.record, + volL: mixer.volume_left, + pan: mixer.pan, + mediaType: mixer.media_type, + isJamTrack: mixer.is_jam_track, + isMetronome: mixer.is_metronome, + volR: mixer.volume_left, + loop: mixer.loop + }; + + // Also update React state for other consumers + setTrackVolumeObject(volumeObj); + + // Redux: Update mixer range in sessionUISlice + dispatch(setCurrentMixerRange({ + min: mixer.range_low, + max: mixer.range_high + })); + + // Return both mixer and the volume object + return { mixer, volumeObj }; + }, [getMixer, setTrackVolumeObject, dispatch]); + + const setMixerVolume = useCallback(async (mixer, volumePercent, relative, originalVolume, controlGroup, baseVolumeObj) => { + const newVolume = faderHelpers.convertPercentToAudioTaper(volumePercent); + + // Use the passed volumeObj instead of relying on async state + const sourceVolumeObj = baseVolumeObj || trackVolumeObject; + + let updatedTrackVolumeObject; + if (relative) { + updatedTrackVolumeObject = { + ...sourceVolumeObj, + volL: sourceVolumeObj.volL + (newVolume - originalVolume), + volR: sourceVolumeObj.volR + (newVolume - originalVolume), }; - // Also update React state for other consumers - setTrackVolumeObject(volumeObj); + // Apply clamping + if (updatedTrackVolumeObject.volL < -80) updatedTrackVolumeObject.volL = -80; + if (updatedTrackVolumeObject.volL > 20) updatedTrackVolumeObject.volL = 20; + if (updatedTrackVolumeObject.volR < -80) updatedTrackVolumeObject.volR = -80; + if (updatedTrackVolumeObject.volR > 20) updatedTrackVolumeObject.volR = 20; + } else { + updatedTrackVolumeObject = { + ...sourceVolumeObj, + volL: newVolume, + volR: newVolume, + }; + } - // Redux: Update mixer range in sessionUISlice - dispatch( - setCurrentMixerRange({ - min: mixer.range_low, - max: mixer.range_high - }) - ); + // Update state + setTrackVolumeObject(updatedTrackVolumeObject); - // Return both mixer and the volume object - return { mixer, volumeObj }; - }, - [getMixer, setTrackVolumeObject, dispatch] - ); - - const setMixerVolume = useCallback( - async (mixer, volumePercent, relative, originalVolume, controlGroup, baseVolumeObj) => { - const newVolume = faderHelpers.convertPercentToAudioTaper(volumePercent); - - // Use the passed volumeObj instead of relying on async state - const sourceVolumeObj = baseVolumeObj || trackVolumeObject; - - let updatedTrackVolumeObject; - if (relative) { - updatedTrackVolumeObject = { - ...sourceVolumeObj, - volL: sourceVolumeObj.volL + (newVolume - originalVolume), - volR: sourceVolumeObj.volR + (newVolume - originalVolume) - }; - - // Apply clamping - if (updatedTrackVolumeObject.volL < -80) updatedTrackVolumeObject.volL = -80; - if (updatedTrackVolumeObject.volL > 20) updatedTrackVolumeObject.volL = 20; - if (updatedTrackVolumeObject.volR < -80) updatedTrackVolumeObject.volR = -80; - if (updatedTrackVolumeObject.volR > 20) updatedTrackVolumeObject.volR = 20; - } else { - updatedTrackVolumeObject = { - ...sourceVolumeObj, - volL: newVolume, - volR: newVolume - }; - } - - // Update state - setTrackVolumeObject(updatedTrackVolumeObject); - - // Use the computed object for jamClient call - if (controlGroup != null) { - const controlGroupsArg = mixer.mode === MIX_MODES.PERSONAL ? 0 : 1; - console.log( - 'setMixerVolume: setting session mixer category playout state for controlGroup', - controlGroup, - 'controlGroupsArg', - controlGroupsArg, - 'volume', - updatedTrackVolumeObject.volL - ); - await jamClient.setSessionMixerCategoryPlayoutState( - controlGroup === 'music', - controlGroupsArg, - updatedTrackVolumeObject.volL - ); - } else { - await jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, updatedTrackVolumeObject); - } - }, - [trackVolumeObject, faderHelpers, jamClient, setTrackVolumeObject] - ); + // Use the computed object for jamClient call + if (controlGroup != null) { + const controlGroupsArg = mixer.mode === MIX_MODES.PERSONAL ? 0 : 1; + console.log("setMixerVolume: setting session mixer category playout state for controlGroup", controlGroup, "controlGroupsArg", controlGroupsArg, "volume", updatedTrackVolumeObject.volL); + await jamClient.setSessionMixerCategoryPlayoutState(controlGroup === 'music', controlGroupsArg, updatedTrackVolumeObject.volL); + } else { + await jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, updatedTrackVolumeObject); + } + }, [trackVolumeObject, faderHelpers, jamClient, setTrackVolumeObject]); const mediaMixers = useCallback((masterMixer, isOpener, currentAllMixers) => { - const personalMixer = isOpener - ? getMixerByResourceId(masterMixer.rid, MIX_MODES.PERSONAL, currentAllMixers) - : masterMixer; + const personalMixer = isOpener ? getMixerByResourceId(masterMixer.rid, MIX_MODES.PERSONAL, currentAllMixers) : masterMixer; const personalVuMixer = isOpener ? personalMixer : masterMixer; return { isOpener: isOpener, @@ -259,18 +232,15 @@ const useMixerHelper = () => { }, []); // Redux: updateMixerData now dispatches Redux actions - const updateMixerData = useCallback( - (session, masterMixers, personalMixers, metro, noAudioUsers, clientsWithAudioOverride, mixMode) => { - //console.debug("useMixerHelper: updateMixerData called", { session, masterMixers, personalMixers, mixMode }); + const updateMixerData = useCallback((session, masterMixers, personalMixers, metro, noAudioUsers, clientsWithAudioOverride, mixMode) => { + //console.debug("useMixerHelper: updateMixerData called", { session, masterMixers, personalMixers, mixMode }); - // Dispatch Redux actions instead of setState calls - dispatch(setMasterMixers(masterMixers)); - dispatch(setPersonalMixers(personalMixers)); - // Note: session is tracked in activeSessionSlice, metro/noAudioUsers/clientsWithAudioOverride in mixersSlice - // mixMode is tracked in sessionUISlice - }, - [dispatch] - ); + // Dispatch Redux actions instead of setState calls + dispatch(setMasterMixers(masterMixers)); + dispatch(setPersonalMixers(personalMixers)); + // Note: session is tracked in activeSessionSlice, metro/noAudioUsers/clientsWithAudioOverride in mixersSlice + // mixMode is tracked in sessionUISlice + }, [dispatch]); // Redux: organizeMixers is now a reducer, trigger it via action useEffect(() => { @@ -282,20 +252,11 @@ const useMixerHelper = () => { }, [currentSession, masterMixers, personalMixers, dispatch]); // Categorize mixers by group_id and dispatch to Redux - // Create a stable key that only changes when mixer composition changes (add/remove) - // Not when mixer properties change (volume, pan, etc.) - const masterMixerIds = useMemo( - () => - masterMixers - ?.map(m => `${m.id}:${m.group_id}`) - .sort() - .join(',') || '', - [masterMixers] - ); - useEffect(() => { if (!masterMixers || !personalMixers || masterMixers.length === 0) return; + console.log('[useMixerHelper] Categorizing mixers by group_id'); + const metronomeTrackMixers = []; const backingTrackMixers = []; const jamTrackMixers = []; @@ -315,6 +276,7 @@ const useMixerHelper = () => { switch (masterMixer.group_id) { case ChannelGroupIds.MetronomeGroup: // 16 metronomeTrackMixers.push(mixerPair); + console.log('[useMixerHelper] Found metronome mixer:', masterMixer.id); break; case ChannelGroupIds.BackingTrackGroup: // 11 backingTrackMixers.push(mixerPair); @@ -327,39 +289,38 @@ const useMixerHelper = () => { break; default: // Other mixer types go into adhoc - if ( - masterMixer.group_id !== ChannelGroupIds.UserMusicInputGroup && - masterMixer.group_id !== ChannelGroupIds.ChatMicGroup && - masterMixer.group_id !== ChannelGroupIds.BroadcastGroup - ) { + if (masterMixer.group_id !== ChannelGroupIds.UserMusicInputGroup && + masterMixer.group_id !== ChannelGroupIds.ChatMicGroup && + masterMixer.group_id !== ChannelGroupIds.BroadcastGroup) { adhocTrackMixers.push(mixerPair); } } } - // Dispatch to Redux - categories only change when mixers are added/removed + console.log('[useMixerHelper] Categorized mixers:', { + metronome: metronomeTrackMixers.length, + backing: backingTrackMixers.length, + jam: jamTrackMixers.length, + recording: recordingTrackMixers.length, + adhoc: adhocTrackMixers.length + }); + + // Dispatch to Redux dispatch(setMetronomeTrackMixers(metronomeTrackMixers)); dispatch(setBackingTrackMixers(backingTrackMixers)); dispatch(setJamTrackMixers(jamTrackMixers)); dispatch(setRecordingTrackMixers(recordingTrackMixers)); dispatch(setAdhocTrackMixers(adhocTrackMixers)); - // Update refs for any other comparisons - prevCategoriesRef.current = { - metronome: metronomeTrackMixers, - backing: backingTrackMixers, - jam: jamTrackMixers, - recording: recordingTrackMixers, - adhoc: adhocTrackMixers - }; - // Only re-run when mixer composition changes (add/remove), not property changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [masterMixerIds, mixMode, dispatch]); + }, [masterMixers, personalMixers, mixMode, dispatch]); // Sync local isReady ref with Redux isReady state // This ensures VU meter callbacks have immediate access to the ready state useEffect(() => { isReady.current = isReadyRedux; + if (isReadyRedux) { + console.log('[useMixerHelper] Mixers ready, VU meters enabled'); + } }, [isReadyRedux]); // Cleanup VU state when mixers are removed @@ -389,202 +350,145 @@ const useMixerHelper = () => { previousMixerIdsRef.current = currentMixerIds; }, [allMixers, isReadyRedux, removeVuState]); - const getMixerByTrackId = useCallback( - (trackId, mode) => { - const mixerPair = mixersByTrackId[trackId]; + const getMixerByTrackId = useCallback((trackId, mode) => { + const mixerPair = mixersByTrackId[trackId]; - if (!mixerPair) return null; + if (!mixerPair) return null; - if (mode === undefined) { - return mixerPair; + if (mode === undefined) { + return mixerPair; + } else { + if (mode === MIX_MODES.MASTER) { + return mixerPair.master; } else { - if (mode === MIX_MODES.MASTER) { - return mixerPair.master; - } else { - return mixerPair.personal; - } + return mixerPair.personal; } - }, - [mixersByTrackId] - ); + } + }, [mixersByTrackId]); - const groupedMixersForClientId = useCallback( - (clientId, groupIds, usedMixers, mixMode, currentAllMixers) => { - const foundMixers = {}; - const mixers = mixMode === MIX_MODES.MASTER ? masterMixers : personalMixers; + const groupedMixersForClientId = useCallback((clientId, groupIds, usedMixers, mixMode, currentAllMixers) => { + const foundMixers = {}; + const mixers = mixMode === MIX_MODES.MASTER ? masterMixers : personalMixers; - for (const mixer of mixers) { - if (!mixer) { - continue; - } + for (const mixer of mixers) { + if (!mixer) { + continue; + } - if (mixer.client_id === clientId) { - for (const groupId of groupIds) { - if (mixer.group_id === groupId) { - if (mixer.groupId !== ChannelGroupIds.UserMusicInputGroup && !(mixer.id in usedMixers)) { - let mixers = foundMixers[mixer.group_id]; - if (!mixers) { - mixers = []; - foundMixers[mixer.group_id] = mixers; - } - mixers.push(mixer); + if (mixer.client_id === clientId) { + for (const groupId of groupIds) { + if (mixer.group_id === groupId) { + if ((mixer.groupId !== ChannelGroupIds.UserMusicInputGroup) && !(mixer.id in usedMixers)) { + let mixers = foundMixers[mixer.group_id]; + if (!mixers) { + mixers = []; + foundMixers[mixer.group_id] = mixers; } + mixers.push(mixer); } } } } + } - return foundMixers; - }, - [masterMixers, personalMixers] - ); + return foundMixers; + }, [masterMixers, personalMixers]); - const findMixerForTrack = useCallback( - (client_id, track, myTrack, mode) => { - let mixer = null; - let oppositeMixer = null; - let vuMixer = null; - let muteMixer = null; + const findMixerForTrack = useCallback((client_id, track, myTrack, mode) => { + let mixer = null; + let oppositeMixer = null; + let vuMixer = null; + let muteMixer = null; - if (myTrack) { - mixer = getMixerByTrackId(track.client_track_id, mode); - vuMixer = mixer; - muteMixer = mixer; + if (myTrack) { + mixer = getMixerByTrackId(track.client_track_id, mode); + vuMixer = mixer; + muteMixer = mixer; - if ( - mixer && - (mixer.group_id !== ChannelGroupIds.AudioInputMusicGroup && - mixer.group_id !== ChannelGroupIds.MidiInputMusicGroup) - ) { - logger.error('found local mixer that was not of groupID: AudioInputMusicGroup', mixer); + if (mixer && (mixer.group_id !== ChannelGroupIds.AudioInputMusicGroup && mixer.group_id !== ChannelGroupIds.MidiInputMusicGroup)) { + logger.error("found local mixer that was not of groupID: AudioInputMusicGroup", mixer); + } + + if (mixer) { + oppositeMixer = getMixerByTrackId(track.client_track_id, !mode); + + if (mode === MIX_MODES.PERSONAL) { + muteMixer = oppositeMixer; } - if (mixer) { - oppositeMixer = getMixerByTrackId(track.client_track_id, !mode); - - if (mode === MIX_MODES.PERSONAL) { - muteMixer = oppositeMixer; - } - - if (!oppositeMixer) { - logger.error('unable to find opposite mixer for local mixer', mixer); - } else if ( - oppositeMixer.group_id !== ChannelGroupIds.AudioInputMusicGroup && - oppositeMixer.group_id !== ChannelGroupIds.MidiInputMusicGroup - ) { - logger.error( - 'found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup', - mixer, - oppositeMixer - ); - } - } else { - logger.debug('local track is not present: ', track, allMixers); + if (!oppositeMixer) { + logger.error("unable to find opposite mixer for local mixer", mixer); + } else if (oppositeMixer.group_id !== ChannelGroupIds.AudioInputMusicGroup && oppositeMixer.group_id !== ChannelGroupIds.MidiInputMusicGroup) { + logger.error("found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup", mixer, oppositeMixer); } } else { - switch (mode) { - case MIX_MODES.MASTER: - mixer = getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER); + logger.debug("local track is not present: ", track, allMixers); + } + } else { + switch (mode) { + case MIX_MODES.MASTER: + mixer = getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER); - if ( - mixer && - (mixer.group_id !== ChannelGroupIds.PeerAudioInputMusicGroup && - mixer.group_id !== ChannelGroupIds.PeerMidiInputMusicGroup) - ) { - logger.warn( - 'master: found remote mixer that was not of groupID: PeerAudioInputMusicGroup', - client_id, - track.client_track_id, - mixer - ); + if (mixer && (mixer.group_id !== ChannelGroupIds.PeerAudioInputMusicGroup && mixer.group_id !== ChannelGroupIds.PeerMidiInputMusicGroup)) { + logger.warn("master: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer); + } + + vuMixer = mixer; + muteMixer = mixer; + + if (mixer) { + const oppositeMixers = groupedMixersForClientId(client_id, [ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL); + if (oppositeMixers[ChannelGroupIds.UserMusicInputGroup]) { + oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0]; } + if (!oppositeMixer) { + logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer, personalMixers); + } + } + break; + + case MIX_MODES.PERSONAL: + const mixers = groupedMixersForClientId(client_id, [ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL); + if (mixers[ChannelGroupIds.UserMusicInputGroup]) { + mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0]; + vuMixer = mixer; muteMixer = mixer; if (mixer) { - const oppositeMixers = groupedMixersForClientId( - client_id, - [ChannelGroupIds.UserMusicInputGroup], - {}, - MIX_MODES.PERSONAL - ); - if (oppositeMixers[ChannelGroupIds.UserMusicInputGroup]) { - oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0]; - } - + oppositeMixer = getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER); if (!oppositeMixer) { - logger.warn( - 'unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer', - mixer, - personalMixers - ); + logger.debug("personal: unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id); + } else if (oppositeMixer.group_id !== ChannelGroupIds.PeerAudioInputMusicGroup && oppositeMixer.group_id !== ChannelGroupIds.PeerMidiInputMusicGroup) { + logger.error("personal: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer); } } - break; - - case MIX_MODES.PERSONAL: - const mixers = groupedMixersForClientId( - client_id, - [ChannelGroupIds.UserMusicInputGroup], - {}, - MIX_MODES.PERSONAL - ); - if (mixers[ChannelGroupIds.UserMusicInputGroup]) { - mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0]; - - vuMixer = mixer; - muteMixer = mixer; - - if (mixer) { - oppositeMixer = getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER); - if (!oppositeMixer) { - logger.debug( - 'personal: unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput', - client_id, - track.client_track_id - ); - } else if ( - oppositeMixer.group_id !== ChannelGroupIds.PeerAudioInputMusicGroup && - oppositeMixer.group_id !== ChannelGroupIds.PeerMidiInputMusicGroup - ) { - logger.error( - 'personal: found remote mixer that was not of groupID: PeerAudioInputMusicGroup', - client_id, - track.client_track_id, - mixer - ); - } - } - } else { - logger.error('no UserMusicInputGroup for client_id #{client_id} in PERSONAL mode', mixers); - } - break; - } + } else { + logger.error("no UserMusicInputGroup for client_id #{client_id} in PERSONAL mode", mixers); + } + break; } + } - // Ensure mixer objects have the correct mode set for proper persistence - const mixerWithMode = mixer ? { ...mixer, mode } : null; - const oppositeMode = mode === MIX_MODES.MASTER ? MIX_MODES.PERSONAL : MIX_MODES.MASTER; - const oppositeMixerWithMode = oppositeMixer ? { ...oppositeMixer, mode: oppositeMode } : null; - const vuMixerWithMode = vuMixer ? { ...vuMixer, mode } : null; - const muteMixerWithMode = muteMixer - ? { ...muteMixer, mode: muteMixer === oppositeMixer ? oppositeMode : mode } - : null; + // Ensure mixer objects have the correct mode set for proper persistence + const mixerWithMode = mixer ? { ...mixer, mode } : null; + const oppositeMode = mode === MIX_MODES.MASTER ? MIX_MODES.PERSONAL : MIX_MODES.MASTER; + const oppositeMixerWithMode = oppositeMixer ? { ...oppositeMixer, mode: oppositeMode } : null; + const vuMixerWithMode = vuMixer ? { ...vuMixer, mode } : null; + const muteMixerWithMode = muteMixer ? { ...muteMixer, mode: muteMixer === oppositeMixer ? oppositeMode : mode } : null; - return { - mixer: mixerWithMode, - oppositeMixer: oppositeMixerWithMode, - vuMixer: vuMixerWithMode, - muteMixer: muteMixerWithMode - }; - }, - [getMixerByTrackId, groupedMixersForClientId, personalMixers, allMixers] - ); + return { + mixer: mixerWithMode, + oppositeMixer: oppositeMixerWithMode, + vuMixer: vuMixerWithMode, + muteMixer: muteMixerWithMode + }; + }, [getMixerByTrackId, groupedMixersForClientId, personalMixers, allMixers]); // Compute myTracks - memoized to prevent infinite re-renders const myTracks = useMemo(() => { - console.debug('useMixerHelper: computing myTracks', { + console.debug("useMixerHelper: computing myTracks", { isConnected, currentSession, jamClient, @@ -593,29 +497,28 @@ const useMixerHelper = () => { }); if (!isConnected || !inSession || !jamClient || !allMixers) { - return previousMyTracksRef.current; // Return previous value, not [] + return previousMyTracksRef.current; // Return previous value, not [] } // Safety check: if allMixers is empty during state transition, return previous value if (typeof allMixers === 'object' && Object.keys(allMixers).length === 0) { - console.warn('useMixerHelper: allMixers is empty, returning previous myTracks'); - return previousMyTracksRef.current; // Return previous value, not [] + console.warn("useMixerHelper: allMixers is empty, returning previous myTracks"); + return previousMyTracksRef.current; // Return previous value, not [] } //const participant = currentSession.participants?.[jamClient.clientId]; const participant = getParticipant(server.clientId); if (!participant) { - return previousMyTracksRef.current; // Return previous value, not [] + return previousMyTracksRef.current; // Return previous value, not [] } const tracks = []; - const connStatsClientId = - participant.client_role === 'child' && participant.parent_client_id - ? participant.parent_client_id - : server.clientId; + const connStatsClientId = participant.client_role === 'child' && participant.parent_client_id + ? participant.parent_client_id + : server.clientId; - console.debug('useMixerHelper: my participant', participant, { connStatsClientId }); + console.debug("useMixerHelper: my participant", participant, { connStatsClientId }); const photoUrl = getAvatarUrl(participant.user.photo_url); const name = participant.user.name; @@ -634,7 +537,7 @@ const useMixerHelper = () => { const assignments = jamClient.VSTListTrackAssignments(); vstTrackAssignments = assignments || { vsts: [] }; } catch (error) { - console.warn('Failed to get VST track assignments:', error); + console.warn("Failed to get VST track assignments:", error); } const participantTracks = participant.tracks || []; @@ -651,10 +554,9 @@ const useMixerHelper = () => { // Check if this track has a VST plugin assigned // Native client uses 0-based track indices for VST assignments - const hasVst = - vstTrackAssignments.vsts && Array.isArray(vstTrackAssignments.vsts) - ? vstTrackAssignments.vsts.some(vst => vst.track === trackIndex) - : false; + const hasVst = vstTrackAssignments.vsts && Array.isArray(vstTrackAssignments.vsts) + ? vstTrackAssignments.vsts.some(vst => vst.track === trackIndex) + : false; tracks.push({ track: { @@ -686,6 +588,7 @@ const useMixerHelper = () => { // console.debug("useMixerHelper: myTracks updated", myTracks); // }, [myTracks]); + const mixersForGroupId = useCallback((groupId, mixMode, currentAllMixers) => { const foundMixers = []; const modePrefix = mixMode === MIX_MODES.MASTER ? 'M' : 'P'; @@ -702,177 +605,132 @@ const useMixerHelper = () => { return foundMixers; }, []); - const getGroupMixer = useCallback( - (categoryId, mode) => { - const groupId = mode === MIX_MODES.MASTER ? ChannelGroupIds.MasterCatGroup : ChannelGroupIds.MonitorCatGroup; - const oppositeGroupId = - !mode === MIX_MODES.MASTER ? ChannelGroupIds.MasterCatGroup : ChannelGroupIds.MonitorCatGroup; - const mixers = mixersForGroupId(groupId, mode, allMixers); - const oppositeMixers = mixersForGroupId(oppositeGroupId, !mode, allMixers); + const getGroupMixer = useCallback((categoryId, mode) => { + const groupId = mode === MIX_MODES.MASTER ? ChannelGroupIds.MasterCatGroup : ChannelGroupIds.MonitorCatGroup; + const oppositeGroupId = !mode === MIX_MODES.MASTER ? ChannelGroupIds.MasterCatGroup : ChannelGroupIds.MonitorCatGroup; + const mixers = mixersForGroupId(groupId, mode, allMixers); + const oppositeMixers = mixersForGroupId(oppositeGroupId, !mode, allMixers); - if (mixers.length === 0) { - return null; + if (mixers.length === 0) { + return null; + } + + let found = null; + let oppositeFound = null; + for (const mixer of mixers) { + if (mixer.name === categoryId) { + found = mixer; + break; } + } - let found = null; - let oppositeFound = null; - for (const mixer of mixers) { - if (mixer.name === categoryId) { - found = mixer; - break; - } + for (const mixer of oppositeMixers) { + if (mixer.name === categoryId) { + oppositeFound = mixer; + break; } + } - for (const mixer of oppositeMixers) { - if (mixer.name === categoryId) { - oppositeFound = mixer; - break; - } - } + if (!found) { + logger.warn("could not find mixer with categoryId: " + categoryId); + return null; + } else { + return { + mixer: found, + muteMixer: found, + vuMixer: found, + oppositeMixer: oppositeFound + }; + } + }, [mixersForGroupId, allMixers]); - if (!found) { - logger.warn('could not find mixer with categoryId: ' + categoryId); - return null; - } else { - return { - mixer: found, - muteMixer: found, - vuMixer: found, - oppositeMixer: oppositeFound - }; - } - }, - [mixersForGroupId, allMixers] - ); + const mixerForGroupId = useCallback((groupId, mixMode, currentAllMixers) => { + const mixers = mixersForGroupId(groupId, mixMode, currentAllMixers); + if (mixers && mixers.length > 0) { + return mixers[0]; + } else { + return null; + } + }, [mixersForGroupId]); - const mixerForGroupId = useCallback( - (groupId, mixMode, currentAllMixers) => { - const mixers = mixersForGroupId(groupId, mixMode, currentAllMixers); - if (mixers && mixers.length > 0) { - return mixers[0]; - } else { - return null; - } - }, - [mixersForGroupId] - ); + const getAudioInputCategoryMixer = useCallback((mode, currentAllMixers) => { + return getGroupMixer(CategoryGroupIds.AudioInputMusic, mode, currentAllMixers); + }, [getGroupMixer]); - const getAudioInputCategoryMixer = useCallback( - (mode, currentAllMixers) => { - return getGroupMixer(CategoryGroupIds.AudioInputMusic, mode, currentAllMixers); - }, - [getGroupMixer] - ); + const getChatCategoryMixer = useCallback((mode, currentAllMixers) => { + return getGroupMixer(CategoryGroupIds.AudioInputChat, mode, currentAllMixers); + }, [getGroupMixer]); - const getChatCategoryMixer = useCallback( - (mode, currentAllMixers) => { - return getGroupMixer(CategoryGroupIds.AudioInputChat, mode, currentAllMixers); - }, - [getGroupMixer] - ); + const getUserChatCategoryMixer = useCallback((mode, currentAllMixers) => { + return getGroupMixer(CategoryGroupIds.UserChat, mode, currentAllMixers); + }, [getGroupMixer]); - const getUserChatCategoryMixer = useCallback( - (mode, currentAllMixers) => { - return getGroupMixer(CategoryGroupIds.UserChat, mode, currentAllMixers); - }, - [getGroupMixer] - ); + const getMediaCategoryMixer = useCallback((mode, currentAllMixers) => { + return getGroupMixer(CategoryGroupIds.MediaTrack, mode, currentAllMixers); + }, [getGroupMixer]); - const getMediaCategoryMixer = useCallback( - (mode, currentAllMixers) => { - return getGroupMixer(CategoryGroupIds.MediaTrack, mode, currentAllMixers); - }, - [getGroupMixer] - ); + const getUserMediaCategoryMixer = useCallback((mode, currentAllMixers) => { + return getGroupMixer(CategoryGroupIds.UserMedia, mode, currentAllMixers); + }, [getGroupMixer]); - const getUserMediaCategoryMixer = useCallback( - (mode, currentAllMixers) => { - return getGroupMixer(CategoryGroupIds.UserMedia, mode, currentAllMixers); - }, - [getGroupMixer] - ); + const getUserMusicCategoryMixer = useCallback((mode, currentAllMixers) => { + return getGroupMixer(CategoryGroupIds.UserMusic, mode, currentAllMixers); + }, [getGroupMixer]); - const getUserMusicCategoryMixer = useCallback( - (mode, currentAllMixers) => { - return getGroupMixer(CategoryGroupIds.UserMusic, mode, currentAllMixers); - }, - [getGroupMixer] - ); + const getMetronomeCategoryMixer = useCallback((mode, currentAllMixers) => { + return getGroupMixer(CategoryGroupIds.Metronome, mode, currentAllMixers); + }, [getGroupMixer]); - const getMetronomeCategoryMixer = useCallback( - (mode, currentAllMixers) => { - return getGroupMixer(CategoryGroupIds.Metronome, mode, currentAllMixers); - }, - [getGroupMixer] - ); + const getOutputCategoryMixer = useCallback((mode, currentAllMixers) => { + if (mode === MIX_MODES.MASTER) { + return getGroupMixer(CategoryGroupIds.MasterCatGroup, mode, currentAllMixers); + } else { + return getGroupMixer(CategoryGroupIds.MonitorCatGroup, mode, currentAllMixers); + } + }, [getGroupMixer]); - const getOutputCategoryMixer = useCallback( - (mode, currentAllMixers) => { + const getOutputMixer = useCallback((mode, currentAllMixers) => { + if (mode === MIX_MODES.MASTER) { + return mixerForGroupId(ChannelGroupIds.MasterGroup, mode, currentAllMixers); + } else { + return mixerForGroupId(ChannelGroupIds.MonitorGroup, mode, currentAllMixers); + } + }, [mixerForGroupId]); + + const setMixerPan = useCallback(async (mixer, panPercent) => { + trackVolumeObject.pan = panHelpers.convertPercentToPan(panPercent); + await jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, trackVolumeObject); + }, [trackVolumeObject, panHelpers, jamClient]); + + const getMixerByResourceId = useCallback((resourceId, mode) => { + const mixerPair = mixersByResourceId[resourceId]; + + if (!mixerPair) return null; + + if (!mode) { + return mixerPair; + } else { if (mode === MIX_MODES.MASTER) { - return getGroupMixer(CategoryGroupIds.MasterCatGroup, mode, currentAllMixers); + return mixerPair.master; } else { - return getGroupMixer(CategoryGroupIds.MonitorCatGroup, mode, currentAllMixers); + return mixerPair.personal; } - }, - [getGroupMixer] - ); + } + }, [mixersByResourceId]); - const getOutputMixer = useCallback( - (mode, currentAllMixers) => { - if (mode === MIX_MODES.MASTER) { - return mixerForGroupId(ChannelGroupIds.MasterGroup, mode, currentAllMixers); - } else { - return mixerForGroupId(ChannelGroupIds.MonitorGroup, mode, currentAllMixers); - } - }, - [mixerForGroupId] - ); + const mute = useCallback(async (mixerId, mode, muting) => { + if (mode == null) { mode = mixMode; } - const setMixerPan = useCallback( - async (mixer, panPercent) => { - trackVolumeObject.pan = panHelpers.convertPercentToPan(panPercent); - await jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, trackVolumeObject); - }, - [trackVolumeObject, panHelpers, jamClient] - ); + const result = fillTrackVolumeObject(mixerId, mode); + if (!result) return; - const getMixerByResourceId = useCallback( - (resourceId, mode) => { - const mixerPair = mixersByResourceId[resourceId]; + const { volumeObj } = result; + const updatedVolumeObj = { ...volumeObj, mute: muting }; + await context.jamClient.SessionSetTrackVolumeData(mixerId, mode, updatedVolumeObj); - if (!mixerPair) return null; - - if (!mode) { - return mixerPair; - } else { - if (mode === MIX_MODES.MASTER) { - return mixerPair.master; - } else { - return mixerPair.personal; - } - } - }, - [mixersByResourceId] - ); - - const mute = useCallback( - async (mixerId, mode, muting) => { - if (mode == null) { - mode = mixMode; - } - - const result = fillTrackVolumeObject(mixerId, mode); - if (!result) return; - - const { volumeObj } = result; - const updatedVolumeObj = { ...volumeObj, mute: muting }; - await context.jamClient.SessionSetTrackVolumeData(mixerId, mode, updatedVolumeObj); - - const updatedMixer = getMixer(mixerId, mode); - updatedMixer.mute = muting; - }, - [mixMode, getMixer, fillTrackVolumeObject] - ); + const updatedMixer = getMixer(mixerId, mode); + updatedMixer.mute = muting; + }, [mixMode, getMixer, fillTrackVolumeObject]); const getOriginalVolume = useCallback((mixers, gainType) => { let originalVolume = null; @@ -890,62 +748,56 @@ const useMixerHelper = () => { return originalVolume; }, []); - const faderChanged = useCallback( - async (data, mixers, gainType, controlGroup) => { - //console.log("MixerHelper: faderChanged called", { data, mixers, gainType, controlGroup }); - if (!Array.isArray(mixers)) { - mixers = [mixers]; + const faderChanged = useCallback(async (data, mixers, gainType, controlGroup) => { + //console.log("MixerHelper: faderChanged called", { data, mixers, gainType, controlGroup }); + if (!Array.isArray(mixers)) { + mixers = [mixers]; + } + const originalVolume = getOriginalVolume(mixers, gainType); + + + // Handle multiple mixers (master + personal pairs like web version) + const mixerIds = mixers.map(m => m.id); + const hasMasterAndPersonalControls = mixerIds.length === 2; + + for (let i = 0; i < mixers.length; i++) { + const m = mixers[i]; + + // Broadcast only when NOT dragging (matches web version logic) + const broadcast = !(data.dragging); + + // Determine mode for multiple mixers (like web version) + let mode = m.mode; + if (hasMasterAndPersonalControls) { + mode = i === 0 ? MIX_MODES.MASTER : MIX_MODES.PERSONAL; } - const originalVolume = getOriginalVolume(mixers, gainType); - // Handle multiple mixers (master + personal pairs like web version) - const mixerIds = mixers.map(m => m.id); - const hasMasterAndPersonalControls = mixerIds.length === 2; - - for (let i = 0; i < mixers.length; i++) { - const m = mixers[i]; - - // Broadcast only when NOT dragging (matches web version logic) - const broadcast = !data.dragging; - - // Determine mode for multiple mixers (like web version) - let mode = m.mode; - if (hasMasterAndPersonalControls) { - mode = i === 0 ? MIX_MODES.MASTER : MIX_MODES.PERSONAL; - } - - const result = fillTrackVolumeObject(m.id, mode, allMixers, broadcast); - if (result == null) { - console.error('MixerHelper: faderChanged: mixer is null, skipping', m, gainType, controlGroup); - continue; - } - - const { mixer, volumeObj } = result; - - // Handle relative volume adjustments for music category (matches web version) - const relative = - gainType === 'music' && - (mixer.name === CategoryGroupIds.UserMedia || mixer.name === CategoryGroupIds.MediaTrack); - - await setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup, volumeObj); - - // Redux: Update local mixer state via dispatch - const newVolume = faderHelpers.convertPercentToAudioTaper(data.percentage); - dispatch( - updateMixer({ - mixerId: mixer.id, - mode: mixer.mode, - updates: { - volume_left: newVolume - } - }) - ); + const result = fillTrackVolumeObject(m.id, mode, allMixers, broadcast); + if (result == null) { + console.error("MixerHelper: faderChanged: mixer is null, skipping", m, gainType, controlGroup); + continue; } - }, - [getOriginalVolume, getMixer, fillTrackVolumeObject, setMixerVolume, allMixers, trackVolumeObject, dispatch] - ); - const initGain = useCallback(mixer => { + const { mixer, volumeObj } = result; + + // Handle relative volume adjustments for music category (matches web version) + const relative = gainType === 'music' && (mixer.name === CategoryGroupIds.UserMedia || mixer.name === CategoryGroupIds.MediaTrack); + + await setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup, volumeObj); + + // Redux: Update local mixer state via dispatch + const newVolume = faderHelpers.convertPercentToAudioTaper(data.percentage); + dispatch(updateMixer({ + mixerId: mixer.id, + mode: mixer.mode, + updates: { + volume_left: newVolume + } + })); + } + }, [getOriginalVolume, getMixer, fillTrackVolumeObject, setMixerVolume, allMixers, trackVolumeObject, dispatch]); + + const initGain = useCallback((mixer) => { if (Array.isArray(mixer)) { mixer = mixer[0]; } @@ -956,54 +808,43 @@ const useMixerHelper = () => { console.debug('initGain called for mixer:', mixer?.id); }, []); - const panChanged = useCallback( - async (data, mixers, groupId) => { - if (!Array.isArray(mixers)) { - mixers = [mixers]; - } - const result = []; - for (const mixer of mixers) { - const broadcast = !data.dragging; - const fillResult = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers, broadcast); - if (!fillResult) continue; + const panChanged = useCallback(async (data, mixers, groupId) => { + if (!Array.isArray(mixers)) { mixers = [mixers]; } + const result = []; + for (const mixer of mixers) { + const broadcast = !(data.dragging); + const fillResult = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers, broadcast); + if (!fillResult) continue; - const { mixer: filledMixer } = fillResult; - await setMixerPan(filledMixer, data.percentage); + const { mixer: filledMixer } = fillResult; + await setMixerPan(filledMixer, data.percentage); - const updatedMixer = getMixer(filledMixer.id, filledMixer.mode, allMixers); - updatedMixer.pan = trackVolumeObject.pan; - result.push(trackVolumeObject.pan); - } - return result; - }, - [getMixer, fillTrackVolumeObject, setMixerPan, allMixers, trackVolumeObject] - ); + const updatedMixer = getMixer(filledMixer.id, filledMixer.mode, allMixers); + updatedMixer.pan = trackVolumeObject.pan; + result.push(trackVolumeObject.pan); + } + return result; + }, [getMixer, fillTrackVolumeObject, setMixerPan, allMixers, trackVolumeObject]); - const initPan = useCallback( - mixer => { - const panPercent = panHelpers.convertPanToPercent(mixer.pan); - faderHelpers.setFaderValue(mixer.id, panPercent, Math.abs(mixer.pan)); - faderHelpers.showFader(mixer.id); - }, - [faderHelpers, panHelpers] - ); + const initPan = useCallback((mixer) => { + const panPercent = panHelpers.convertPanToPercent(mixer.pan); + faderHelpers.setFaderValue(mixer.id, panPercent, Math.abs(mixer.pan)); + faderHelpers.showFader(mixer.id); + }, [faderHelpers, panHelpers]); - const loopChanged = useCallback( - async (mixer, shouldLoop) => { - const result = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers); - if (!result) return; + const loopChanged = useCallback(async (mixer, shouldLoop) => { + const result = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers); + if (!result) return; - const { volumeObj } = result; - const updatedVolumeObj = { ...volumeObj, loop: shouldLoop }; + const { volumeObj } = result; + const updatedVolumeObj = { ...volumeObj, loop: shouldLoop }; - setTrackVolumeObject(updatedVolumeObj); - await context.jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, updatedVolumeObj); + setTrackVolumeObject(updatedVolumeObj); + await context.jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, updatedVolumeObj); - const updatedMixer = getMixer(mixer.id, mixer.mode, allMixers); - updatedMixer.loop = shouldLoop; - }, - [getMixer, fillTrackVolumeObject, allMixers, setTrackVolumeObject] - ); + const updatedMixer = getMixer(mixer.id, mixer.mode, allMixers); + updatedMixer.loop = shouldLoop; + }, [getMixer, fillTrackVolumeObject, allMixers, setTrackVolumeObject]); const percentFromMixerValue = useCallback((min, max, value) => { try { @@ -1019,7 +860,7 @@ const useMixerHelper = () => { const percentToMixerValue = useCallback((min, max, percent) => { const range = Math.abs(max - min); const multiplier = percent / 100; - let value = min + multiplier * range; + let value = min + (multiplier * range); if (value < min) { value = min; @@ -1043,29 +884,26 @@ const useMixerHelper = () => { mixerStats.count++; }, []); - const dumpVUStats = useCallback(currentVuStats => { - logger.debug('VU STAT DUMP'); + const dumpVUStats = useCallback((currentVuStats) => { + logger.debug("VU STAT DUMP"); for (const mixerId in currentVuStats) { const mixerStat = currentVuStats[mixerId]; - logger.debug('VU STAT: #{mixerStat.group_name} count=#{mixerStat.count}'); + logger.debug("VU STAT: #{mixerStat.group_name} count=#{mixerStat.count}"); } }, []); // Change updateVU to use the ref - const updateVU = useCallback( - (mixerId, mode, leftValue, leftClipping, rightValue, rightClipping) => { - if (!isReady.current) { - return; - } + const updateVU = useCallback((mixerId, mode, leftValue, leftClipping, rightValue, rightClipping) => { + if (!isReady.current) { + return; + } - const mixer = getMixer(mixerId, mode); + const mixer = getMixer(mixerId, mode); - if (mixer) { - updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping); - } - }, - [getMixer, updateVU3] - ); + if (mixer) { + updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping); + } + }, [getMixer, updateVU3]); const getTrackInfo = useCallback(async () => { return await context.JK.TrackHelpers.getTrackInfo(context.jamClient, masterMixers); @@ -1087,59 +925,54 @@ const useMixerHelper = () => { return foundMixers; }, []); - const refreshMixer = useCallback( - (mixers, currentAllMixers) => { - if (!mixers || !mixers.mixer) return null; + const refreshMixer = useCallback((mixers, currentAllMixers) => { + if (!mixers || !mixers.mixer) return null; - let updateMixers = null; - if (Array.isArray(mixers.mixer)) { - if (mixers.mixer.length > 0) { - updateMixers = []; - for (const mixer of mixers.mixer) { - updateMixers.push(getMixer(mixer.id, mixer.mode, currentAllMixers)); - } + let updateMixers = null; + if (Array.isArray(mixers.mixer)) { + if (mixers.mixer.length > 0) { + updateMixers = []; + for (const mixer of mixers.mixer) { + updateMixers.push(getMixer(mixer.id, mixer.mode, currentAllMixers)); } - } else { - updateMixers = getMixer(mixers.mixer.id, mixers.mixer.mode, currentAllMixers); } + } else { + updateMixers = getMixer(mixers.mixer.id, mixers.mixer.mode, currentAllMixers); + } - let updatedVUMixers = null; - if (Array.isArray(mixers.vuMixer)) { - updatedVUMixers = []; - for (const vuMixer of mixers.vuMixer) { - updatedVUMixers.push(getMixer(vuMixer.id, vuMixer.mode, currentAllMixers)); - } - } else { - updatedVUMixers = getMixer(mixers.vuMixer.id, mixers.vuMixer.mode, currentAllMixers); + let updatedVUMixers = null; + if (Array.isArray(mixers.vuMixer)) { + updatedVUMixers = []; + for (const vuMixer of mixers.vuMixer) { + updatedVUMixers.push(getMixer(vuMixer.id, vuMixer.mode, currentAllMixers)); } + } else { + updatedVUMixers = getMixer(mixers.vuMixer.id, mixers.vuMixer.mode, currentAllMixers); + } - let updateMuteMixers = null; - if (Array.isArray(mixers.muteMixer)) { - updateMuteMixers = []; - for (const muteMixer of mixers.muteMixer) { - updateMuteMixers.push(getMixer(muteMixer.id, muteMixer.mode, currentAllMixers)); - } - } else { - updateMuteMixers = getMixer(mixers.muteMixer.id, mixers.muteMixer.mode, currentAllMixers); + let updateMuteMixers = null; + if (Array.isArray(mixers.muteMixer)) { + updateMuteMixers = []; + for (const muteMixer of mixers.muteMixer) { + updateMuteMixers.push(getMixer(muteMixer.id, muteMixer.mode, currentAllMixers)); } + } else { + updateMuteMixers = getMixer(mixers.muteMixer.id, mixers.muteMixer.mode, currentAllMixers); + } - const oppositeMixer = mixers.oppositeMixer - ? getMixer(mixers.oppositeMixer.id, mixers.oppositeMixer.mode, currentAllMixers) - : null; + const oppositeMixer = mixers.oppositeMixer ? getMixer(mixers.oppositeMixer.id, mixers.oppositeMixer.mode, currentAllMixers) : null; - if (updateMixers) { - return { - mixer: updateMixers, - vuMixer: updatedVUMixers, - muteMixer: updateMuteMixers, - oppositeMixer: oppositeMixer - }; - } else { - return null; - } - }, - [getMixer] - ); + if (updateMixers) { + return { + mixer: updateMixers, + vuMixer: updatedVUMixers, + muteMixer: updateMuteMixers, + oppositeMixer: oppositeMixer + }; + } else { + return null; + } + }, [getMixer]); const recordingName = useCallback(() => { return currentSession.recordingName(); @@ -1149,6 +982,7 @@ const useMixerHelper = () => { return currentSession.jamTrackName(); }, [currentSession]); + return { isReady, session: currentSession, // Return currentSession from Redux instead of local state @@ -1173,6 +1007,6 @@ const useMixerHelper = () => { jamTracks, recordedTracks }; -}; +} export default useMixerHelper; diff --git a/jam-ui/src/hooks/useSessionModel.js b/jam-ui/src/hooks/useSessionModel.js index e051227f7..60f4141d3 100644 --- a/jam-ui/src/hooks/useSessionModel.js +++ b/jam-ui/src/hooks/useSessionModel.js @@ -1,7 +1,7 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; -import { useDebounceCallback } from './useDebounceCallback'; +import { debounce } from 'lodash'; // Add lodash for debouncing import { useJamClient } from '../context/JamClientContext'; import { selectActiveSession, @@ -10,7 +10,11 @@ import { setSessionId, updateSessionData } from '../store/features/activeSessionSlice'; -import { setBackingTracks, setJamTracks, setRecordedTracks } from '../store/features/mediaSlice'; +import { + setBackingTracks, + setJamTracks, + setRecordedTracks +} from '../store/features/mediaSlice'; import useGearUtils from './useGearUtils'; import useTrackHelpers from './useTrackHelpers'; import useRecordingHelpers from './useRecordingHelpers'; @@ -62,27 +66,21 @@ export default function useSessionModel(app, server, sessionScreen) { return sessionIdRef.current !== null; }, []); - const setCurrentSessionId = useCallback( - id => { - console.log('Setting current session ID to: ', id); - dispatch(setSessionId(id)); - }, - [dispatch] - ); + const setCurrentSessionId = useCallback((id) => { + console.log("Setting current session ID to: ", id); + dispatch(setSessionId(id)); + }, [dispatch]); - const setCurrentSession = useCallback( - updater => { - if (typeof updater === 'function') { - // Handle functional setState pattern - const currentData = currentSession; - const newData = updater(currentData); - dispatch(updateSessionData(newData)); - } else { - dispatch(updateSessionData(updater)); - } - }, - [dispatch, currentSession] - ); + const setCurrentSession = useCallback((updater) => { + if (typeof updater === 'function') { + // Handle functional setState pattern + const currentData = currentSession; + const newData = updater(currentData); + dispatch(updateSessionData(newData)); + } else { + dispatch(updateSessionData(updater)); + } + }, [dispatch, currentSession]); const history = useHistory(); @@ -124,14 +122,14 @@ export default function useSessionModel(app, server, sessionScreen) { const clientId = jamClient?.clientID || 'unknown'; // Promise management functions (from useSessionEnter) - const resolvePendingPromises = useCallback(inputTracks => { + const resolvePendingPromises = useCallback((inputTracks) => { for (const { resolve } of pendingPromisesRef.current.values()) { resolve(inputTracks); } pendingPromisesRef.current.clear(); }, []); - const rejectPendingPromises = useCallback(reason => { + const rejectPendingPromises = useCallback((reason) => { for (const { reject } of pendingPromisesRef.current.values()) { reject(reason); } @@ -139,21 +137,15 @@ export default function useSessionModel(app, server, sessionScreen) { }, []); // Event handlers (from useSessionEnter) - const onWatchedInputs = useCallback( - inputTracks => { - resolvePendingPromises(inputTracks); - }, - [resolvePendingPromises] - ); + const onWatchedInputs = useCallback((inputTracks) => { + resolvePendingPromises(inputTracks); + }, [resolvePendingPromises]); - const onMixersChanged = useCallback( - (type, text, trackInfo) => { - if (text === 'RebuildAudioIoControl' && trackInfo.userTracks.length > 0) { - resolvePendingPromises(trackInfo.userTracks); - } - }, - [resolvePendingPromises] - ); + const onMixersChanged = useCallback((type, text, trackInfo) => { + if (text === 'RebuildAudioIoControl' && trackInfo.userTracks.length > 0) { + resolvePendingPromises(trackInfo.userTracks); + } + }, [resolvePendingPromises]); // Initialize session model // useEffect(() => { @@ -190,15 +182,12 @@ export default function useSessionModel(app, server, sessionScreen) { return currentSessionIdRef.current; }, [currentSessionIdRef]); - const start = useCallback( - sessionId => { - setCurrentSessionId(sessionId); - setStartTime(new Date().getTime()); - }, - [setCurrentSessionId] - ); + const start = useCallback((sessionId) => { + setCurrentSessionId(sessionId); + setStartTime(new Date().getTime()); + }, [setCurrentSessionId]); - const setUserTracksState = useCallback(_userTracks => { + const setUserTracksState = useCallback((_userTracks) => { setUserTracks(_userTracks); }, []); @@ -212,7 +201,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Check if metronome is open const isMetronomeOpen = useCallback(() => { let metronomeOpen = false; - participants().forEach(participant => { + participants().forEach((participant) => { if (participant.metronome_open) { metronomeOpen = true; } @@ -244,7 +233,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Get backing tracks const backingTracks = useCallback(() => { let backingTracks = []; - participants().forEach(participant => { + participants().forEach((participant) => { if (participant.backing_tracks && participant.backing_tracks.length > 0) { backingTracks = participant.backing_tracks; } @@ -286,7 +275,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Check if self opened jam tracks const selfOpenedJamTracks = useCallback(() => { - return currentSession && currentSession.jam_track_initiator_id === window.JK?.currentUserId; + return currentSession && (currentSession.jam_track_initiator_id === window.JK?.currentUserId); }, [currentSession]); // Get backing track @@ -302,7 +291,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Get creator ID const creatorId = useCallback(() => { if (!currentSession) { - throw 'creator is not known'; + throw "creator is not known"; } return currentSession.user_id; }, [currentSession]); @@ -316,12 +305,13 @@ export default function useSessionModel(app, server, sessionScreen) { } else if (currentSession.musician_access && !currentSession.approval_required) { return SESSION_PRIVACY_MAP.public; } + }, [currentSession]); // Check if already in session const alreadyInSession = useCallback(() => { let inSession = false; - participants().forEach(participant => { + participants().forEach((participant) => { if (participant.user.id === window.JK?.currentUserId) { inSession = true; } @@ -343,21 +333,18 @@ export default function useSessionModel(app, server, sessionScreen) { }, []); // Mixer mode functions - const onMixerModeChanged = useCallback(newMixerMode => { + const onMixerModeChanged = useCallback((newMixerMode) => { setMixerMode(newMixerMode); - const mode = newMixerMode === MIX_MODES.MASTER ? 'master' : 'personal'; - logger.debug('onMixerModeChanged:' + mode); + const mode = newMixerMode === MIX_MODES.MASTER ? "master" : "personal"; + logger.debug("onMixerModeChanged:" + mode); // Trigger event - in React this would be through state updates or callbacks }, []); - const setMixerModeState = useCallback( - newMixerMode => { - if (mixerMode !== newMixerMode) { - onMixerModeChanged(newMixerMode); - } - }, - [mixerMode, onMixerModeChanged] - ); + const setMixerModeState = useCallback((newMixerMode) => { + if (mixerMode !== newMixerMode) { + onMixerModeChanged(newMixerMode); + } + }, [mixerMode, onMixerModeChanged]); const isMasterMixMode = useCallback(() => { return mixerMode === MIX_MODES.MASTER; @@ -377,10 +364,10 @@ export default function useSessionModel(app, server, sessionScreen) { // Check if tracks are already available const inputTracks = await getUserTracks(); - logger.debug('isNoInputProfile', await isNoInputProfile()); - logger.debug('inputTracks', inputTracks); - if (inputTracks.length > 0 || (await isNoInputProfile())) { - logger.debug('on page enter, tracks are already available'); + 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; } @@ -410,46 +397,44 @@ export default function useSessionModel(app, server, sessionScreen) { // Duplicate declaration of SessionPageEnter removed to fix redeclaration error. + // Join session - const joinSession = useCallback( - async sessionId => { - logger.debug('SessionModel.joinSession(' + sessionId + ')'); + const joinSession = useCallback(async (sessionId) => { + logger.debug("SessionModel.joinSession(" + sessionId + ")"); - const deferred = joinSessionRest({ session_id: sessionId }); - setJoinDeferred(deferred); + const deferred = joinSessionRest({ session_id: sessionId }); + setJoinDeferred(deferred); - try { - const response = await deferred; + try { + const response = await deferred; - if (!inSession()) { - logger.debug('user left before fully joined to session. telling server again that they have left'); - // leaveSessionRest(response.id); // Would need to implement - return; - } - - logger.debug('calling jamClient.JoinSession'); - if (!alreadyInSession()) { - // GA tracking would go here - } - - resetRecordingState(currentSessionIdRef.current); - // Register message callbacks would go here - - // Trigger session started event - // $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId}}); - - refreshCurrentSession(true); - } catch (error) { - updateCurrentSession(null); + if (!inSession()) { + logger.debug("user left before fully joined to session. telling server again that they have left"); + // leaveSessionRest(response.id); // Would need to implement + return; } - return deferred; - }, - [currentSessionIdRef] - ); + logger.debug("calling jamClient.JoinSession"); + if (!alreadyInSession()) { + // GA tracking would go here + } + + resetRecordingState(currentSessionIdRef.current); + // Register message callbacks would go here + + // Trigger session started event + // $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId}}); + + refreshCurrentSession(true); + } catch (error) { + updateCurrentSession(null); + } + + return deferred; + }, [currentSessionIdRef]); // Set recording model (from useSessionLeave) - const setRecordingModel = useCallback(recordingModel => { + const setRecordingModel = useCallback((recordingModel) => { recordingModelRef.current = recordingModel; }, []); @@ -460,17 +445,18 @@ export default function useSessionModel(app, server, sessionScreen) { try { await deleteParticipant(clientId); - logger.debug('Successfully left session via REST API'); + logger.debug("Successfully left session via REST API"); } catch (error) { - logger.error('Error leaving session via REST:', error); + logger.error("Error leaving session via REST:", error); // Don't throw - we want to continue with client-side cleanup } }, [jamClient, logger]); // Perform the actual leave session (from useSessionLeave) const performLeaveSession = useCallback(async () => { + if (isLeavingRef.current) { - logger.debug('Leave session already in progress'); + logger.debug("Leave session already in progress"); return; } @@ -478,25 +464,27 @@ export default function useSessionModel(app, server, sessionScreen) { const sessionId = currentSessionIdRef.current; try { - logger.debug('Starting session leave process'); + logger.debug("Starting session leave process"); // Stop recording if needed if (recordingModelRef.current?.stopRecordingIfNeeded) { try { await recordingModelRef.current.stopRecordingIfNeeded(); - logger.debug('Recording stopped successfully'); + logger.debug("Recording stopped successfully"); } catch (error) { - logger.error('Error stopping recording:', error); + logger.error("Error stopping recording:", error); } } + + // Leave the session via jamClient (don't wait for REST) - logger.debug('Leaving session via jamClient for sessionId:', sessionId); + logger.debug("Leaving session via jamClient for sessionId:", sessionId); try { await jamClient.LeaveSession({ sessionID: sessionId }); - logger.debug('Successfully left session via jamClient'); + logger.debug("Successfully left session via jamClient"); } catch (error) { - logger.error('Error leaving session via jamClient:', error); + logger.error("Error leaving session via jamClient:", error); } // Make REST call to server (fire and forget for faster UX) @@ -504,21 +492,23 @@ export default function useSessionModel(app, server, sessionScreen) { // Unregister callbacks try { - await jamClient.SessionRegisterCallback(''); - await jamClient.SessionSetAlertCallback(''); + await jamClient.SessionRegisterCallback(""); + await jamClient.SessionSetAlertCallback(""); await jamClient.SessionSetConnectionStatusRefreshRate(0); - logger.debug('Callbacks unregistered successfully'); + logger.debug("Callbacks unregistered successfully"); } catch (error) { - logger.error('Error unregistering callbacks:', error); + logger.error("Error unregistering callbacks:", error); } + + // Call session page leave try { // Note: SessionPageLeave would need to be imported from useSessionUtils // For now, we'll skip this as it's not directly available - logger.debug('Session page leave completed'); + logger.debug("Session page leave completed"); } catch (error) { - logger.error('Error in SessionPageLeave:', error); + logger.error("Error in SessionPageLeave:", error); } // Trigger session ended event @@ -528,9 +518,11 @@ export default function useSessionModel(app, server, sessionScreen) { // logger.debug("Session ended event triggered"); // } - logger.debug('Session leave completed successfully'); + logger.debug("Session leave completed successfully"); + + } catch (error) { - logger.error('Unexpected error during session leave:', error); + logger.error("Unexpected error during session leave:", error); throw error; } finally { // Reset session state @@ -546,49 +538,49 @@ export default function useSessionModel(app, server, sessionScreen) { }, [performLeaveSession]); // Handle leave session with behavior (navigation, notifications, etc.) (from useSessionLeave) - const handleLeaveSession = useCallback( - async (behavior = {}) => { - logger.debug('Handling leave session with behavior:', behavior); + const handleLeaveSession = useCallback(async (behavior = {}) => { + logger.debug("Handling leave session with behavior:", behavior); - try { - // Handle notifications - // if (behavior.notify && window.JK?.app?.layout) { - // window.JK.app.layout.notify(behavior.notify); - // } + try { + // Handle notifications + // if (behavior.notify && window.JK?.app?.layout) { + // window.JK.app.layout.notify(behavior.notify); + // } - // // Allow leave session (trigger any leave confirmation logic) - // if (window.SessionActions?.allowLeaveSession) { - // window.SessionActions.allowLeaveSession.trigger(); - // } + // // Allow leave session (trigger any leave confirmation logic) + // if (window.SessionActions?.allowLeaveSession) { + // window.SessionActions.allowLeaveSession.trigger(); + // } - // Perform the leave operation - await leaveSession(); + // Perform the leave operation + await leaveSession(); - // Handle navigation after successful leave - // if (behavior.location) { - // if (typeof behavior.location === 'number') { - // window.history.go(behavior.location); - // } else { - // window.location = behavior.location; - // } - // } else if (behavior.hash) { - // window.location.hash = behavior.hash; - // } else { - // logger.warn("No location specified in leaveSession action, defaulting to home", behavior); - // window.location = '/client#/home'; - // } + // Handle navigation after successful leave + // if (behavior.location) { + // if (typeof behavior.location === 'number') { + // window.history.go(behavior.location); + // } else { + // window.location = behavior.location; + // } + // } else if (behavior.hash) { + // window.location.hash = behavior.hash; + // } else { + // logger.warn("No location specified in leaveSession action, defaulting to home", behavior); + // window.location = '/client#/home'; + // } - // Handle lesson session rating if applicable - // Note: This would need additional context/state to implement fully - // For now, just log that rating logic would go here - //logger.debug("Lesson session rating logic would be handled here"); - } catch (error) { - logger.error('Error handling leave session:', error); - throw error; - } - }, - [leaveSession, logger] - ); + // Handle lesson session rating if applicable + // Note: This would need additional context/state to implement fully + // For now, just log that rating logic would go here + //logger.debug("Lesson session rating logic would be handled here"); + + + + } catch (error) { + logger.error("Error handling leave session:", error); + throw error; + } + }, [leaveSession, logger]); // Check if currently leaving (from useSessionLeave) const isLeaving = useCallback(() => { @@ -603,28 +595,27 @@ export default function useSessionModel(app, server, sessionScreen) { // Refresh current session const refreshCurrentSession = useCallback(async (force = false) => { if (force) { - logger.debug('refreshCurrentSession(force=true)'); + logger.debug("refreshCurrentSession(force=true)"); } await refreshCurrentSessionRest(sessionChanged, force); }, []); // Track changes handler - debounced to prevent excessive session refreshes - // Uses useDebounceCallback for stable timer (doesn't reset when deps change) - const trackChanges = useDebounceCallback((header, payload) => { + const trackChanges = useCallback(debounce((header, payload) => { if (currentTrackChanges < payload.track_changes_counter) { - logger.debug('track_changes_counter = stale. refreshing...'); + logger.debug("track_changes_counter = stale. refreshing..."); refreshCurrentSession(); } else { if (header.type !== 'HEARTBEAT_ACK') { - logger.info('track_changes_counter = fresh. skipping refresh...', header, payload); + logger.info("track_changes_counter = fresh. skipping refresh...", header, payload); } } - }, 500); + }, 500), [currentTrackChanges, refreshCurrentSession]); // Subscribe to session changes const subscribe = useCallback((subscriberId, sessionChangedCallback) => { - logger.debug('SessionModel.subscribe(' + subscriberId + ', [callback])'); + logger.debug("SessionModel.subscribe(" + subscriberId + ", [callback])"); setSubscribers(prev => ({ ...prev, [subscriberId]: sessionChangedCallback @@ -639,245 +630,199 @@ export default function useSessionModel(app, server, sessionScreen) { }, [subscribers, currentSession]); // Session ended cleanup - const sessionEnded = useCallback( - fullyJoined => { - // Cleanup logic - setUserTracks(null); - setStartTime(null); - setJoinDeferred(null); - setMixerMode(MIX_MODES.PERSONAL); + const sessionEnded = useCallback((fullyJoined) => { + // Cleanup logic + setUserTracks(null); + setStartTime(null); + setJoinDeferred(null); + setMixerMode(MIX_MODES.PERSONAL); - if (sessionPageEnterDeferred) { - // sessionPageEnterDeferred.reject('session_over'); - setSessionPageEnterDeferred(null); - } + if (sessionPageEnterDeferred) { + // sessionPageEnterDeferred.reject('session_over'); + setSessionPageEnterDeferred(null); + } - setCurrentParticipants({}); - setPreviousAllTracks({ - userTracks: [], - backingTracks: [], - metronomeTracks: [] - }); - setOpenBackingTrack(null); - setShownAudioMediaMixerHelp(false); - setControlsLockedForJamTrackRecording(false); + setCurrentParticipants({}); + setPreviousAllTracks({ + userTracks: [], + backingTracks: [], + metronomeTracks: [] + }); + setOpenBackingTrack(null); + setShownAudioMediaMixerHelp(false); + setControlsLockedForJamTrackRecording(false); - if (fullyJoined) { - // $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionIdRef.current}}); - } - setCurrentSessionId(null); - }, - [sessionPageEnterDeferred] - ); + if (fullyJoined) { + // $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionIdRef.current}}); + } + setCurrentSessionId(null); + }, [sessionPageEnterDeferred]); // Update current session - const updateCurrentSession = useCallback( - sessionData => { - if (sessionData !== null) { - setCurrentOrLastSession(sessionData); - } + const updateCurrentSession = useCallback((sessionData) => { + if (sessionData !== null) { + setCurrentOrLastSession(sessionData); + } - const beforeUpdate = currentSession; - setCurrentSession(sessionData); + const beforeUpdate = currentSession; + setCurrentSession(sessionData); - if (sessionData === null) { - sessionEnded(beforeUpdate !== null); - } - }, - [currentSession, sessionEnded, setCurrentSession] - ); + if (sessionData === null) { + sessionEnded(beforeUpdate !== null); + } + }, [currentSession, sessionEnded, setCurrentSession]); // Update session info - const updateSessionInfo = useCallback( - (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 - ); - setCurrentTrackChanges(response.track_changes_counter); + const updateSessionInfo = useCallback((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); + setCurrentTrackChanges(response.track_changes_counter); - // sendClientParticipantChanges logic would go here + // sendClientParticipantChanges logic would go here - updateCurrentSession(response); + updateCurrentSession(response); - // Extract and dispatch media arrays to Redux - // Extract backing tracks from participants - let extractedBackingTracks = []; - if (response.participants) { - response.participants.forEach(participant => { - if (participant.backing_tracks && participant.backing_tracks.length > 0) { - extractedBackingTracks = participant.backing_tracks; - } - }); - } - // Only update backing tracks if server has data - // Avoids race condition: local state set from native client gets cleared - // by session refresh before server sync completes - if (extractedBackingTracks.length > 0) { - dispatch(setBackingTracks(extractedBackingTracks)); - console.log('[useSessionModel] Extracted backing tracks:', extractedBackingTracks); - } - - // Extract jam tracks - const extractedJamTracks = - response.jam_track && response.jam_track.tracks - ? response.jam_track.tracks.filter(track => track.track_type === 'Track') - : []; - dispatch(setJamTracks(extractedJamTracks)); - console.log('[useSessionModel] Extracted jam tracks:', extractedJamTracks); - - // Extract recorded tracks - const extractedRecordedTracks = - response.claimed_recording && response.claimed_recording.recording - ? response.claimed_recording.recording.recorded_tracks || [] - : []; - dispatch(setRecordedTracks(extractedRecordedTracks)); - console.log('[useSessionModel] Extracted recorded tracks:', extractedRecordedTracks); - - if (callback) { - callback(); - } - } else { - logger.info( - 'ignoring refresh because we already have current: ' + - currentTrackChanges + - ', seen: ' + - response.track_changes_counter - ); + // Extract and dispatch media arrays to Redux + // Extract backing tracks from participants + let extractedBackingTracks = []; + if (response.participants) { + response.participants.forEach((participant) => { + if (participant.backing_tracks && participant.backing_tracks.length > 0) { + extractedBackingTracks = participant.backing_tracks; + } + }); } - }, - [currentTrackChanges, updateCurrentSession, dispatch] - ); + // Only update backing tracks if server has data + // Avoids race condition: local state set from native client gets cleared + // by session refresh before server sync completes + if (extractedBackingTracks.length > 0) { + dispatch(setBackingTracks(extractedBackingTracks)); + console.log('[useSessionModel] Extracted backing tracks:', extractedBackingTracks); + } + + // Extract jam tracks + const extractedJamTracks = response.jam_track && response.jam_track.tracks + ? response.jam_track.tracks.filter(track => track.track_type === 'Track') + : []; + dispatch(setJamTracks(extractedJamTracks)); + console.log('[useSessionModel] Extracted jam tracks:', extractedJamTracks); + + // Extract recorded tracks + const extractedRecordedTracks = response.claimed_recording && response.claimed_recording.recording + ? response.claimed_recording.recording.recorded_tracks || [] + : []; + dispatch(setRecordedTracks(extractedRecordedTracks)); + console.log('[useSessionModel] Extracted recorded tracks:', extractedRecordedTracks); + + if (callback) { + callback(); + } + } else { + logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); + } + }, [currentTrackChanges, updateCurrentSession, dispatch]); // Refresh current session REST call - const refreshCurrentSessionRest = useCallback( - async (callback, force) => { - if (!inSession()) { - logger.debug('refreshCurrentSession skipped: '); - return; - } + const refreshCurrentSessionRest = useCallback(async (callback, force) => { + if (!inSession()) { + logger.debug("refreshCurrentSession skipped: "); + return; + } - if (requestingSessionRefresh) { - logger.debug('queueing refresh'); - setPendingSessionRefresh(true); - return; - } + if (requestingSessionRefresh) { + logger.debug("queueing refresh"); + setPendingSessionRefresh(true); + return; + } - setRequestingSessionRefresh(true); + setRequestingSessionRefresh(true); - try { - const response = await getSession(currentSessionIdRef.current); - const data = await response.json(); - updateSessionInfo(data, callback, force); - } catch (jqXHR) { - if (jqXHR.status !== 404) { - app.notifyServerError(jqXHR, 'Unable to refresh session data'); - } else { - logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone"); - } - } finally { - setRequestingSessionRefresh(false); - if (pendingSessionRefresh) { - setPendingSessionRefresh(false); - refreshCurrentSessionRest(sessionChanged, force); - } + try { + const response = await getSession(currentSessionIdRef.current); + const data = await response.json(); + updateSessionInfo(data, callback, force); + } catch (jqXHR) { + if (jqXHR.status !== 404) { + app.notifyServerError(jqXHR, "Unable to refresh session data"); + } else { + logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone"); } - }, - [ - inSession, - requestingSessionRefresh, - pendingSessionRefresh, - currentSessionIdRef, - updateSessionInfo, - sessionChanged, - app - ] - ); + } finally { + setRequestingSessionRefresh(false); + if (pendingSessionRefresh) { + setPendingSessionRefresh(false); + refreshCurrentSessionRest(sessionChanged, force); + } + } + }, [inSession, requestingSessionRefresh, pendingSessionRefresh, currentSessionIdRef, updateSessionInfo, sessionChanged, app]); // Participant management functions - const participantForClientId = useCallback( - clientId => { - const foundParticipant = participants().find(participant => participant.client_id === clientId); - return foundParticipant; - }, - [participants] - ); + const participantForClientId = useCallback((clientId) => { + const foundParticipant = participants().find(participant => participant.client_id === clientId); + return foundParticipant; + }, [participants]); // Track sync functionality - const syncTracks = useCallback( - async allTracks => { - if (!inSession()) { - logger.debug('dropping queued up sync tracks because no longer in session'); - return null; - } + const syncTracks = useCallback(async (allTracks) => { + if (!inSession()) { + logger.debug("dropping queued up sync tracks because no longer in session"); + return null; + } - if (!allTracks) { - allTracks = await getTrackInfo(); - } + if (!allTracks) { + allTracks = await getTrackInfo(); + } - const inputTracks = allTracks.userTracks; - const backingTracksData = allTracks.backingTracks; - const metronomeTracks = allTracks.metronomeTracks; + const inputTracks = allTracks.userTracks; + const backingTracksData = allTracks.backingTracks; + const metronomeTracks = allTracks.metronomeTracks; - const syncTrackRequest = { - client_id: clientId, - tracks: inputTracks, - backing_tracks: backingTracksData, - metronome_open: metronomeTracks.length > 0, - id: id() - }; + const syncTrackRequest = { + client_id: clientId, + tracks: inputTracks, + backing_tracks: backingTracksData, + metronome_open: metronomeTracks.length > 0, + id: id() + }; - // REST call would go here - // return rest.putTrackSyncChange(syncTrackRequest); - return Promise.resolve(); - }, - [inSession, getTrackInfo, clientId, id] - ); + // REST call would go here + // return rest.putTrackSyncChange(syncTrackRequest); + return Promise.resolve(); + }, [inSession, getTrackInfo, clientId, id]); // WebSocket disconnected handler - const onWebsocketDisconnected = useCallback( - async in_error => { - if (currentSessionIdRef.current) { - logger.debug('onWebsocketDisconnect: calling jamClient.LeaveSession for clientId=' + clientId); - await jamClient.LeaveSession({ sessionID: currentSessionIdRef.current }); - } - }, - [jamClient, currentSessionIdRef, clientId] - ); + const onWebsocketDisconnected = useCallback(async (in_error) => { + if (currentSessionIdRef.current) { + logger.debug("onWebsocketDisconnect: calling jamClient.LeaveSession for clientId=" + clientId); + await jamClient.LeaveSession({ sessionID: currentSessionIdRef.current }); + } + }, [jamClient, currentSessionIdRef, clientId]); // Find user by criteria - const findUserBy = useCallback( - finder => { - if (finder.clientId) { - const foundParticipant = participants().find(participant => participant.client_id === finder.clientId); - if (foundParticipant) { - return Promise.resolve(foundParticipant.user); - } + const findUserBy = useCallback((finder) => { + if (finder.clientId) { + const foundParticipant = participants().find(participant => participant.client_id === finder.clientId); + if (foundParticipant) { + return Promise.resolve(foundParticipant.user); } - return Promise.reject(); - }, - [participants] - ); + } + return Promise.reject(); + }, [participants]); // Alert handlers - const onDeadUserRemove = useCallback( - (type, text) => { - if (!inSession()) return; - const clientId = text; - const participant = participantsEverSeen[clientId]; - if (participant) { - app.notify({ - title: ALERT_TYPES[type]?.title || 'User Issue', - text: participant.user.name + ' is no longer sending audio.', - icon_url: '' // avatar URL - }); - // Track disabling logic would go here - } - }, - [inSession, participantsEverSeen, app] - ); + const onDeadUserRemove = useCallback((type, text) => { + if (!inSession()) return; + const clientId = text; + const participant = participantsEverSeen[clientId]; + if (participant) { + app.notify({ + "title": ALERT_TYPES[type]?.title || "User Issue", + "text": participant.user.name + " is no longer sending audio.", + "icon_url": "" // avatar URL + }); + // Track disabling logic would go here + } + }, [inSession, participantsEverSeen, app]); const onWindowBackgrounded = useCallback((type, text) => { // Window backgrounded logic @@ -895,68 +840,58 @@ export default function useSessionModel(app, server, sessionScreen) { // Broadcast stopped logic }, []); - const onPlaybackStateChange = useCallback( - (type, text) => { - if (sessionScreen) { - sessionScreen.onPlaybackStateChange(text); + const onPlaybackStateChange = useCallback((type, text) => { + if (sessionScreen) { + sessionScreen.onPlaybackStateChange(text); + } + }, [sessionScreen]); + + const onBackendMixerChanged = useCallback(async (type, text) => { + logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text); + + if (inSession() && text === "RebuildAudioIoControl") { + if (backendMixerAlertThrottleTimerRef.current) { + clearTimeout(backendMixerAlertThrottleTimerRef.current); } - }, - [sessionScreen] - ); - const onBackendMixerChanged = useCallback( - async (type, text) => { - logger.debug('BACKEND_MIXER_CHANGE alert. reason:' + text); - - if (inSession() && text === 'RebuildAudioIoControl') { - if (backendMixerAlertThrottleTimerRef.current) { - clearTimeout(backendMixerAlertThrottleTimerRef.current); + backendMixerAlertThrottleTimerRef.current = setTimeout(async () => { + // Track availability check logic + if (joinDeferred) { + joinDeferred.done(() => { + syncTracks(); + }); } + }, 100); + } else if (inSession() && (text === 'RebuildMediaControl' || text === 'RebuildRemoteUserControl')) { + const allTracks = await getTrackInfo(); + const backingTracksData = allTracks.backingTracks; + const previousBackingTracks = previousAllTracks.backingTracks; + const metronomeTracks = allTracks.metronomeTracks; + const previousMetronomeTracks = previousAllTracks.metronomeTracks; - backendMixerAlertThrottleTimerRef.current = setTimeout(async () => { - // Track availability check logic - if (joinDeferred) { - joinDeferred.done(() => { - syncTracks(); - }); - } - }, 100); - } else if (inSession() && (text === 'RebuildMediaControl' || text === 'RebuildRemoteUserControl')) { - const allTracks = await getTrackInfo(); - const backingTracksData = allTracks.backingTracks; - const previousBackingTracks = previousAllTracks.backingTracks; - const metronomeTracks = allTracks.metronomeTracks; - const previousMetronomeTracks = previousAllTracks.metronomeTracks; - - if ( - !(previousBackingTracks.length === 0 && backingTracksData.length === 0) && - JSON.stringify(previousBackingTracks) !== JSON.stringify(backingTracksData) - ) { - logger.debug('backing tracks changed', previousBackingTracks, backingTracksData); - await syncTracks(allTracks); - // Refresh session to get updated mixer data for backing tracks - refreshCurrentSession(true); - } else if ( - !(previousMetronomeTracks.length === 0 && metronomeTracks.length === 0) && - JSON.stringify(previousMetronomeTracks) !== JSON.stringify(metronomeTracks) - ) { - logger.debug('metronome state changed ', previousMetronomeTracks, metronomeTracks); - await syncTracks(allTracks); - // Refresh session to get updated mixer data - refreshCurrentSession(true); - } else { - refreshCurrentSession(true); - } - - setPreviousAllTracks(allTracks); - } else if (inSession() && text === 'Global Peer Input Mixer Mode') { - setMixerModeState(MIX_MODES.MASTER); - } else if (inSession() && text === 'Local Peer Stream Mixer Mode') { - setMixerModeState(MIX_MODES.PERSONAL); + if (!(previousBackingTracks.length === 0 && backingTracksData.length === 0) && + JSON.stringify(previousBackingTracks) !== JSON.stringify(backingTracksData)) { + logger.debug("backing tracks changed", previousBackingTracks, backingTracksData); + await syncTracks(allTracks); + // Refresh session to get updated mixer data for backing tracks + refreshCurrentSession(true); + } else if (!(previousMetronomeTracks.length === 0 && metronomeTracks.length === 0) && + JSON.stringify(previousMetronomeTracks) !== JSON.stringify(metronomeTracks)) { + logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks); + await syncTracks(allTracks); + // Refresh session to get updated mixer data + refreshCurrentSession(true); + } else { + refreshCurrentSession(true); } - }, - [inSession, joinDeferred, syncTracks, getTrackInfo, previousAllTracks, refreshCurrentSession, setMixerModeState] - ); + + setPreviousAllTracks(allTracks); + } else if (inSession() && (text === 'Global Peer Input Mixer Mode')) { + setMixerModeState(MIX_MODES.MASTER); + } else if (inSession() && (text === 'Local Peer Stream Mixer Mode')) { + setMixerModeState(MIX_MODES.PERSONAL); + } + }, [inSession, joinDeferred, syncTracks, getTrackInfo, previousAllTracks, refreshCurrentSession, setMixerModeState]); // Audio establishment tracking const setAudioEstablished = useCallback((clientId, audioEstablished) => { @@ -984,7 +919,7 @@ export default function useSessionModel(app, server, sessionScreen) { // FTUE functions (from useSessionUtils) const FTUEPageEnter = useCallback(async () => { - logger.debug('sessionUtils: FTUEPageEnter'); + logger.debug("sessionUtils: FTUEPageEnter"); clearAudioTimeout(); if (jamClient?.FTUEPageEnter) { await jamClient.FTUEPageEnter(); @@ -992,7 +927,7 @@ export default function useSessionModel(app, server, sessionScreen) { }, [jamClient, logger, clearAudioTimeout]); const FTUEPageLeave = useCallback(async () => { - logger.debug('sessionUtils: FTUEPageLeave'); + logger.debug("sessionUtils: FTUEPageLeave"); clearAudioTimeout(); if (jamClient?.FTUEPageLeave) { await jamClient.FTUEPageLeave(); @@ -1000,7 +935,7 @@ export default function useSessionModel(app, server, sessionScreen) { }, [jamClient, logger, clearAudioTimeout]); const SessionPageEnter = useCallback(async () => { - logger.debug('sessionUtils: SessionPageEnter'); + logger.debug("sessionUtils: SessionPageEnter"); clearAudioTimeout(); if (jamClient?.SessionPageEnter) { return await jamClient.SessionPageEnter(); @@ -1008,7 +943,7 @@ export default function useSessionModel(app, server, sessionScreen) { }, [jamClient, logger, clearAudioTimeout]); const SessionPageLeave = useCallback(async () => { - logger.debug('sessionUtils: SessionPageLeave'); + logger.debug("sessionUtils: SessionPageLeave"); clearAudioTimeout(); if (jamClient?.SessionPageLeave) { await jamClient.SessionPageLeave(); @@ -1018,23 +953,20 @@ export default function useSessionModel(app, server, sessionScreen) { // Auto-open jam track functionality (from useSessionUtils) const autoOpenJamTrackRef = useRef(null); - const setAutoOpenJamTrack = useCallback( - jamTrack => { - logger.debug('setting auto-load jamtrack', jamTrack); - autoOpenJamTrackRef.current = jamTrack; - }, - [logger] - ); + const setAutoOpenJamTrack = useCallback((jamTrack) => { + logger.debug("setting auto-load jamtrack", jamTrack); + autoOpenJamTrackRef.current = jamTrack; + }, [logger]); const grabAutoOpenJamTrack = useCallback(() => { const jamTrack = autoOpenJamTrackRef.current; autoOpenJamTrackRef.current = null; - logger.debug('grabbing auto-load jamtrack', jamTrack); + logger.debug("grabbing auto-load jamtrack", jamTrack); return jamTrack; }, [logger]); // Latency data structure conversion (from useSessionUtils) - const changeLatencyDataStructure = useCallback(data => { + const changeLatencyDataStructure = useCallback((data) => { return { id: data.user_id, audio_latency: data.audio_latency, @@ -1109,47 +1041,41 @@ export default function useSessionModel(app, server, sessionScreen) { }, []); // Create latency info (from useSessionUtils) - const createLatency = useCallback( - userLatency => { - // Note: In React, we don't have access to currentUserId in the same way - // This would need to be passed as a parameter or from context - const isSameUser = userLatency.id === window.JK?.currentUserId; // Fallback for now - return scoreInfo(userLatency, isSameUser); - }, - [scoreInfo] - ); + const createLatency = useCallback((userLatency) => { + // Note: In React, we don't have access to currentUserId in the same way + // This would need to be passed as a parameter or from context + const isSameUser = userLatency.id === window.JK?.currentUserId; // Fallback for now + return scoreInfo(userLatency, isSameUser); + }, [scoreInfo]); // Join session from custom URL scheme (from useSessionUtils) - const joinSessionFromCustomUrlScheme = useCallback( - hash => { - const qStr = hash.substring(hash.lastIndexOf('/') + 1); - const qParamsArr = qStr.split('|'); - let isCustom = undefined; - let sessionId = undefined; + const joinSessionFromCustomUrlScheme = useCallback((hash) => { + const qStr = hash.substring(hash.lastIndexOf('/') + 1); + const qParamsArr = qStr.split('|'); + let isCustom = undefined; + let sessionId = undefined; - qParamsArr.forEach(q => { - const qp = q.split('~'); - if (qp[0] === 'custom') { - isCustom = qp[1]; - } - if (qp[0] === 'sessionId') { - sessionId = qp[1]; - } - }); - - if (!isCustom || isCustom !== 'yes') { - return; + qParamsArr.forEach((q) => { + const qp = q.split('~'); + if (qp[0] === 'custom') { + isCustom = qp[1]; } - if (!sessionId) { - return; + if (qp[0] === 'sessionId') { + sessionId = qp[1]; } + }); - // Note: joinSession implementation would need to be provided - // For now, just log - logger.debug('Would join session from custom URL:', sessionId); - }, - [logger] - ); + if (!isCustom || isCustom !== 'yes') { + return; + } + if (!sessionId) { + return; + } + + // Note: joinSession implementation would need to be provided + // For now, just log + logger.debug("Would join session from custom URL:", sessionId); + }, [logger]); // Ensure session ended const ensureEnded = useCallback(() => { @@ -1215,7 +1141,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Getters getCurrentSession: () => currentSession, getCurrentOrLastSession: () => currentOrLastSession, - getParticipant: clientId => participantsEverSeen[clientId], + getParticipant: (clientId) => participantsEverSeen[clientId], setBackingTrack: setOpenBackingTrack, getBackingTrack: () => openBackingTrack, hasShownAudioMediaMixerHelp: () => shownAudioMediaMixerHelp, diff --git a/jam-ui/src/store/features/mixersSlice.js b/jam-ui/src/store/features/mixersSlice.js index bc624e7ce..149f5d122 100644 --- a/jam-ui/src/store/features/mixersSlice.js +++ b/jam-ui/src/store/features/mixersSlice.js @@ -1,4 +1,4 @@ -import { createSlice, createSelector } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; const initialState = { // Core mixer collections @@ -313,69 +313,3 @@ export const selectMixerPairByResourceId = (resourceId) => (state) => { export const selectMixerPairByTrackId = (trackId) => (state) => { return state.mixers.mixersByTrackId[trackId]; }; - -// Composed memoized selectors for useMixerHelper optimization -// These group related data that's typically used together - -export const selectCoreMixerState = createSelector( - [selectChatMixer, selectBroadcastMixer, selectRecordingMixer], - (chatMixer, broadcastMixer, recordingMixer) => ({ - chatMixer, - broadcastMixer, - recordingMixer - }) -); - -export const selectTrackMixerState = createSelector( - [ - selectRecordingTrackMixers, - selectBackingTrackMixers, - selectJamTrackMixers, - selectMetronomeTrackMixers, - selectAdhocTrackMixers - ], - (recordingTrackMixers, backingTrackMixers, jamTrackMixers, metronomeTrackMixers, adhocTrackMixers) => ({ - recordingTrackMixers, - backingTrackMixers, - jamTrackMixers, - metronomeTrackMixers, - adhocTrackMixers - }) -); - -export const selectMixerLookupTables = createSelector( - [selectAllMixers, selectMixersByResourceId, selectMixersByTrackId], - (allMixers, mixersByResourceId, mixersByTrackId) => ({ - allMixers, - mixersByResourceId, - mixersByTrackId - }) -); - -export const selectMasterPersonalMixers = createSelector( - [selectMasterMixers, selectPersonalMixers], - (masterMixers, personalMixers) => ({ - masterMixers, - personalMixers - }) -); - -export const selectMixerMetadata = createSelector( - [selectMetronome, selectMetronomeSettings, selectMediaSummary, selectNoAudioUsers, selectClientsWithAudioOverride, selectMixersReady], - (metronome, metronomeSettings, mediaSummary, noAudioUsers, clientsWithAudioOverride, isReady) => ({ - metronome, - metronomeSettings, - mediaSummary, - noAudioUsers, - clientsWithAudioOverride, - isReady - }) -); - -export const selectSimulatedCategoryMixers = createSelector( - [selectSimulatedMusicCategoryMixers, selectSimulatedChatCategoryMixers], - (simulatedMusicCategoryMixers, simulatedChatCategoryMixers) => ({ - simulatedMusicCategoryMixers, - simulatedChatCategoryMixers - }) -);