1422 lines
55 KiB
JavaScript
1422 lines
55 KiB
JavaScript
// jam-ui/src/components/client/JKSessionScreen.js
|
||
import React, { useEffect, useRef, useState, memo, useMemo, useCallback } from 'react'
|
||
import { useParams, useHistory } from 'react-router-dom';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
|
||
//import useJamServer, { ConnectionStatus } from '../../hooks/useJamServer'
|
||
import useGearUtils from '../../hooks/useGearUtils'
|
||
import useSessionUtils from '../../hooks/useSessionUtils.js';
|
||
import useSessionModel from '../../hooks/useSessionModel.js';
|
||
import useSessionHelper from '../../hooks/useSessionHelper.js';
|
||
import useRecordingHelpers from '../../hooks/useRecordingHelpers.js';
|
||
import useMixerStore from '../../hooks/useMixerStore.js';
|
||
|
||
import { useJamServerContext } from '../../context/JamServerContext.js';
|
||
import { useGlobalContext } from '../../context/GlobalContext.js';
|
||
import { useJamKazamApp } from '../../context/JamKazamAppContext.js';
|
||
import { useMixersContext } from '../../context/MixersContext.js';
|
||
import { useAuth } from '../../context/UserAuth';
|
||
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';
|
||
|
||
// Redux imports
|
||
import { openModal, closeModal, toggleModal, selectModal } from '../../store/features/sessionUISlice';
|
||
import { selectMediaSummary } from '../../store/features/mixersSlice';
|
||
import {
|
||
fetchActiveSession,
|
||
joinActiveSession,
|
||
leaveActiveSession,
|
||
setGuardsPassed,
|
||
setUserTracks,
|
||
setConnectionStatus,
|
||
setSessionId,
|
||
updateSessionData,
|
||
setSelectedJamTrack,
|
||
setJamTrackStems,
|
||
setBackingTrackData,
|
||
clearBackingTrackData,
|
||
setJamTrackData,
|
||
clearJamTrackData,
|
||
clearSession,
|
||
selectActiveSession,
|
||
selectJoinStatus,
|
||
selectHasJoined,
|
||
selectGuardsPassed,
|
||
selectUserTracks,
|
||
selectShowConnectionAlert,
|
||
selectSessionId,
|
||
selectInSession,
|
||
selectSelectedJamTrack,
|
||
selectJamTrackStems,
|
||
selectBackingTrackData,
|
||
selectJamTrackData
|
||
} from '../../store/features/activeSessionSlice';
|
||
|
||
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 FalconCardHeader from '../common/FalconCardHeader';
|
||
import SessionTrackVU from './SessionTrackVU.js';
|
||
import JKSessionMyTrack from './JKSessionMyTrack.js';
|
||
import JKSessionAudioInputs from './JKSessionAudioInputs.js';
|
||
import JKSessionRemoteTracks from './JKSessionRemoteTracks.js';
|
||
import JKSessionSettingsModal from './JKSessionSettingsModal.js';
|
||
import JKSessionInviteModal from './JKSessionInviteModal.js';
|
||
import JKSessionVolumeModal from './JKSessionVolumeModal.js';
|
||
import JKSessionRecordingModal from './JKSessionRecordingModal.js';
|
||
import JKSessionLeaveModal from './JKSessionLeaveModal.js';
|
||
import JKSessionJamTrackModal from './JKSessionJamTrackModal.js';
|
||
import JKSessionJamTrackStems from './JKSessionJamTrackStems.js';
|
||
import JKSessionOpenMenu from './JKSessionOpenMenu.js';
|
||
import WindowPortal from '../common/WindowPortal.js';
|
||
import JKSessionBackingTrackPlayer from './JKSessionBackingTrackPlayer.js';
|
||
import JKSessionJamTrackPlayer from './JKSessionJamTrackPlayer.js';
|
||
import JKSessionBackingTrack from './JKSessionBackingTrack.js';
|
||
import JKPopupMediaControls from '../popups/JKPopupMediaControls.js';
|
||
import { SESSION_PRIVACY_MAP } from '../../helpers/globals.js';
|
||
import { toast } from 'react-toastify';
|
||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||
|
||
//icon imports
|
||
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';
|
||
|
||
const JKSessionScreen = () => {
|
||
const logger = console; // Replace with another logging mechanism if needed
|
||
const dispatch = useDispatch();
|
||
const app = useJamKazamApp();
|
||
const { currentUser } = useAuth();
|
||
|
||
const {
|
||
guardAgainstInvalidConfiguration,
|
||
guardAgainstActiveProfileMissing,
|
||
guardAgainstSinglePlayerProfile,
|
||
} = useGearUtils();
|
||
|
||
const { initialize: initializeMixer, onSessionChange } = useMixerStore();
|
||
const mixerHelper = useMixersContext();
|
||
const { isConnected,
|
||
ConnectionStatus,
|
||
connectionStatus,
|
||
reconnectAttempts,
|
||
lastError,
|
||
jamClient,
|
||
server,
|
||
registerMessageCallback,
|
||
unregisterMessageCallback } = useJamServerContext();
|
||
|
||
// Phase 4: Replace CurrentSessionContext with Redux
|
||
const currentSession = useSelector(selectActiveSession);
|
||
const sessionId = useSelector(selectSessionId);
|
||
const inSessionFlag = useSelector(selectInSession);
|
||
|
||
// Create ref for line 225, 365 compatibility
|
||
const currentSessionIdRef = useRef(sessionId);
|
||
useEffect(() => {
|
||
currentSessionIdRef.current = sessionId;
|
||
}, [sessionId]);
|
||
|
||
const inSession = useCallback(() => inSessionFlag, [inSessionFlag]);
|
||
|
||
const { globalObject, metronomeState, closeMetronome, resetMetronome } = useGlobalContext();
|
||
const { getCurrentRecordingState, reset: resetRecordingState, currentlyRecording } = useRecordingHelpers();
|
||
const { SessionPageEnter } = useSessionUtils();
|
||
|
||
// Redux media state and actions
|
||
const mediaSummary = useSelector(selectMediaSummary);
|
||
const { openBackingTrack, openMetronome, loadJamTrack, closeMedia } = useMediaActions();
|
||
|
||
// Use the session model hook
|
||
const sessionModel = useSessionModel(app, server, null); // sessionScreen is null for now
|
||
const sessionHelper = useSessionHelper();
|
||
const { id: sessionIdFromUrl } = useParams(); // Renamed to avoid conflict with Redux sessionId
|
||
const history = useHistory();
|
||
|
||
// Redux session lifecycle state
|
||
const activeSession = useSelector(selectActiveSession);
|
||
const joinStatus = useSelector(selectJoinStatus);
|
||
const hasJoined = useSelector(selectHasJoined);
|
||
const sessionGuardsPassed = useSelector(selectGuardsPassed);
|
||
const userTracks = useSelector(selectUserTracks);
|
||
const showConnectionAlert = useSelector(selectShowConnectionAlert);
|
||
const reduxSessionId = useSelector(selectSessionId);
|
||
|
||
// Non-lifecycle state (keeping as local for now)
|
||
const [requestingSessionRefresh, setRequestingSessionRefresh] = useState(false);
|
||
const [pendingSessionRefresh, setPendingSessionRefresh] = useState(false);
|
||
|
||
// Redux modal state
|
||
const showSettingsModal = useSelector(selectModal('settings'));
|
||
const showInviteModal = useSelector(selectModal('invite'));
|
||
const showVolumeModal = useSelector(selectModal('volume'));
|
||
const showRecordingModal = useSelector(selectModal('recording'));
|
||
const showLeaveModal = useSelector(selectModal('leave'));
|
||
const showJamTrackModal = useSelector(selectModal('jamTrack'));
|
||
const showBackingTrackPopup = useSelector(selectModal('backingTrack'));
|
||
const showMediaControlsPopup = useSelector(selectModal('mediaControls'));
|
||
|
||
// Non-modal state for settings modal
|
||
const [settingsLoading, setSettingsLoading] = useState(false);
|
||
|
||
// Non-modal state for invite modal
|
||
const [friends, setFriends] = useState([]);
|
||
const [sessionInvitees, setSessionInvitees] = useState([]);
|
||
const [inviteLoading, setInviteLoading] = useState(false);
|
||
|
||
// Non-modal state for volume modal
|
||
const [volumeLevel, setVolumeLevel] = useState(100);
|
||
|
||
// Non-modal state for leave modal
|
||
const [leaveRating, setLeaveRating] = useState(null); // null, 'thumbsUp', 'thumbsDown'
|
||
const [leaveComments, setLeaveComments] = useState('');
|
||
const [leaveLoading, setLeaveLoading] = useState(false);
|
||
|
||
//state for video button
|
||
const [videoLoading, setVideoLoading] = useState(false);
|
||
|
||
// Redux backing track state (modal visibility and data)
|
||
const backingTrackData = useSelector(selectBackingTrackData);
|
||
const showBackingTrackPlayer = Boolean(backingTrackData);
|
||
|
||
// Stable callback for backing track popup close
|
||
const handleBackingTrackClose = useCallback(() => {
|
||
console.log('JKSessionScreen: Backing Track Popup closing');
|
||
dispatch(closeModal('backingTrack'));
|
||
dispatch(clearBackingTrackData());
|
||
}, [dispatch]);
|
||
|
||
// Stable callback for backing track close in main screen
|
||
const handleBackingTrackMainClose = useCallback(async () => {
|
||
console.log('JKSessionScreen: Backing Track main screen close requested');
|
||
await closeMedia();
|
||
// Also clear the backing track popup data
|
||
dispatch(clearBackingTrackData());
|
||
dispatch(closeModal('backingTrack'));
|
||
}, [closeMedia, dispatch]);
|
||
|
||
// Redux JamTrack player state (modal visibility and data)
|
||
const jamTrackData = useSelector(selectJamTrackData);
|
||
const showJamTrackPlayer = Boolean(jamTrackData);
|
||
|
||
// Redux jam track state
|
||
const selectedJamTrack = useSelector(selectSelectedJamTrack);
|
||
const jamTrackStems = useSelector(selectJamTrackStems);
|
||
const jamTrackDownloadState = useSelector(state => state.media.downloadState);
|
||
|
||
// JamTrack close handler (used by both player and session screen)
|
||
const handleJamTrackClose = useCallback(async () => {
|
||
console.log('Closing jam track');
|
||
try {
|
||
// Call the close jam track API
|
||
await closeJamTrack({ id: currentSession.id });
|
||
|
||
// Clear the selected jam track and stems (session screen)
|
||
dispatch(setSelectedJamTrack(null));
|
||
dispatch(setJamTrackStems([]));
|
||
|
||
// Clear the player popup data
|
||
dispatch(clearJamTrackData());
|
||
|
||
toast.success('JamTrack closed successfully');
|
||
} catch (error) {
|
||
console.error('Error closing jam track:', error);
|
||
toast.error('Failed to close JamTrack');
|
||
}
|
||
}, [currentSession, dispatch]);
|
||
|
||
// Stable callback for JamTrack player popup close (WindowPortal X button or ESC)
|
||
const handleJamTrackPlayerClose = useCallback(async () => {
|
||
console.log('JKSessionScreen: JamTrack Player Popup closing');
|
||
// Call the full close handler to clean up everything
|
||
await handleJamTrackClose();
|
||
}, [handleJamTrackClose]);
|
||
|
||
// State for media controls popup (modal visibility now in Redux)
|
||
const [mediaControlsOpened, setMediaControlsOpened] = useState(false);
|
||
const [popupGuard, setPopupGuard] = useState(false); // Hard guard against infinite loops
|
||
|
||
// Store references to registered message callbacks for cleanup
|
||
const [registeredCallbacks, setRegisteredCallbacks] = useState([]);
|
||
|
||
// Function to unregister message callbacks
|
||
const unregisterMessageCallbacks = useCallback(() => {
|
||
registeredCallbacks.forEach(({ type, callback }) => {
|
||
unregisterMessageCallback(type, callback);
|
||
});
|
||
setRegisteredCallbacks([]);
|
||
}, [registeredCallbacks, unregisterMessageCallback]);
|
||
|
||
// Fetch session data when URL sessionId changes
|
||
useEffect(() => {
|
||
if (sessionIdFromUrl && sessionIdFromUrl !== sessionId) {
|
||
console.log('Fetching active session:', sessionIdFromUrl);
|
||
dispatch(fetchActiveSession(sessionIdFromUrl));
|
||
}
|
||
}, [sessionIdFromUrl, sessionId, dispatch]);
|
||
|
||
useEffect(() => {
|
||
if (!isConnected || !jamClient) return;
|
||
console.debug("JKSessionScreen: -DEBUG- isConnected changed to true");
|
||
|
||
guardOnJoinSession();
|
||
}, [isConnected, jamClient]); // Added jamClient to dependencies for stability
|
||
|
||
const guardOnJoinSession = async () => {
|
||
try {
|
||
const musicSessionResp = await getSessionHistory(sessionIdFromUrl);
|
||
const musicSession = await musicSessionResp.json();
|
||
logger.log("fetched session history: ", musicSession);
|
||
dispatch(setSessionId(musicSession.id)); // Phase 4: dispatch to Redux
|
||
|
||
const musicianAccessOnJoin = musicSession.musician_access;
|
||
const shouldVerifyNetwork = musicSession.musician_access;
|
||
const clientRole = await jamClient.getClientParentChildRole();
|
||
|
||
logger.log("musicianAccessOnJoin when joining session: " + musicianAccessOnJoin);
|
||
logger.log("clientRole when joining session: " + clientRole);
|
||
logger.log("currentSessionId when joining session: " + currentSessionIdRef.current);
|
||
|
||
if (clientRole === CLIENT_ROLE.CHILD) {
|
||
logger.debug("client is configured to act as child. skipping all checks. assuming 0 tracks");
|
||
dispatch(setUserTracks([]));
|
||
|
||
//skipping all checks. assuming 0 tracks
|
||
await joinSession();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await guardAgainstInvalidConfiguration(app, shouldVerifyNetwork);
|
||
const result = await SessionPageEnter();
|
||
logger.log("SessionPageEnter result: ", result);
|
||
|
||
try {
|
||
await guardAgainstActiveProfileMissing(app, result);
|
||
logger.log("user has an active profile");
|
||
try {
|
||
const tracks = await sessionModel.waitForSessionPageEnterDone();
|
||
dispatch(setUserTracks(tracks));
|
||
logger.log("userTracks: ", tracks);
|
||
try {
|
||
await ensureAppropriateProfile(musicianAccessOnJoin)
|
||
logger.log("user has passed all session guards")
|
||
|
||
dispatch(setGuardsPassed(true))
|
||
|
||
} catch (error) {
|
||
logger.error("User profile is not appropriate for session:", error);
|
||
if (!error.controlled_location) {
|
||
}
|
||
}
|
||
} catch (error) {
|
||
logger.error("Error: waiting for session page enter to complete:", error);
|
||
if (error === "timeout") {
|
||
//TODO: show some error
|
||
} else if (error === 'session_over') {
|
||
// do nothing; session ended before we got the user track info. just bail
|
||
logger.debug("Error:: session is over; bailing");
|
||
} else {
|
||
//TODO: show some error
|
||
}
|
||
|
||
await sessionModel.handleLeaveSession(); //TODO: handle redirection
|
||
}
|
||
} catch (error) {
|
||
// Active profile is missing, redirect to home or if the error has a location, redirect there
|
||
logger.error("Error: Active profile is missing or invalid:", error);
|
||
}
|
||
} catch (error) {
|
||
// Invalid configuration, redirect to home
|
||
await sessionModel.handleLeaveSession(); //TODO: handle redirection
|
||
logger.error("Error: Invalid configuration:", error);
|
||
}
|
||
|
||
} catch (error) {
|
||
logger.error("Error: Error fetching session history:", error);
|
||
//TODO: Show some error
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!sessionGuardsPassed || userTracks.length === 0 || hasJoined) { return }
|
||
joinSession();
|
||
}, [sessionGuardsPassed, userTracks, hasJoined])
|
||
|
||
const joinSession = async () => {
|
||
|
||
await jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2");
|
||
await jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
|
||
|
||
await jamClient.SessionSetConnectionStatusRefreshRate(1000);
|
||
|
||
let clientRole = await jamClient.getClientParentChildRole();
|
||
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) {
|
||
clientRole = null;
|
||
}
|
||
|
||
// subscribe to events from the recording model
|
||
//this.recordingRegistration(); //TODO: implement recording registration
|
||
|
||
// tell the server we want to join
|
||
|
||
|
||
|
||
//const clientId = await jamClient.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
|
||
|
||
console.log("joinSession parameters: ", {
|
||
client_id: clientId,
|
||
ip_address: server.publicIP,
|
||
as_musician: true,
|
||
tracks: userTracks,
|
||
session_id: sessionId,
|
||
client_role: clientRole,
|
||
parent_client_id: parentClientId,
|
||
audio_latency: latency,
|
||
});
|
||
|
||
joinSessionRest({
|
||
client_id: clientId,
|
||
ip_address: server.publicIP,
|
||
as_musician: true,
|
||
tracks: userTracks,
|
||
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);
|
||
|
||
// 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 }
|
||
];
|
||
|
||
callbacksToRegister.forEach(({ type, callback }) => {
|
||
registerMessageCallback(type, callback);
|
||
});
|
||
|
||
// Store registered callbacks for cleanup
|
||
setRegisteredCallbacks(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]);
|
||
|
||
const ensureAppropriateProfile = async (musicianAccess) => {
|
||
return new Promise(async function (resolve, reject) {
|
||
if (musicianAccess) {
|
||
try {
|
||
await guardAgainstSinglePlayerProfile(app);
|
||
resolve();
|
||
} catch (error) {
|
||
reject(error)
|
||
}
|
||
} else {
|
||
resolve();
|
||
}
|
||
})
|
||
};
|
||
|
||
// Use sessionModel functions
|
||
const trackChanges = sessionModel.trackChanges;
|
||
const refreshCurrentSession = sessionModel.refreshCurrentSession;
|
||
const updateSession = sessionModel.updateSession;
|
||
|
||
const musicianAccess = useMemo(() => {
|
||
if (!currentSession) return null;
|
||
return sessionModel.getMusicianAccess();
|
||
}, [currentSession]);
|
||
|
||
// Memoize chat object to prevent unnecessary re-renders
|
||
const chat = useMemo(() => {
|
||
if (!mixerHelper.chatMixer) return null;
|
||
return {
|
||
mixers: mixerHelper.chatMixer,
|
||
mode: mixerHelper.mixMode,
|
||
photoUrl: mixerHelper.myTracks.length > 0 ? mixerHelper.myTracks[0].photoUrl : '',
|
||
name: 'Chat'
|
||
};
|
||
}, [mixerHelper.chatMixer, mixerHelper.mixMode, mixerHelper.myTracks]);
|
||
|
||
// useEffect(() => {
|
||
// if (!isConnected) return;
|
||
// // validate session by fetching the session from the server
|
||
// fetchSession();
|
||
// }, [isConnected]);
|
||
|
||
// const fetchSession = async () => {
|
||
// const session = await getSession(sessionId);
|
||
// if (session) {
|
||
// setSessionState(prevState => ({
|
||
// ...prevState,
|
||
// ...session
|
||
// }));
|
||
// } else {
|
||
// logger.error("Invalid session ID or unable to fetch session");
|
||
// //TODO: Handle invalid session (e.g., redirect or show error)
|
||
// }
|
||
// };
|
||
|
||
// Monitor connection status changes
|
||
useEffect(() => {
|
||
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]);
|
||
|
||
// Handlers for recording and playback
|
||
// const handleStartRecording = () => {
|
||
// jamClient.StartRecording({ recordingId: `rec_${Date.now()}` });
|
||
// };
|
||
|
||
// const handleStopRecording = () => {
|
||
// jamClient.StopRecording({});
|
||
// };
|
||
|
||
// const handlePlayPause = () => {
|
||
// if (isPlaying) {
|
||
// jamClient.SessionPausePlay();
|
||
// } else {
|
||
// jamClient.SessionStartPlay();
|
||
// }
|
||
// };
|
||
|
||
// const handleStopPlayback = () => {
|
||
// jamClient.SessionStopPlay();
|
||
// };
|
||
|
||
// // Callback handlers (these would be implemented to handle WebSocket responses)
|
||
// const HandleSessionCallback = (data) => {
|
||
// logger.log('Session callback:', data);
|
||
// // Handle session events
|
||
// };
|
||
|
||
// const HandleRecordingStarted = (data) => {
|
||
// logger.log('Recording started:', data);
|
||
// // Update recording state
|
||
// };
|
||
|
||
// const HandleRecordingStopped = (data) => {
|
||
// logger.log('Recording stopped:', data);
|
||
// // Update recording state
|
||
// };
|
||
|
||
// const HandleVolumeChangeCallback = (mixerId, isLeft, value, isMuted) => {
|
||
// logger.log('Volume changed:', { mixerId, isLeft, value, isMuted });
|
||
// // Update mixer state
|
||
// };
|
||
|
||
// const HandleBridgeCallback = (vuData) => {
|
||
// logger.log('Bridge callback:', vuData);
|
||
// // Handle VU meter updates
|
||
// };
|
||
|
||
|
||
|
||
useEffect(() => {
|
||
fetchFriends();
|
||
}, []);
|
||
|
||
const fetchFriends = () => {
|
||
if (currentUser) {
|
||
getFriends(currentUser.id)
|
||
.then(resp => {
|
||
if (resp.ok) {
|
||
return resp.json();
|
||
}
|
||
})
|
||
.then(data => {
|
||
setFriends(data);
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleRecordingSubmit = async (settings) => {
|
||
settings.volume = getCurrentRecordingState().inputVolumeLevel;
|
||
try {
|
||
localStorage.setItem("recordSettings", JSON.stringify(settings));
|
||
} catch (e) {
|
||
logger.info("error while saving recordSettings to localStorage");
|
||
logger.log(e.stack);
|
||
}
|
||
|
||
const params = {
|
||
recordingType: settings.recordingType,
|
||
name: settings.recordingName,
|
||
audioFormat: settings.audioFormat,
|
||
audioStoreType: settings.audioStoreType,
|
||
includeChat: settings.includeChat,
|
||
volume: settings.volume,
|
||
};
|
||
|
||
if (params.recordingType === RECORD_TYPE_BOTH) {
|
||
params['videoFormat'] = settings.videoFormat;
|
||
params['audioDelay'] = settings.audioDelay;
|
||
|
||
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.");
|
||
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.");
|
||
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"];
|
||
for (let i = 0; i < recordingTracks.length; i++) {
|
||
let clientId = recordingTracks[i].client_id;
|
||
|
||
let tracksForClient = groupedTracks[clientId];
|
||
if (!tracksForClient) {
|
||
tracksForClient = [];
|
||
groupedTracks[clientId] = tracksForClient;
|
||
}
|
||
tracksForClient.push(recordingTracks[i]);
|
||
}
|
||
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 handleLeaveSession = () => {
|
||
// Just show the modal - no leave operations yet
|
||
dispatch(openModal('leave'));
|
||
};
|
||
|
||
const handleLeaveSubmit = async (feedbackData) => {
|
||
try {
|
||
setLeaveLoading(true);
|
||
|
||
// Unregister message callbacks before leaving
|
||
unregisterMessageCallbacks();
|
||
|
||
// Close metronome if open before leaving
|
||
if (metronomeState.isOpen) {
|
||
console.log('Closing metronome before leaving session');
|
||
closeMetronome();
|
||
}
|
||
|
||
// Submit feedback to backend first
|
||
const clientId = server.clientId;
|
||
const backendDetails = jamClient.getAllClientsStateMap ? jamClient.getAllClientsStateMap() : {};
|
||
|
||
await submitSessionFeedback(clientId, {
|
||
rating: feedbackData.rating,
|
||
comment: feedbackData.comments,
|
||
backend_details: backendDetails
|
||
});
|
||
|
||
// Then perform leave operations
|
||
await sessionModel.handleLeaveSession();
|
||
|
||
// Clear Redux session state
|
||
dispatch(clearSession());
|
||
|
||
dispatch(closeModal('leave'));
|
||
toast.success('Thank you for your feedback!');
|
||
|
||
// Navigate to sessions page using React Router
|
||
history.push('/sessions');
|
||
} catch (error) {
|
||
console.error('Error submitting feedback or leaving session:', error);
|
||
toast.error('Failed to submit feedback or leave session');
|
||
} finally {
|
||
setLeaveLoading(false);
|
||
}
|
||
};
|
||
|
||
// Cleanup metronome state and message callbacks when component unmounts
|
||
useEffect(() => {
|
||
return () => {
|
||
// Unregister message callbacks
|
||
unregisterMessageCallbacks();
|
||
|
||
// Reset metronome state when component unmounts (session ends)
|
||
if (metronomeState.isOpen) {
|
||
console.log('Resetting metronome state on session cleanup');
|
||
resetMetronome();
|
||
}
|
||
|
||
// Clear Redux session state on unmount
|
||
dispatch(clearSession());
|
||
};
|
||
}, [metronomeState.isOpen, resetMetronome, dispatch]);
|
||
|
||
// Check if user can use video (subscription/permission check)
|
||
const canVideo = () => {
|
||
// This would need to be implemented based on user subscription logic
|
||
console.debug("JKSessionScreen: Checking video permission for user", currentSession);
|
||
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
|
||
// This could use a modal context or toast notification
|
||
console.log('Show video upgrade prompt');
|
||
toast.warning(<VideoUpgradeContent />, { autoClose: false });
|
||
};
|
||
|
||
const VideoUpgradeContent = () => (
|
||
<div>
|
||
<h5>Upgrade Required</h5>
|
||
<p>The video feature requires a premium subscription. Please upgrade your plan to access video conferencing.</p>
|
||
<Button color="primary" onClick={() => {
|
||
// Redirect to upgrade page
|
||
history.push('/upgrade');
|
||
}}>
|
||
Upgrade Now
|
||
</Button>
|
||
</div>
|
||
);
|
||
|
||
const handleBroadcast = async (e) => {
|
||
e.preventDefault();
|
||
try {
|
||
await jamClient.LaunchBroadcastSettings();
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
const handleBackingTrackSelected = async (result) => {
|
||
console.log('JKSessionScreen: handleBackingTrackSelected called with:', result);
|
||
console.log('JKSessionScreen: Current state - showBackingTrackPopup:', showBackingTrackPopup, 'popupGuard:', popupGuard);
|
||
|
||
try {
|
||
console.log('JKSessionScreen: Calling jamClient.SessionOpenBackingTrackFile...');
|
||
// Open the backing track file
|
||
await jamClient.SessionOpenBackingTrackFile(result.file, false);
|
||
console.log('JKSessionScreen: jamClient.SessionOpenBackingTrackFile completed');
|
||
|
||
// 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
|
||
}));
|
||
|
||
// Show the popup
|
||
console.log('JKSessionScreen: Setting showBackingTrackPopup to true...');
|
||
dispatch(openModal('backingTrack'));
|
||
console.log('JKSessionScreen: handleBackingTrackSelected completed successfully');
|
||
//TODO: In the legacy client, the popup window was opened as a native window through the client. decide whether we need to replicate that behavior here or do it through the browser only
|
||
} catch (error) {
|
||
console.error('JKSessionScreen: Error opening backing track:', error);
|
||
toast.error('Failed to open backing track');
|
||
}
|
||
};
|
||
|
||
const handleJamTrackSelect = async (jamTrack) => {
|
||
console.log('Jam track selected:', jamTrack);
|
||
try {
|
||
// Fetch jam track details with stems
|
||
const response = await getJamTrack({ id: jamTrack.id });
|
||
const jamTrackWithStems = await response.json();
|
||
|
||
console.log('Jam track data:', jamTrackWithStems);
|
||
|
||
// 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
|
||
}));
|
||
|
||
toast.success(`Loaded JamTrack: ${jamTrackWithStems.name}`);
|
||
} catch (error) {
|
||
console.error('Error loading jam track:', error);
|
||
toast.error('Failed to load JamTrack');
|
||
}
|
||
};
|
||
|
||
const handleMetronomeSelected = async () => {
|
||
console.log('Opening metronome');
|
||
try {
|
||
// Check if currently recording - can't open metronome while recording
|
||
if (currentlyRecording) {
|
||
toast.warning("You can't open a metronome while recording.");
|
||
return;
|
||
}
|
||
|
||
// 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(", ");
|
||
toast.warning(`Couldn't open metronome due to unstable clocks: ${names}`);
|
||
return;
|
||
}
|
||
|
||
// Track analytics (like legacy SessionStore)
|
||
if (window.stats && window.stats.write) {
|
||
const data = {
|
||
value: 1,
|
||
session_size: currentSession.participants?.length || 1,
|
||
user_id: currentUser?.id,
|
||
user_name: currentUser?.name
|
||
};
|
||
window.stats.write('web.metronome.open', data);
|
||
}
|
||
|
||
// Stop any current playback first (like legacy MixerStore)
|
||
await jamClient.SessionStopPlay();
|
||
|
||
// Open the metronome with default settings
|
||
const bpm = 120;
|
||
const sound = "Beep";
|
||
const meter = 1;
|
||
const mode = 0;
|
||
|
||
console.log(`Opening metronome with bpm: ${bpm}, sound: ${sound}, meter: ${meter}, mode: ${mode}`);
|
||
|
||
// Inform server about metronome opening (like legacy SessionStore)
|
||
await openMetronome({ id: currentSession.id });
|
||
|
||
// Start the metronome audio (backend will handle GUI via callback)
|
||
|
||
//alert('About to start metronome');
|
||
const result = await jamClient.SessionOpenMetronome(bpm, sound, meter, mode);
|
||
//alert('Metronome is started ' + JSON.stringify(result));
|
||
|
||
toast.success('Metronome opened successfully');
|
||
} catch (error) {
|
||
console.error('Error opening metronome:', error);
|
||
toast.error('Failed to open metronome');
|
||
}
|
||
};
|
||
|
||
const checkUnstableClocks = async () => {
|
||
try {
|
||
const unstable = [];
|
||
|
||
// Check current user's NTP stability
|
||
const myState = await jamClient.getMyNetworkState();
|
||
if (!myState.ntp_stable) {
|
||
unstable.push('this computer');
|
||
}
|
||
|
||
// Check other participants' NTP stability
|
||
if (currentSession.participants) {
|
||
for (const participant of currentSession.participants) {
|
||
if (participant.client_id !== server.clientId) {
|
||
try {
|
||
const peerState = await jamClient.getPeerState(participant.client_id);
|
||
if (!peerState.ntp_stable) {
|
||
unstable.push(participant.user.first_name + ' ' + participant.user.last_name);
|
||
}
|
||
} catch (error) {
|
||
// Ignore errors for individual peer checks
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return unstable;
|
||
} catch (error) {
|
||
console.error('Error checking NTP stability:', error);
|
||
return [];
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Card>
|
||
{!isConnected && <div className='d-flex align-items-center'>Connecting to backend...</div>}
|
||
<FalconCardHeader title="Session" titleClass="font-weight-bold">
|
||
<Button color="primary" size="md" onClick={handleLeaveSession}>Leave Session</Button>
|
||
</FalconCardHeader>
|
||
|
||
<CardHeader className="bg-light border-bottom border-top py-2 border-3">
|
||
<div className="d-flex flex-nowrap overflow-auto" style={{ gap: '0.5rem', zIndex: 1100 }}>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={() => dispatch(openModal('settings'))}>
|
||
<img src={gearIcon} alt="Settings" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Settings</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={() => dispatch(openModal('invite'))}>
|
||
<img src={inviteIcon} alt="Invite" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Invite</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={() => dispatch(openModal('volume'))}>
|
||
<img src={volumeIcon} alt="Volume" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Volume</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={handleVideoClick} disabled={videoLoading}>
|
||
<img src={videoIcon} alt="Video" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
{videoLoading && (<Spinner size="sm" />)}
|
||
Video
|
||
</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={() => dispatch(openModal('recording'))}>
|
||
<img src={recordIcon} alt="Record" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Record</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={ handleBroadcast}>
|
||
<img src={broadcastIcon} alt="Broadcast" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Broadcast</Button>
|
||
<JKSessionOpenMenu onBackingTrackSelected={handleBackingTrackSelected} onJamTrackSelected={() => dispatch(openModal('jamTrack'))} onMetronomeSelected={handleMetronomeSelected} />
|
||
<Button className='btn-custom-outline' outline size="md">
|
||
<img src={chatIcon} alt="Chat" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Chat</Button>
|
||
<Button className='btn-custom-outline' outline size="md">
|
||
<img src={attachIcon} alt="Attach" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Attach</Button>
|
||
<Button className='btn-custom-outline' outline size="md">
|
||
<img src={resyncIcon} alt="Resync" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Resync</Button>
|
||
</div>
|
||
</CardHeader>
|
||
|
||
<CardBody className="pl-4" style={{ backgroundColor: '#edf2f9f5', overflowX: 'auto', width: '100%' }}>
|
||
<div className='d-flex' style={{ gap: '1rem' }}>
|
||
<div className='audioInputs'>
|
||
<h5>Audio Inputs <FontAwesomeIcon icon="question-circle" id="audioInputsTooltip" className="ml-2" style={{ cursor: 'pointer' }} /></h5>
|
||
<div style={{ borderRight: '1px #ddd solid', paddingRight: '1rem' }}>
|
||
<JKSessionAudioInputs
|
||
myTracks={mixerHelper.myTracks}
|
||
chat={chat}
|
||
mixerHelper={mixerHelper}
|
||
isRemote={false}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className='sessionMix'>
|
||
<h5>Session Mix <FontAwesomeIcon icon="question-circle" id="sessionMixTooltip" className="ml-2" style={{ cursor: 'pointer' }} /></h5>
|
||
<div className='d-flex' style={{ gap: '1rem' }}>
|
||
<JKSessionAudioInputs
|
||
myTracks={mixerHelper.myTracks}
|
||
chat={chat}
|
||
mixerHelper={mixerHelper}
|
||
isRemote={true}
|
||
/>
|
||
<JKSessionRemoteTracks
|
||
mixerHelper={mixerHelper}
|
||
sessionModel={sessionModel}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* JamTrack Section - Show stems when player is ready */}
|
||
{selectedJamTrack && jamTrackStems.length > 0 &&
|
||
(jamTrackDownloadState.state === 'synchronized' || jamTrackDownloadState.state === 'idle') && (
|
||
<>
|
||
<div style={{ borderLeft: '1px solid #ddd', paddingLeft: '1rem' }}></div>
|
||
<div className='jamTrack'>
|
||
<h5>
|
||
JamTrack: {selectedJamTrack.name}
|
||
<a
|
||
href="#"
|
||
className="text-muted ml-2"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
handleJamTrackClose();
|
||
}}
|
||
style={{ fontSize: '1.2em', textDecoration: 'none' }}
|
||
title="Close JamTrack"
|
||
>
|
||
<FontAwesomeIcon icon="times" /> Close
|
||
</a>
|
||
</h5>
|
||
<JKSessionJamTrackStems
|
||
jamTrackStems={jamTrackStems}
|
||
mixerHelper={mixerHelper}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Backing Track Section - Show track when player is open */}
|
||
{showBackingTrackPlayer && mixerHelper.backingTracks && mixerHelper.backingTracks.length > 0 && (
|
||
<>
|
||
<div style={{ borderLeft: '1px solid #ddd', paddingLeft: '1rem' }}></div>
|
||
<div className='backingTrack'>
|
||
<h5>
|
||
Backing Track: {mixerHelper.backingTracks[0].shortFilename || 'Audio File'}
|
||
<a
|
||
href="#"
|
||
className="text-muted ml-2"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
handleBackingTrackMainClose();
|
||
}}
|
||
style={{ fontSize: '1.2em', textDecoration: 'none' }}
|
||
title="Close Backing Track"
|
||
>
|
||
<FontAwesomeIcon icon="times" /> Close
|
||
</a>
|
||
</h5>
|
||
<JKSessionBackingTrack
|
||
backingTrack={mixerHelper.backingTracks[0]}
|
||
mixers={mixerHelper.backingTracks[0].mixers}
|
||
onClose={handleBackingTrackMainClose}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
</div>
|
||
|
||
{/* Connection Status Alerts */}
|
||
{showConnectionAlert && (
|
||
<div className='mt-4'>
|
||
<Alert color={
|
||
connectionStatus === ConnectionStatus.DISCONNECTED ? 'warning' :
|
||
connectionStatus === ConnectionStatus.RECONNECTING ? 'info' :
|
||
connectionStatus === ConnectionStatus.ERROR ? 'danger' : 'success'
|
||
}>
|
||
<div className='d-flex align-items-center'>
|
||
<div className='me-2'>
|
||
{connectionStatus === ConnectionStatus.DISCONNECTED && '⚠️'}
|
||
{connectionStatus === ConnectionStatus.RECONNECTING && '🔄'}
|
||
{connectionStatus === ConnectionStatus.ERROR && '❌'}
|
||
{connectionStatus === ConnectionStatus.CONNECTED && '✅'}
|
||
</div>
|
||
<div>
|
||
{connectionStatus === ConnectionStatus.DISCONNECTED && (
|
||
<strong>Connection Lost</strong>
|
||
)}
|
||
{connectionStatus === ConnectionStatus.RECONNECTING && (
|
||
<div>
|
||
<strong>Reconnecting...</strong>
|
||
{reconnectAttempts > 0 && (
|
||
<div className='small mt-1'>
|
||
Attempt {reconnectAttempts} of 10
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{connectionStatus === ConnectionStatus.ERROR && (
|
||
<div>
|
||
<strong>Connection Failed</strong>
|
||
{lastError && (
|
||
<div className='small mt-1'>
|
||
{lastError.message || 'Unknown error'}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{connectionStatus === ConnectionStatus.CONNECTED && (
|
||
<strong>Reconnected Successfully</strong>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Alert>
|
||
</div>
|
||
)}
|
||
|
||
{/* Connection Status Badge in Header */}
|
||
<div className='mt-3 mb-3'>
|
||
<Badge
|
||
color={
|
||
connectionStatus === ConnectionStatus.CONNECTED ? 'success' :
|
||
connectionStatus === ConnectionStatus.CONNECTING ? 'warning' :
|
||
connectionStatus === ConnectionStatus.RECONNECTING ? 'info' :
|
||
connectionStatus === ConnectionStatus.ERROR ? 'danger' : 'secondary'
|
||
}
|
||
className='me-2'
|
||
>
|
||
{connectionStatus === ConnectionStatus.CONNECTED && '🟢 Connected'}
|
||
{connectionStatus === ConnectionStatus.CONNECTING && '🟡 Connecting...'}
|
||
{connectionStatus === ConnectionStatus.RECONNECTING && '🔄 Reconnecting...'}
|
||
{connectionStatus === ConnectionStatus.DISCONNECTED && '🟠 Disconnected'}
|
||
{connectionStatus === ConnectionStatus.ERROR && '🔴 Error'}
|
||
</Badge>
|
||
{reconnectAttempts > 0 && (
|
||
<Badge color='info' className='me-2'>
|
||
Attempt {reconnectAttempts}/10
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
{/* Debug Info */}
|
||
{/* <div className='mt-4 p-3 bg-white rounded'>
|
||
<h6>Debug Information</h6>
|
||
<div className='row'>
|
||
<div className='col-md-3'>
|
||
<strong>Connection:</strong> {isConnected ? '✅ Connected' : '❌ Disconnected'}
|
||
</div>
|
||
<div className='col-md-3'>
|
||
<strong>Status:</strong> {connectionStatus}
|
||
</div>
|
||
</div>
|
||
<div className='row mt-2'>
|
||
<div className='col-md-3'>
|
||
<strong>Reconnect Attempts:</strong> {reconnectAttempts}
|
||
</div>
|
||
<div className='col-md-6'>
|
||
<strong>Last Error:</strong> {lastError ? lastError.message : 'None'}
|
||
</div>
|
||
</div>
|
||
<div className='row mt-2'>
|
||
<div className='col-md-12'>
|
||
<strong>Current Session:</strong> {JSON.stringify(currentSession)}
|
||
</div>
|
||
</div>
|
||
</div> */}
|
||
</CardBody>
|
||
|
||
<JKSessionSettingsModal
|
||
isOpen={showSettingsModal}
|
||
toggle={() => dispatch(toggleModal('settings'))}
|
||
currentSession={{ ...currentSession, privacy: musicianAccess }}
|
||
loading={settingsLoading}
|
||
onSave={async (payload) => {
|
||
console.log('Session settings :', payload);
|
||
try {
|
||
setSettingsLoading(true);
|
||
|
||
switch (parseInt(payload.privacy)) {
|
||
case SESSION_PRIVACY_MAP['public']:
|
||
payload.musician_access = true;
|
||
payload.approval_required = false;
|
||
break;
|
||
case SESSION_PRIVACY_MAP['private_approve']:
|
||
payload.musician_access = true;
|
||
payload.approval_required = true;
|
||
break;
|
||
case SESSION_PRIVACY_MAP['private_invite']:
|
||
payload.musician_access = false;
|
||
payload.approval_required = false;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
const response = await updateSessionSettings({
|
||
id: currentSessionIdRef.current,
|
||
...payload
|
||
});
|
||
const data = await response.json();
|
||
console.log('Updated session settings response:', data);
|
||
dispatch(updateSessionData(data)); // Phase 4: dispatch to Redux
|
||
dispatch(closeModal('settings'));
|
||
toast.success('Session settings updated successfully');
|
||
} catch (error) {
|
||
console.error('Error updating session settings:', error);
|
||
toast.error('Failed to update session settings');
|
||
} finally {
|
||
setSettingsLoading(false);
|
||
}
|
||
|
||
}}
|
||
/>
|
||
|
||
<JKSessionInviteModal
|
||
currentSession={currentSession}
|
||
show={showInviteModal}
|
||
size="lg"
|
||
onToggle={() => dispatch(closeModal('invite'))}
|
||
friends={friends}
|
||
initialInvitees={sessionInvitees}
|
||
loading={inviteLoading}
|
||
onSubmit={async (invitees) => {
|
||
setSessionInvitees(invitees);
|
||
console.log('Submitted invitees:', invitees);
|
||
const inviteeIds = invitees.map(i => i.id)
|
||
const payload = {
|
||
inviteeIds: inviteeIds.join()
|
||
};
|
||
try {
|
||
setInviteLoading(true);
|
||
const response = await updateSessionSettings({
|
||
id: currentSessionIdRef.current,
|
||
...payload
|
||
});
|
||
const data = await response.json();
|
||
console.log('Updated session settings response:', data);
|
||
dispatch(updateSessionData(data)); // Phase 4: dispatch to Redux
|
||
dispatch(closeModal('invite'));
|
||
toast.success('Invitations sent successfully');
|
||
} catch (error) {
|
||
console.error('Error updating session settings:', error);
|
||
toast.error('Failed to send invitations');
|
||
} finally {
|
||
setInviteLoading(false);
|
||
}
|
||
}}
|
||
/>
|
||
|
||
<JKSessionVolumeModal
|
||
isOpen={showVolumeModal}
|
||
toggle={() => dispatch(toggleModal('volume'))}
|
||
/>
|
||
|
||
<JKSessionRecordingModal
|
||
isOpen={showRecordingModal}
|
||
toggle={() => dispatch(toggleModal('recording'))}
|
||
onSubmit={handleRecordingSubmit}
|
||
/>
|
||
|
||
<JKSessionLeaveModal
|
||
isOpen={showLeaveModal}
|
||
toggle={() => dispatch(closeModal('leave'))} // Just close modal, don't navigate since session not left yet
|
||
onSubmit={handleLeaveSubmit}
|
||
loading={leaveLoading}
|
||
/>
|
||
|
||
<UncontrolledTooltip target="audioInputsTooltip" trigger="hover click">
|
||
<div>
|
||
<p>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.</p>
|
||
<p>For instructions on how to set your Audio Input levels, read <a href="/help/audio-input-levels" target="_blank">this help article</a>.</p>
|
||
</div>
|
||
</UncontrolledTooltip>
|
||
|
||
<UncontrolledTooltip target="sessionMixTooltip" trigger="hover click">
|
||
<div>
|
||
<p>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.</p>
|
||
<p>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.</p>
|
||
<p>For instructions on how to set your Session Mix levels, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124834" target="_blank" rel="noopener noreferrer">read this help article</a>.</p>
|
||
</div>
|
||
</UncontrolledTooltip>
|
||
|
||
{/* Backing Track Popup */}
|
||
{showBackingTrackPopup && backingTrackData && (
|
||
<WindowPortal
|
||
onClose={handleBackingTrackClose}
|
||
windowFeatures="width=500,height=400,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no, addressbar=no"
|
||
>
|
||
<JKSessionBackingTrackPlayer
|
||
backingTrack={backingTrackData.backingTrack}
|
||
jamClient={jamClient}
|
||
session={backingTrackData.session}
|
||
currentUser={backingTrackData.currentUser}
|
||
isPopup={true}
|
||
onClose={() => {
|
||
console.log('JKSessionScreen: JKSessionBackingTrackPlayer onClose called');
|
||
dispatch(closeModal('backingTrack'));
|
||
dispatch(clearBackingTrackData());
|
||
}}
|
||
/>
|
||
</WindowPortal>
|
||
)}
|
||
|
||
{/* JamTrack Player Popup */}
|
||
{showJamTrackPlayer && jamTrackData && (
|
||
<WindowPortal
|
||
onClose={handleJamTrackPlayerClose}
|
||
windowFeatures="width=600,height=500,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no, addressbar=no"
|
||
>
|
||
<JKSessionJamTrackPlayer
|
||
jamTrack={jamTrackData.jamTrack}
|
||
jamClient={jamClient}
|
||
session={jamTrackData.session}
|
||
currentUser={jamTrackData.currentUser}
|
||
isPopup={true}
|
||
onClose={handleJamTrackClose}
|
||
/>
|
||
</WindowPortal>
|
||
)}
|
||
|
||
<JKSessionJamTrackModal
|
||
isOpen={showJamTrackModal}
|
||
toggle={() => dispatch(toggleModal('jamTrack'))}
|
||
onJamTrackSelect={handleJamTrackSelect}
|
||
/>
|
||
|
||
{/* 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 (
|
||
<WindowPortal
|
||
onClose={() => {
|
||
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"
|
||
>
|
||
<JKPopupMediaControls onClose={() => {
|
||
console.log('JKSessionScreen: JKPopupMediaControls onClose called');
|
||
dispatch(closeModal('mediaControls'));
|
||
setMediaControlsOpened(false);
|
||
setPopupGuard(false); // Reset guard when closing
|
||
}} />
|
||
</WindowPortal>
|
||
);
|
||
})()}
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
export default memo(JKSessionScreen)
|