From de5b331e07c59bbb3ab9e719079a771be05e8334 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 5 Mar 2026 19:36:27 +0530 Subject: [PATCH] feat(32-01): consolidate track sync to single debounced call - Replace triple setTimeout pattern (1s, 1.4s, 6s) with single 1.5s debounced call - Eliminates redundant API calls on session join (STATE-01) - Uses useMemo to create stable debounced function - Debounce delay of 1.5s covers mixer initialization window - Cleanup via cancel() on unmount --- .../src/components/client/JKSessionScreen.js | 981 ++++++++++-------- 1 file changed, 544 insertions(+), 437 deletions(-) diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js index 3478cc953..0996a979b 100644 --- a/jam-ui/src/components/client/JKSessionScreen.js +++ b/jam-ui/src/components/client/JKSessionScreen.js @@ -1,9 +1,10 @@ // 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'; @@ -19,11 +20,32 @@ 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, @@ -54,14 +76,39 @@ 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'; @@ -111,12 +158,13 @@ const JKSessionScreen = () => { guardAgainstInvalidConfiguration, guardAgainstActiveProfileMissing, guardAgainstSinglePlayerProfile, - resyncAudio, + resyncAudio } = useGearUtils(); const { initialize: initializeMixer, onSessionChange } = useMixerStore(); const mixerHelper = useMixersContext(); - const { isConnected, + const { + isConnected, ConnectionStatus, connectionStatus, reconnectAttempts, @@ -124,7 +172,8 @@ const JKSessionScreen = () => { jamClient, server, registerMessageCallback, - unregisterMessageCallback } = useJamServerContext(); + unregisterMessageCallback + } = useJamServerContext(); // Phase 4: Replace CurrentSessionContext with Redux const currentSession = useSelector(selectActiveSession); @@ -354,7 +403,7 @@ const JKSessionScreen = () => { // console.debug("-DEBUG- JKSessionScreen: isConnected changed to true"); guardOnJoinSession(); - }, [isConnected, jamClient]); // Added jamClient to dependencies for stability + }, [guardOnJoinSession, isConnected, jamClient]); // Added jamClient to dependencies for stability const guardOnJoinSession = async () => { // console.log("-DEBUG- JKSessionScreen: guardOnJoinSession") @@ -394,11 +443,10 @@ 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) @@ -407,8 +455,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 @@ -418,7 +466,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 @@ -438,7 +486,6 @@ 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 @@ -446,43 +493,36 @@ const JKSessionScreen = () => { }; useEffect(() => { - if (!sessionGuardsPassed || userTracks.length === 0 || hasJoined) { return } + if (!sessionGuardsPassed || userTracks.length === 0 || hasJoined) { + return; + } joinSession(); - }, [sessionGuardsPassed, userTracks, hasJoined]) + }, [sessionGuardsPassed, userTracks, hasJoined, joinSession]); - // Track sync: Sync tracks to server when session joined (3-call pattern matching legacy) + // 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 // 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 track sync calls'); + // console.log('[Track Sync] Mixers ready, scheduling single debounced sync'); + syncTracksDebounced(sessionId, server.clientId, dispatch); - // 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); - }; + return () => syncTracksDebounced.cancel(); // 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]) + }, [hasJoined, sessionId, mixersReady, dispatch, syncTracksDebounced]); // Fetch chat history when session joins to populate unread badge // This ensures unread count persists across page reloads @@ -491,18 +531,25 @@ const JKSessionScreen = () => { return; } - dispatch(fetchChatHistory({ - channel: sessionId, - sessionId: sessionId - })); + dispatch( + fetchChatHistory({ + channel: sessionId, + sessionId: sessionId + }) + ); }, [hasJoined, sessionId, dispatch]); - const joinSession = async () => { - + const joinSession = useCallback(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); @@ -510,15 +557,13 @@ 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; } @@ -527,13 +572,11 @@ 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, @@ -554,150 +597,147 @@ const JKSessionScreen = () => { session_id: sessionId, client_role: clientRole, parent_client_id: parentClientId, - 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 - } + 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 + } + }); + }); useEffect(() => { if (!isConnected || !hasJoined) return; onSessionChange(sessionHelper); - }, [isConnected, hasJoined, sessionHelper.id()]); + }, [isConnected, hasJoined, onSessionChange, sessionHelper]); - 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 @@ -708,7 +748,7 @@ const JKSessionScreen = () => { const musicianAccess = useMemo(() => { if (!currentSession) return null; return sessionModel.getMusicianAccess(); - }, [currentSession]); + }, [currentSession, sessionModel]); // Memoize chat object to prevent unnecessary re-renders const chat = useMemo(() => { @@ -742,15 +782,21 @@ 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, dispatch]); + }, [ + ConnectionStatus.CONNECTED, + ConnectionStatus.DISCONNECTED, + ConnectionStatus.ERROR, + ConnectionStatus.RECONNECTING, + connectionStatus, + dispatch + ]); // Handlers for recording and playback // const handleStartRecording = () => { @@ -799,11 +845,9 @@ const JKSessionScreen = () => { // // Handle VU meter updates // }; - - useEffect(() => { fetchFriends(); - }, []); + }, [fetchFriends]); const fetchFriends = () => { if (currentUser) { @@ -819,12 +863,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); } @@ -834,7 +878,7 @@ const JKSessionScreen = () => { audioFormat: settings.audioFormat, audioStoreType: settings.audioStoreType, includeChat: settings.includeChat, - volume: settings.volume, + volume: settings.volume }; if (params.recordingType === RECORD_TYPE_BOTH) { @@ -844,25 +888,28 @@ 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; @@ -876,27 +923,29 @@ 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); @@ -959,7 +1008,7 @@ const JKSessionScreen = () => { // Clear Redux session state on unmount dispatch(clearSession()); }; - }, [metronomeState.isOpen, resetMetronome, dispatch]); + }, [metronomeState.isOpen, resetMetronome, dispatch, unregisterMessageCallbacks]); // Check if user can use video (subscription/permission check) const canVideo = () => { @@ -969,7 +1018,7 @@ const JKSessionScreen = () => { }; // Open external link in new window/tab - const openExternalLink = (url) => { + const openExternalLink = url => { window.open(url, '_blank', 'noopener,noreferrer'); }; @@ -991,7 +1040,6 @@ const JKSessionScreen = () => { // 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 @@ -1014,16 +1062,19 @@ 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(); @@ -1054,25 +1105,28 @@ const JKSessionScreen = () => { }; // Handle Resync button click - performs audio resync via native client - const handleResync = useCallback(async (e) => { - e.preventDefault(); - if (resyncLoading) return; + 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')); + 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); } - } finally { - setResyncLoading(false); - } - }, [resyncAudio, resyncLoading]); + }, + [resyncAudio, resyncLoading] + ); // Attach button handlers const handleAttachClick = useCallback(() => { @@ -1081,41 +1135,46 @@ 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 - })); - }, [dispatch, sessionId, server?.clientId, currentUser?.id, currentUser?.name]); + // 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] + ); // Show error toast when upload fails useEffect(() => { @@ -1134,7 +1193,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); @@ -1151,11 +1210,13 @@ 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...'); @@ -1168,7 +1229,7 @@ const JKSessionScreen = () => { } }; - const handleJamTrackSelect = async (jamTrack) => { + const handleJamTrackSelect = async jamTrack => { // console.log('Jam track selected:', jamTrack); try { // Fetch jam track details with stems @@ -1180,21 +1241,25 @@ 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) { @@ -1224,7 +1289,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; } @@ -1246,7 +1311,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; @@ -1317,30 +1382,34 @@ 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...
} - +
- - + - + - + - + Broadcast + dispatch(openModal('jamTrack'))} @@ -1373,31 +1443,44 @@ 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(); }} @@ -1492,14 +1574,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}
@@ -1568,15 +1640,20 @@ const JKSessionScreen = () => { )} {/* Connection Status Badge in Header */} -
+
{connectionStatus === ConnectionStatus.CONNECTED && '🟢 Connected'} {connectionStatus === ConnectionStatus.CONNECTING && '🟡 Connecting...'} @@ -1585,7 +1662,7 @@ const JKSessionScreen = () => { {connectionStatus === ConnectionStatus.ERROR && '🔴 Error'} {reconnectAttempts > 0 && ( - + Attempt {reconnectAttempts}/10 )} @@ -1623,7 +1700,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); @@ -1660,7 +1737,6 @@ const JKSessionScreen = () => { } finally { setSettingsLoading(false); } - }} /> @@ -1672,10 +1748,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() }; @@ -1699,10 +1775,7 @@ 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 + + . +

@@ -1800,32 +1903,36 @@ 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);