1170 lines
38 KiB
JavaScript
1170 lines
38 KiB
JavaScript
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
import { useHistory } from 'react-router-dom';
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
import { debounce } from 'lodash'; // Add lodash for debouncing
|
|
import { useJamClient } from '../context/JamClientContext';
|
|
import {
|
|
selectActiveSession,
|
|
selectSessionId,
|
|
selectInSession,
|
|
setSessionId,
|
|
updateSessionData
|
|
} from '../store/features/activeSessionSlice';
|
|
import {
|
|
setBackingTracks,
|
|
setJamTracks,
|
|
setRecordedTracks
|
|
} from '../store/features/mediaSlice';
|
|
import useGearUtils from './useGearUtils';
|
|
import useTrackHelpers from './useTrackHelpers';
|
|
import useRecordingHelpers from './useRecordingHelpers';
|
|
import useSessionUtils from './useSessionUtils';
|
|
import { joinSession as joinSessionRest, getSession, deleteParticipant } from '../helpers/rest';
|
|
import { MessageType } from '../helpers/MessageFactory';
|
|
import { LATENCY, SESSION_PRIVACY_MAP } from '../helpers/globals';
|
|
|
|
const logger = console;
|
|
|
|
// Constants from original sessionModelAsync.js
|
|
const ALERT_TYPES = {
|
|
// Add alert types as needed
|
|
};
|
|
|
|
const EVENTS = {
|
|
MIXER_MODE_CHANGED: 'mixer_mode_changed',
|
|
SESSION_STARTED: 'session_started',
|
|
SESSION_ENDED: 'session_ended'
|
|
};
|
|
|
|
const MIX_MODES = {
|
|
PERSONAL: 'personal',
|
|
MASTER: 'master'
|
|
};
|
|
|
|
export default function useSessionModel(app, server, sessionScreen) {
|
|
const jamClient = useJamClient();
|
|
const { isNoInputProfile } = useGearUtils();
|
|
const { getTrackInfo, getUserTracks } = useTrackHelpers();
|
|
const { getCurrentRecordingState, reset: resetRecordingState } = useRecordingHelpers();
|
|
|
|
// Phase 4: Replace CurrentSessionContext with Redux
|
|
const dispatch = useDispatch();
|
|
const currentSession = useSelector(selectActiveSession);
|
|
const sessionId = useSelector(selectSessionId);
|
|
const inSessionFlag = useSelector(selectInSession);
|
|
|
|
// Create ref for high-frequency access (performance optimization)
|
|
const sessionIdRef = useRef(sessionId);
|
|
useEffect(() => {
|
|
sessionIdRef.current = sessionId;
|
|
}, [sessionId]);
|
|
|
|
// Maintain function interface for backward compatibility
|
|
const currentSessionIdRef = sessionIdRef;
|
|
|
|
const inSession = useCallback(() => {
|
|
return sessionIdRef.current !== null;
|
|
}, []);
|
|
|
|
const setCurrentSessionId = useCallback((id) => {
|
|
console.log("Setting current session ID to: ", id);
|
|
dispatch(setSessionId(id));
|
|
}, [dispatch]);
|
|
|
|
const setCurrentSession = useCallback((updater) => {
|
|
if (typeof updater === 'function') {
|
|
// Handle functional setState pattern
|
|
const currentData = currentSession;
|
|
const newData = updater(currentData);
|
|
dispatch(updateSessionData(newData));
|
|
} else {
|
|
dispatch(updateSessionData(updater));
|
|
}
|
|
}, [dispatch, currentSession]);
|
|
|
|
const history = useHistory();
|
|
|
|
// State variables from original SessionModel
|
|
const [userTracks, setUserTracks] = useState(null);
|
|
const [currentOrLastSession, setCurrentOrLastSession] = useState(null);
|
|
const [subscribers, setSubscribers] = useState({});
|
|
const [users, setUsers] = useState({}); // User info for session participants
|
|
const [requestingSessionRefresh, setRequestingSessionRefresh] = useState(false);
|
|
const [pendingSessionRefresh, setPendingSessionRefresh] = useState(false);
|
|
const [currentTrackChanges, setCurrentTrackChanges] = useState(0);
|
|
const [participantsEverSeen, setParticipantsEverSeen] = useState({});
|
|
const [currentParticipants, setCurrentParticipants] = useState({});
|
|
const [sessionPageEnterDeferred, setSessionPageEnterDeferred] = useState(null);
|
|
const [sessionPageEnterTimeout, setSessionPageEnterTimeout] = useState(null);
|
|
const [startTime, setStartTime] = useState(null);
|
|
const [joinDeferred, setJoinDeferred] = useState(null);
|
|
const [previousAllTracks, setPreviousAllTracks] = useState({
|
|
userTracks: [],
|
|
backingTracks: [],
|
|
metronomeTracks: []
|
|
});
|
|
const [openBackingTrack, setOpenBackingTrack] = useState(null);
|
|
const [shownAudioMediaMixerHelp, setShownAudioMediaMixerHelp] = useState(false);
|
|
const [controlsLockedForJamTrackRecording, setControlsLockedForJamTrackRecording] = useState(false);
|
|
const [mixerMode, setMixerMode] = useState(MIX_MODES.PERSONAL);
|
|
|
|
// Refs for backend alert throttling
|
|
const backendMixerAlertThrottleTimerRef = useRef(null);
|
|
|
|
// State to hold pending promises and their resolvers (from useSessionEnter)
|
|
const pendingPromisesRef = useRef(new Map());
|
|
|
|
// Refs for session leave functionality (from useSessionLeave)
|
|
const isLeavingRef = useRef(false);
|
|
const recordingModelRef = useRef(null);
|
|
|
|
// Client ID - would need to be passed or obtained from context
|
|
const clientId = jamClient?.clientID || 'unknown';
|
|
|
|
// Promise management functions (from useSessionEnter)
|
|
const resolvePendingPromises = useCallback((inputTracks) => {
|
|
for (const { resolve } of pendingPromisesRef.current.values()) {
|
|
resolve(inputTracks);
|
|
}
|
|
pendingPromisesRef.current.clear();
|
|
}, []);
|
|
|
|
const rejectPendingPromises = useCallback((reason) => {
|
|
for (const { reject } of pendingPromisesRef.current.values()) {
|
|
reject(reason);
|
|
}
|
|
pendingPromisesRef.current.clear();
|
|
}, []);
|
|
|
|
// Event handlers (from useSessionEnter)
|
|
const onWatchedInputs = useCallback((inputTracks) => {
|
|
resolvePendingPromises(inputTracks);
|
|
}, [resolvePendingPromises]);
|
|
|
|
const onMixersChanged = useCallback((type, text, trackInfo) => {
|
|
if (text === 'RebuildAudioIoControl' && trackInfo.userTracks.length > 0) {
|
|
resolvePendingPromises(trackInfo.userTracks);
|
|
}
|
|
}, [resolvePendingPromises]);
|
|
|
|
// Initialize session model
|
|
// useEffect(() => {
|
|
// if (server) {
|
|
// server.registerOnSocketClosed(onWebsocketDisconnected);
|
|
// }
|
|
|
|
// return () => {
|
|
// if (server) {
|
|
// server.unregisterOnSocketClosed(onWebsocketDisconnected);
|
|
// }
|
|
// };
|
|
// }, [server]);
|
|
|
|
// Cleanup on unmount (from useSessionEnter)
|
|
useEffect(() => {
|
|
return () => {
|
|
rejectPendingPromises('component_unmounted');
|
|
};
|
|
}, [rejectPendingPromises]);
|
|
|
|
// Cleanup backend mixer alert throttle timer on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (backendMixerAlertThrottleTimerRef.current) {
|
|
clearTimeout(backendMixerAlertThrottleTimerRef.current);
|
|
backendMixerAlertThrottleTimerRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Core session functions
|
|
const id = useCallback(() => {
|
|
return currentSessionIdRef.current;
|
|
}, [currentSessionIdRef]);
|
|
|
|
const start = useCallback((sessionId) => {
|
|
setCurrentSessionId(sessionId);
|
|
setStartTime(new Date().getTime());
|
|
}, [setCurrentSessionId]);
|
|
|
|
const setUserTracksState = useCallback((_userTracks) => {
|
|
setUserTracks(_userTracks);
|
|
}, []);
|
|
|
|
const participants = useCallback(() => {
|
|
if (currentSession) {
|
|
return currentSession.participants || [];
|
|
}
|
|
return [];
|
|
}, [currentSession]);
|
|
|
|
// Check if metronome is open
|
|
const isMetronomeOpen = useCallback(() => {
|
|
let metronomeOpen = false;
|
|
participants().forEach((participant) => {
|
|
if (participant.metronome_open) {
|
|
metronomeOpen = true;
|
|
}
|
|
});
|
|
return metronomeOpen;
|
|
}, [participants]);
|
|
|
|
// Check if playing recording
|
|
const isPlayingRecording = useCallback(() => {
|
|
return !!(currentSession && currentSession.claimed_recording);
|
|
}, [currentSession]);
|
|
|
|
// Get recorded tracks
|
|
const recordedTracks = useCallback(() => {
|
|
if (currentSession && currentSession.claimed_recording) {
|
|
return currentSession.claimed_recording.recording.recorded_tracks;
|
|
}
|
|
return null;
|
|
}, [currentSession]);
|
|
|
|
// Get recorded backing tracks
|
|
const recordedBackingTracks = useCallback(() => {
|
|
if (currentSession && currentSession.claimed_recording) {
|
|
return currentSession.claimed_recording.recording.recorded_backing_tracks;
|
|
}
|
|
return null;
|
|
}, [currentSession]);
|
|
|
|
// Get backing tracks
|
|
const backingTracks = useCallback(() => {
|
|
let backingTracks = [];
|
|
participants().forEach((participant) => {
|
|
if (participant.backing_tracks && participant.backing_tracks.length > 0) {
|
|
backingTracks = participant.backing_tracks;
|
|
}
|
|
});
|
|
return backingTracks;
|
|
}, [participants]);
|
|
|
|
// Get jam tracks
|
|
const jamTracks = useCallback(() => {
|
|
if (currentSession && currentSession.jam_track) {
|
|
return currentSession.jam_track.tracks.filter(track => track.track_type === 'Track');
|
|
}
|
|
return null;
|
|
}, [currentSession]);
|
|
|
|
// Get recorded jam tracks
|
|
const recordedJamTracks = useCallback(() => {
|
|
if (currentSession && currentSession.claimed_recording) {
|
|
return currentSession.claimed_recording.recording.recorded_jam_track_tracks;
|
|
}
|
|
return null;
|
|
}, [currentSession]);
|
|
|
|
// Get jam track name
|
|
const jamTrackName = useCallback(() => {
|
|
if (currentSession && currentSession.jam_track) {
|
|
return currentSession.jam_track.name;
|
|
}
|
|
return null;
|
|
}, [currentSession]);
|
|
|
|
// Get recorded jam track name
|
|
const recordedJamTrackName = useCallback(() => {
|
|
if (currentSession && currentSession.claimed_recording && currentSession.claimed_recording.recording.jam_track) {
|
|
return currentSession.claimed_recording.recording.jam_track.name;
|
|
}
|
|
return null;
|
|
}, [currentSession]);
|
|
|
|
// Check if self opened jam tracks
|
|
const selfOpenedJamTracks = useCallback(() => {
|
|
return currentSession && (currentSession.jam_track_initiator_id === window.JK?.currentUserId);
|
|
}, [currentSession]);
|
|
|
|
// Get backing track
|
|
const backingTrack = useCallback(() => {
|
|
if (currentSession) {
|
|
return {
|
|
path: currentSession.backing_track_path
|
|
};
|
|
}
|
|
return null;
|
|
}, [currentSession]);
|
|
|
|
// Get creator ID
|
|
const creatorId = useCallback(() => {
|
|
if (!currentSession) {
|
|
throw "creator is not known";
|
|
}
|
|
return currentSession.user_id;
|
|
}, [currentSession]);
|
|
|
|
// Get musician access
|
|
const getMusicianAccess = useCallback(() => {
|
|
if (!currentSession.musician_access && !currentSession.approval_required) {
|
|
return SESSION_PRIVACY_MAP.private_invite;
|
|
} else if (currentSession.musician_access && currentSession.approval_required) {
|
|
return SESSION_PRIVACY_MAP.private_approve;
|
|
} else if (currentSession.musician_access && !currentSession.approval_required) {
|
|
return SESSION_PRIVACY_MAP.public;
|
|
}
|
|
|
|
}, [currentSession]);
|
|
|
|
// Check if already in session
|
|
const alreadyInSession = useCallback(() => {
|
|
let inSession = false;
|
|
participants().forEach((participant) => {
|
|
if (participant.user.id === window.JK?.currentUserId) {
|
|
inSession = true;
|
|
}
|
|
});
|
|
return inSession;
|
|
}, [participants]);
|
|
|
|
// Control locking functions
|
|
const lockControlsforJamTrackRecording = useCallback(() => {
|
|
setControlsLockedForJamTrackRecording(true);
|
|
}, []);
|
|
|
|
const unlockControlsforJamTrackRecording = useCallback(() => {
|
|
setControlsLockedForJamTrackRecording(false);
|
|
}, []);
|
|
|
|
const areControlsLockedForJamTrackRecording = useCallback(() => {
|
|
return controlsLockedForJamTrackRecording;
|
|
}, []);
|
|
|
|
// Mixer mode functions
|
|
const onMixerModeChanged = useCallback((newMixerMode) => {
|
|
setMixerMode(newMixerMode);
|
|
const mode = newMixerMode === MIX_MODES.MASTER ? "master" : "personal";
|
|
logger.debug("onMixerModeChanged:" + mode);
|
|
// Trigger event - in React this would be through state updates or callbacks
|
|
}, []);
|
|
|
|
const setMixerModeState = useCallback((newMixerMode) => {
|
|
if (mixerMode !== newMixerMode) {
|
|
onMixerModeChanged(newMixerMode);
|
|
}
|
|
}, [mixerMode, onMixerModeChanged]);
|
|
|
|
const isMasterMixMode = useCallback(() => {
|
|
return mixerMode === MIX_MODES.MASTER;
|
|
}, [mixerMode]);
|
|
|
|
const isPersonalMixMode = useCallback(() => {
|
|
return mixerMode === MIX_MODES.PERSONAL;
|
|
}, [mixerMode]);
|
|
|
|
const getMixMode = useCallback(() => {
|
|
return mixerMode;
|
|
}, [mixerMode]);
|
|
|
|
// Wait for session page enter done (promise-based from useSessionEnter)
|
|
const waitForSessionPageEnterDone = useCallback(() => {
|
|
return new Promise(async (resolve, reject) => {
|
|
// Check if tracks are already available
|
|
const inputTracks = await getUserTracks();
|
|
|
|
logger.debug("isNoInputProfile", await isNoInputProfile());
|
|
logger.debug("inputTracks", inputTracks);
|
|
if (inputTracks.length > 0 || await isNoInputProfile()) {
|
|
logger.debug("on page enter, tracks are already available");
|
|
resolve(inputTracks);
|
|
return;
|
|
}
|
|
|
|
// Create timeout promise
|
|
const timeoutPromise = new Promise((_, rejectTimeout) => {
|
|
setTimeout(() => rejectTimeout('timeout'), 5000);
|
|
});
|
|
|
|
// Create promise that waits for tracks
|
|
const tracksPromise = new Promise((resolveTracks, rejectTracks) => {
|
|
const promiseId = Symbol('sessionPageEnter');
|
|
pendingPromisesRef.current.set(promiseId, { resolve: resolveTracks, reject: rejectTracks });
|
|
|
|
// Cleanup function to remove this promise if it gets resolved/rejected elsewhere
|
|
return () => {
|
|
pendingPromisesRef.current.delete(promiseId);
|
|
};
|
|
});
|
|
|
|
// Race between tracks and timeout
|
|
Promise.race([tracksPromise, timeoutPromise])
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
}, [getUserTracks, isNoInputProfile]);
|
|
|
|
// Duplicate declaration of SessionPageEnter removed to fix redeclaration error.
|
|
|
|
|
|
// Join session
|
|
const joinSession = useCallback(async (sessionId) => {
|
|
logger.debug("SessionModel.joinSession(" + sessionId + ")");
|
|
|
|
const deferred = joinSessionRest({ session_id: sessionId });
|
|
setJoinDeferred(deferred);
|
|
|
|
try {
|
|
const response = await deferred;
|
|
|
|
if (!inSession()) {
|
|
logger.debug("user left before fully joined to session. telling server again that they have left");
|
|
// leaveSessionRest(response.id); // Would need to implement
|
|
return;
|
|
}
|
|
|
|
logger.debug("calling jamClient.JoinSession");
|
|
if (!alreadyInSession()) {
|
|
// GA tracking would go here
|
|
}
|
|
|
|
resetRecordingState(currentSessionIdRef.current);
|
|
// Register message callbacks would go here
|
|
|
|
// Trigger session started event
|
|
// $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId}});
|
|
|
|
refreshCurrentSession(true);
|
|
} catch (error) {
|
|
updateCurrentSession(null);
|
|
}
|
|
|
|
return deferred;
|
|
}, [currentSessionIdRef]);
|
|
|
|
// Set recording model (from useSessionLeave)
|
|
const setRecordingModel = useCallback((recordingModel) => {
|
|
recordingModelRef.current = recordingModel;
|
|
}, []);
|
|
|
|
// Leave session REST call (from useSessionLeave)
|
|
const leaveSessionRest = useCallback(async () => {
|
|
const clientId = jamClient.GetClientID();
|
|
if (!clientId) return;
|
|
|
|
try {
|
|
await deleteParticipant(clientId);
|
|
logger.debug("Successfully left session via REST API");
|
|
} catch (error) {
|
|
logger.error("Error leaving session via REST:", error);
|
|
// Don't throw - we want to continue with client-side cleanup
|
|
}
|
|
}, [jamClient, logger]);
|
|
|
|
// Perform the actual leave session (from useSessionLeave)
|
|
const performLeaveSession = useCallback(async () => {
|
|
|
|
if (isLeavingRef.current) {
|
|
logger.debug("Leave session already in progress");
|
|
return;
|
|
}
|
|
|
|
isLeavingRef.current = true;
|
|
const sessionId = currentSessionIdRef.current;
|
|
|
|
try {
|
|
logger.debug("Starting session leave process");
|
|
|
|
// Stop recording if needed
|
|
if (recordingModelRef.current?.stopRecordingIfNeeded) {
|
|
try {
|
|
await recordingModelRef.current.stopRecordingIfNeeded();
|
|
logger.debug("Recording stopped successfully");
|
|
} catch (error) {
|
|
logger.error("Error stopping recording:", error);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Leave the session via jamClient (don't wait for REST)
|
|
logger.debug("Leaving session via jamClient for sessionId:", sessionId);
|
|
try {
|
|
await jamClient.LeaveSession({ sessionID: sessionId });
|
|
logger.debug("Successfully left session via jamClient");
|
|
} catch (error) {
|
|
logger.error("Error leaving session via jamClient:", error);
|
|
}
|
|
|
|
// Make REST call to server (fire and forget for faster UX)
|
|
await leaveSessionRest(sessionId);
|
|
|
|
// Unregister callbacks
|
|
try {
|
|
await jamClient.SessionRegisterCallback("");
|
|
await jamClient.SessionSetConnectionStatusRefreshRate(0);
|
|
logger.debug("Callbacks unregistered successfully");
|
|
} catch (error) {
|
|
logger.error("Error unregistering callbacks:", error);
|
|
}
|
|
|
|
|
|
|
|
// Call session page leave
|
|
try {
|
|
// Note: SessionPageLeave would need to be imported from useSessionUtils
|
|
// For now, we'll skip this as it's not directly available
|
|
logger.debug("Session page leave completed");
|
|
} catch (error) {
|
|
logger.error("Error in SessionPageLeave:", error);
|
|
}
|
|
|
|
// Trigger session ended event
|
|
// TODO: Re-implement this with React context or state management
|
|
// if (document && window.$) {
|
|
// // $(document).trigger('SESSION_ENDED', { session: { id: sessionId } });
|
|
// logger.debug("Session ended event triggered");
|
|
// }
|
|
|
|
logger.debug("Session leave completed successfully");
|
|
|
|
|
|
} catch (error) {
|
|
logger.error("Unexpected error during session leave:", error);
|
|
throw error;
|
|
} finally {
|
|
// Reset session state
|
|
currentSessionIdRef.current = null;
|
|
recordingModelRef.current = null;
|
|
isLeavingRef.current = false;
|
|
}
|
|
}, [jamClient, leaveSessionRest, logger]);
|
|
|
|
// Main leave session function (from useSessionLeave)
|
|
const leaveSession = useCallback(async () => {
|
|
await performLeaveSession();
|
|
}, [performLeaveSession]);
|
|
|
|
// Handle leave session with behavior (navigation, notifications, etc.) (from useSessionLeave)
|
|
const handleLeaveSession = useCallback(async (behavior = {}) => {
|
|
logger.debug("Handling leave session with behavior:", behavior);
|
|
|
|
try {
|
|
// Handle notifications
|
|
// if (behavior.notify && window.JK?.app?.layout) {
|
|
// window.JK.app.layout.notify(behavior.notify);
|
|
// }
|
|
|
|
// // Allow leave session (trigger any leave confirmation logic)
|
|
// if (window.SessionActions?.allowLeaveSession) {
|
|
// window.SessionActions.allowLeaveSession.trigger();
|
|
// }
|
|
|
|
// Perform the leave operation
|
|
await leaveSession();
|
|
|
|
// Handle navigation after successful leave
|
|
// if (behavior.location) {
|
|
// if (typeof behavior.location === 'number') {
|
|
// window.history.go(behavior.location);
|
|
// } else {
|
|
// window.location = behavior.location;
|
|
// }
|
|
// } else if (behavior.hash) {
|
|
// window.location.hash = behavior.hash;
|
|
// } else {
|
|
// logger.warn("No location specified in leaveSession action, defaulting to home", behavior);
|
|
// window.location = '/client#/home';
|
|
// }
|
|
|
|
// Handle lesson session rating if applicable
|
|
// Note: This would need additional context/state to implement fully
|
|
// For now, just log that rating logic would go here
|
|
//logger.debug("Lesson session rating logic would be handled here");
|
|
|
|
|
|
|
|
} catch (error) {
|
|
logger.error("Error handling leave session:", error);
|
|
throw error;
|
|
}
|
|
}, [leaveSession, logger]);
|
|
|
|
// Check if currently leaving (from useSessionLeave)
|
|
const isLeaving = useCallback(() => {
|
|
return isLeavingRef.current;
|
|
}, []);
|
|
|
|
// Leave current session (legacy function, now delegates to leaveSession)
|
|
const leaveCurrentSession = useCallback(async () => {
|
|
return leaveSession();
|
|
}, [leaveSession]);
|
|
|
|
// Refresh current session
|
|
const refreshCurrentSession = useCallback(async (force = false) => {
|
|
if (force) {
|
|
logger.debug("refreshCurrentSession(force=true)");
|
|
}
|
|
|
|
await refreshCurrentSessionRest(sessionChanged, force);
|
|
}, []);
|
|
|
|
// Track changes handler - debounced to prevent excessive session refreshes
|
|
const trackChanges = useCallback(debounce((header, payload) => {
|
|
if (currentTrackChanges < payload.track_changes_counter) {
|
|
logger.debug("track_changes_counter = stale. refreshing...");
|
|
refreshCurrentSession();
|
|
} else {
|
|
if (header.type !== 'HEARTBEAT_ACK') {
|
|
logger.info("track_changes_counter = fresh. skipping refresh...", header, payload);
|
|
}
|
|
}
|
|
}, 500), [currentTrackChanges, refreshCurrentSession]);
|
|
|
|
// Subscribe to session changes
|
|
const subscribe = useCallback((subscriberId, sessionChangedCallback) => {
|
|
logger.debug("SessionModel.subscribe(" + subscriberId + ", [callback])");
|
|
setSubscribers(prev => ({
|
|
...prev,
|
|
[subscriberId]: sessionChangedCallback
|
|
}));
|
|
}, []);
|
|
|
|
// Notify subscribers of session changes
|
|
const sessionChanged = useCallback(() => {
|
|
Object.values(subscribers).forEach(callback => {
|
|
callback(currentSession);
|
|
});
|
|
}, [subscribers, currentSession]);
|
|
|
|
// Session ended cleanup
|
|
const sessionEnded = useCallback((fullyJoined) => {
|
|
// Cleanup logic
|
|
setUserTracks(null);
|
|
setStartTime(null);
|
|
setJoinDeferred(null);
|
|
setMixerMode(MIX_MODES.PERSONAL);
|
|
|
|
if (sessionPageEnterDeferred) {
|
|
// sessionPageEnterDeferred.reject('session_over');
|
|
setSessionPageEnterDeferred(null);
|
|
}
|
|
|
|
setCurrentParticipants({});
|
|
setPreviousAllTracks({
|
|
userTracks: [],
|
|
backingTracks: [],
|
|
metronomeTracks: []
|
|
});
|
|
setOpenBackingTrack(null);
|
|
setShownAudioMediaMixerHelp(false);
|
|
setControlsLockedForJamTrackRecording(false);
|
|
|
|
if (fullyJoined) {
|
|
// $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionIdRef.current}});
|
|
}
|
|
setCurrentSessionId(null);
|
|
}, [sessionPageEnterDeferred]);
|
|
|
|
// Update current session
|
|
const updateCurrentSession = useCallback((sessionData) => {
|
|
if (sessionData !== null) {
|
|
setCurrentOrLastSession(sessionData);
|
|
}
|
|
|
|
const beforeUpdate = currentSession;
|
|
setCurrentSession(sessionData);
|
|
|
|
if (sessionData === null) {
|
|
sessionEnded(beforeUpdate !== null);
|
|
}
|
|
}, [currentSession, sessionEnded, setCurrentSession]);
|
|
|
|
// Update session info
|
|
const updateSessionInfo = useCallback((response, callback, force) => {
|
|
if (force === true || currentTrackChanges < response.track_changes_counter) {
|
|
logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter);
|
|
setCurrentTrackChanges(response.track_changes_counter);
|
|
|
|
// sendClientParticipantChanges logic would go here
|
|
|
|
updateCurrentSession(response);
|
|
|
|
// Extract and dispatch media arrays to Redux
|
|
// Extract backing tracks from participants
|
|
let extractedBackingTracks = [];
|
|
if (response.participants) {
|
|
response.participants.forEach((participant) => {
|
|
if (participant.backing_tracks && participant.backing_tracks.length > 0) {
|
|
extractedBackingTracks = participant.backing_tracks;
|
|
}
|
|
});
|
|
}
|
|
dispatch(setBackingTracks(extractedBackingTracks));
|
|
console.log('[useSessionModel] Extracted backing tracks:', extractedBackingTracks);
|
|
|
|
// Extract jam tracks
|
|
const extractedJamTracks = response.jam_track && response.jam_track.tracks
|
|
? response.jam_track.tracks.filter(track => track.track_type === 'Track')
|
|
: [];
|
|
dispatch(setJamTracks(extractedJamTracks));
|
|
console.log('[useSessionModel] Extracted jam tracks:', extractedJamTracks);
|
|
|
|
// Extract recorded tracks
|
|
const extractedRecordedTracks = response.claimed_recording && response.claimed_recording.recording
|
|
? response.claimed_recording.recording.recorded_tracks || []
|
|
: [];
|
|
dispatch(setRecordedTracks(extractedRecordedTracks));
|
|
console.log('[useSessionModel] Extracted recorded tracks:', extractedRecordedTracks);
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
} else {
|
|
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
|
|
}
|
|
}, [currentTrackChanges, updateCurrentSession, dispatch]);
|
|
|
|
// Refresh current session REST call
|
|
const refreshCurrentSessionRest = useCallback(async (callback, force) => {
|
|
if (!inSession()) {
|
|
logger.debug("refreshCurrentSession skipped: ");
|
|
return;
|
|
}
|
|
|
|
if (requestingSessionRefresh) {
|
|
logger.debug("queueing refresh");
|
|
setPendingSessionRefresh(true);
|
|
return;
|
|
}
|
|
|
|
setRequestingSessionRefresh(true);
|
|
|
|
try {
|
|
const response = await getSession(currentSessionIdRef.current);
|
|
updateSessionInfo(response, callback, force);
|
|
} catch (jqXHR) {
|
|
if (jqXHR.status !== 404) {
|
|
app.notifyServerError(jqXHR, "Unable to refresh session data");
|
|
} else {
|
|
logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone");
|
|
}
|
|
} finally {
|
|
setRequestingSessionRefresh(false);
|
|
if (pendingSessionRefresh) {
|
|
setPendingSessionRefresh(false);
|
|
refreshCurrentSessionRest(sessionChanged, force);
|
|
}
|
|
}
|
|
}, [inSession, requestingSessionRefresh, pendingSessionRefresh, currentSessionIdRef, updateSessionInfo, sessionChanged, app]);
|
|
|
|
// Participant management functions
|
|
const participantForClientId = useCallback((clientId) => {
|
|
const foundParticipant = participants().find(participant => participant.client_id === clientId);
|
|
return foundParticipant;
|
|
}, [participants]);
|
|
|
|
// Track sync functionality
|
|
const syncTracks = useCallback(async (allTracks) => {
|
|
if (!inSession()) {
|
|
logger.debug("dropping queued up sync tracks because no longer in session");
|
|
return null;
|
|
}
|
|
|
|
if (!allTracks) {
|
|
allTracks = await getTrackInfo();
|
|
}
|
|
|
|
const inputTracks = allTracks.userTracks;
|
|
const backingTracksData = allTracks.backingTracks;
|
|
const metronomeTracks = allTracks.metronomeTracks;
|
|
|
|
const syncTrackRequest = {
|
|
client_id: clientId,
|
|
tracks: inputTracks,
|
|
backing_tracks: backingTracksData,
|
|
metronome_open: metronomeTracks.length > 0,
|
|
id: id()
|
|
};
|
|
|
|
// REST call would go here
|
|
// return rest.putTrackSyncChange(syncTrackRequest);
|
|
return Promise.resolve();
|
|
}, [inSession, getTrackInfo, clientId, id]);
|
|
|
|
// WebSocket disconnected handler
|
|
const onWebsocketDisconnected = useCallback(async (in_error) => {
|
|
if (currentSessionIdRef.current) {
|
|
logger.debug("onWebsocketDisconnect: calling jamClient.LeaveSession for clientId=" + clientId);
|
|
await jamClient.LeaveSession({ sessionID: currentSessionIdRef.current });
|
|
}
|
|
}, [jamClient, currentSessionIdRef, clientId]);
|
|
|
|
// Find user by criteria
|
|
const findUserBy = useCallback((finder) => {
|
|
if (finder.clientId) {
|
|
const foundParticipant = participants().find(participant => participant.client_id === finder.clientId);
|
|
if (foundParticipant) {
|
|
return Promise.resolve(foundParticipant.user);
|
|
}
|
|
}
|
|
return Promise.reject();
|
|
}, [participants]);
|
|
|
|
// Alert handlers
|
|
const onDeadUserRemove = useCallback((type, text) => {
|
|
if (!inSession()) return;
|
|
const clientId = text;
|
|
const participant = participantsEverSeen[clientId];
|
|
if (participant) {
|
|
app.notify({
|
|
"title": ALERT_TYPES[type]?.title || "User Issue",
|
|
"text": participant.user.name + " is no longer sending audio.",
|
|
"icon_url": "" // avatar URL
|
|
});
|
|
// Track disabling logic would go here
|
|
}
|
|
}, [inSession, participantsEverSeen, app]);
|
|
|
|
const onWindowBackgrounded = useCallback((type, text) => {
|
|
// Window backgrounded logic
|
|
}, []);
|
|
|
|
const onBroadcastSuccess = useCallback((type, text) => {
|
|
// Broadcast success logic
|
|
}, []);
|
|
|
|
const onBroadcastFailure = useCallback((type, text) => {
|
|
// Broadcast failure logic
|
|
}, []);
|
|
|
|
const onBroadcastStopped = useCallback((type, text) => {
|
|
// Broadcast stopped logic
|
|
}, []);
|
|
|
|
const onPlaybackStateChange = useCallback((type, text) => {
|
|
if (sessionScreen) {
|
|
sessionScreen.onPlaybackStateChange(text);
|
|
}
|
|
}, [sessionScreen]);
|
|
|
|
const onBackendMixerChanged = useCallback(async (type, text) => {
|
|
logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);
|
|
|
|
if (inSession() && text === "RebuildAudioIoControl") {
|
|
if (backendMixerAlertThrottleTimerRef.current) {
|
|
clearTimeout(backendMixerAlertThrottleTimerRef.current);
|
|
}
|
|
|
|
backendMixerAlertThrottleTimerRef.current = setTimeout(async () => {
|
|
// Track availability check logic
|
|
if (joinDeferred) {
|
|
joinDeferred.done(() => {
|
|
syncTracks();
|
|
});
|
|
}
|
|
}, 100);
|
|
} else if (inSession() && (text === 'RebuildMediaControl' || text === 'RebuildRemoteUserControl')) {
|
|
const allTracks = await getTrackInfo();
|
|
const backingTracksData = allTracks.backingTracks;
|
|
const previousBackingTracks = previousAllTracks.backingTracks;
|
|
const metronomeTracks = allTracks.metronomeTracks;
|
|
const previousMetronomeTracks = previousAllTracks.metronomeTracks;
|
|
|
|
if (!(previousBackingTracks.length === 0 && backingTracksData.length === 0) &&
|
|
JSON.stringify(previousBackingTracks) !== JSON.stringify(backingTracksData)) {
|
|
logger.debug("backing tracks changed", previousBackingTracks, backingTracksData);
|
|
await syncTracks(allTracks);
|
|
// Refresh session to get updated mixer data for backing tracks
|
|
refreshCurrentSession(true);
|
|
} else if (!(previousMetronomeTracks.length === 0 && metronomeTracks.length === 0) &&
|
|
JSON.stringify(previousMetronomeTracks) !== JSON.stringify(metronomeTracks)) {
|
|
logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks);
|
|
await syncTracks(allTracks);
|
|
// Refresh session to get updated mixer data
|
|
refreshCurrentSession(true);
|
|
} else {
|
|
refreshCurrentSession(true);
|
|
}
|
|
|
|
setPreviousAllTracks(allTracks);
|
|
} else if (inSession() && (text === 'Global Peer Input Mixer Mode')) {
|
|
setMixerModeState(MIX_MODES.MASTER);
|
|
} else if (inSession() && (text === 'Local Peer Stream Mixer Mode')) {
|
|
setMixerModeState(MIX_MODES.PERSONAL);
|
|
}
|
|
}, [inSession, joinDeferred, syncTracks, getTrackInfo, previousAllTracks, refreshCurrentSession, setMixerModeState]);
|
|
|
|
// Audio establishment tracking
|
|
const setAudioEstablished = useCallback((clientId, audioEstablished) => {
|
|
setCurrentParticipants(prev => {
|
|
const participant = prev[clientId];
|
|
if (participant) {
|
|
if (participant.client.audio_established === null) {
|
|
participant.client.audio_established = audioEstablished;
|
|
// Stats tracking would go here
|
|
} else if (participant.client.audio_established === false && audioEstablished === true) {
|
|
// Delayed success tracking
|
|
}
|
|
}
|
|
return prev;
|
|
});
|
|
}, []);
|
|
|
|
// Clear audio timeout (from useSessionUtils)
|
|
const clearAudioTimeout = useCallback(() => {
|
|
if (window.JK?.AudioStopTimeout) {
|
|
clearTimeout(window.JK.AudioStopTimeout);
|
|
window.JK.AudioStopTimeout = null;
|
|
}
|
|
}, []);
|
|
|
|
// FTUE functions (from useSessionUtils)
|
|
const FTUEPageEnter = useCallback(async () => {
|
|
logger.debug("sessionUtils: FTUEPageEnter");
|
|
clearAudioTimeout();
|
|
if (jamClient?.FTUEPageEnter) {
|
|
await jamClient.FTUEPageEnter();
|
|
}
|
|
}, [jamClient, logger, clearAudioTimeout]);
|
|
|
|
const FTUEPageLeave = useCallback(async () => {
|
|
logger.debug("sessionUtils: FTUEPageLeave");
|
|
clearAudioTimeout();
|
|
if (jamClient?.FTUEPageLeave) {
|
|
await jamClient.FTUEPageLeave();
|
|
}
|
|
}, [jamClient, logger, clearAudioTimeout]);
|
|
|
|
const SessionPageEnter = useCallback(async () => {
|
|
logger.debug("sessionUtils: SessionPageEnter");
|
|
clearAudioTimeout();
|
|
if (jamClient?.SessionPageEnter) {
|
|
return await jamClient.SessionPageEnter();
|
|
}
|
|
}, [jamClient, logger, clearAudioTimeout]);
|
|
|
|
const SessionPageLeave = useCallback(async () => {
|
|
logger.debug("sessionUtils: SessionPageLeave");
|
|
clearAudioTimeout();
|
|
if (jamClient?.SessionPageLeave) {
|
|
await jamClient.SessionPageLeave();
|
|
}
|
|
}, [jamClient, logger, clearAudioTimeout]);
|
|
|
|
// Auto-open jam track functionality (from useSessionUtils)
|
|
const autoOpenJamTrackRef = useRef(null);
|
|
|
|
const setAutoOpenJamTrack = useCallback((jamTrack) => {
|
|
logger.debug("setting auto-load jamtrack", jamTrack);
|
|
autoOpenJamTrackRef.current = jamTrack;
|
|
}, [logger]);
|
|
|
|
const grabAutoOpenJamTrack = useCallback(() => {
|
|
const jamTrack = autoOpenJamTrackRef.current;
|
|
autoOpenJamTrackRef.current = null;
|
|
logger.debug("grabbing auto-load jamtrack", jamTrack);
|
|
return jamTrack;
|
|
}, [logger]);
|
|
|
|
// Latency data structure conversion (from useSessionUtils)
|
|
const changeLatencyDataStructure = useCallback((data) => {
|
|
return {
|
|
id: data.user_id,
|
|
audio_latency: data.audio_latency,
|
|
full_score: data.ars['total_latency'],
|
|
internet_score: data.ars['internet_latency']
|
|
};
|
|
}, []);
|
|
|
|
// Score info calculation (from useSessionUtils)
|
|
const scoreInfo = useCallback((userSession, isSameUser) => {
|
|
const full_score = userSession.full_score;
|
|
const internet_score = parseInt(userSession.internet_score);
|
|
const audio_latency = parseInt(userSession.audio_latency);
|
|
|
|
let latencyDescription;
|
|
let latencyStyle;
|
|
let iconName;
|
|
let description;
|
|
let latencyInfo;
|
|
|
|
if (isSameUser) {
|
|
latencyDescription = LATENCY.ME.description;
|
|
latencyStyle = LATENCY.ME.style;
|
|
iconName = 'purple';
|
|
description = 'me';
|
|
latencyInfo = '';
|
|
} else if (full_score <= LATENCY.FAILED.max) {
|
|
latencyDescription = LATENCY.FAILED.description;
|
|
latencyStyle = LATENCY.FAILED.style;
|
|
iconName = 'gray';
|
|
description = 'failed';
|
|
latencyInfo = '';
|
|
} else if (!full_score || full_score <= LATENCY.UNKNOWN.max) {
|
|
latencyDescription = LATENCY.UNKNOWN.description;
|
|
latencyStyle = LATENCY.UNKNOWN.style;
|
|
iconName = 'purple';
|
|
description = 'missing';
|
|
latencyInfo = '';
|
|
} else if (full_score <= LATENCY.GOOD.max) {
|
|
latencyDescription = LATENCY.GOOD.description;
|
|
latencyStyle = LATENCY.GOOD.style;
|
|
iconName = 'green';
|
|
description = 'good';
|
|
latencyInfo = 'Internet ' + internet_score + 'ms + Audio ' + audio_latency + 'ms';
|
|
} else if (full_score <= LATENCY.MEDIUM.max) {
|
|
latencyDescription = LATENCY.MEDIUM.description;
|
|
latencyStyle = LATENCY.MEDIUM.style;
|
|
iconName = 'yellow';
|
|
description = 'fair';
|
|
latencyInfo = 'Internet ' + internet_score + 'ms + Audio ' + audio_latency + 'ms';
|
|
} else if (full_score <= LATENCY.POOR.max) {
|
|
latencyDescription = LATENCY.POOR.description;
|
|
latencyStyle = LATENCY.POOR.style;
|
|
iconName = 'red';
|
|
description = 'poor';
|
|
latencyInfo = 'Internet ' + internet_score + 'ms + Audio ' + audio_latency + 'ms';
|
|
} else if (full_score > LATENCY.UNACCEPTABLE.min) {
|
|
latencyStyle = LATENCY.UNACCEPTABLE.style;
|
|
latencyDescription = LATENCY.UNACCEPTABLE.description;
|
|
iconName = 'blue';
|
|
description = 'unacceptable';
|
|
latencyInfo = 'Internet ' + internet_score + 'ms + Audio ' + audio_latency + 'ms';
|
|
}
|
|
|
|
return {
|
|
latency_style: latencyStyle,
|
|
latency_text: latencyDescription,
|
|
icon_name: iconName,
|
|
description: description,
|
|
latency_info: latencyInfo
|
|
};
|
|
}, []);
|
|
|
|
// Create latency info (from useSessionUtils)
|
|
const createLatency = useCallback((userLatency) => {
|
|
// Note: In React, we don't have access to currentUserId in the same way
|
|
// This would need to be passed as a parameter or from context
|
|
const isSameUser = userLatency.id === window.JK?.currentUserId; // Fallback for now
|
|
return scoreInfo(userLatency, isSameUser);
|
|
}, [scoreInfo]);
|
|
|
|
// Join session from custom URL scheme (from useSessionUtils)
|
|
const joinSessionFromCustomUrlScheme = useCallback((hash) => {
|
|
const qStr = hash.substring(hash.lastIndexOf('/') + 1);
|
|
const qParamsArr = qStr.split('|');
|
|
let isCustom = undefined;
|
|
let sessionId = undefined;
|
|
|
|
qParamsArr.forEach((q) => {
|
|
const qp = q.split('~');
|
|
if (qp[0] === 'custom') {
|
|
isCustom = qp[1];
|
|
}
|
|
if (qp[0] === 'sessionId') {
|
|
sessionId = qp[1];
|
|
}
|
|
});
|
|
|
|
if (!isCustom || isCustom !== 'yes') {
|
|
return;
|
|
}
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
|
|
// Note: joinSession implementation would need to be provided
|
|
// For now, just log
|
|
logger.debug("Would join session from custom URL:", sessionId);
|
|
}, [logger]);
|
|
|
|
// Ensure session ended
|
|
const ensureEnded = useCallback(() => {
|
|
updateCurrentSession(null);
|
|
}, [updateCurrentSession]);
|
|
|
|
// Return public interface
|
|
return {
|
|
// Core session functions
|
|
id,
|
|
start,
|
|
backingTrack,
|
|
backingTracks,
|
|
recordedBackingTracks,
|
|
recordedJamTracks,
|
|
setUserTracks: setUserTracksState,
|
|
recordedTracks,
|
|
jamTrackName,
|
|
recordedJamTrackName,
|
|
jamTracks,
|
|
participants,
|
|
joinSession,
|
|
leaveSessionRest,
|
|
leaveCurrentSession,
|
|
refreshCurrentSession,
|
|
updateSessionInfo,
|
|
updateCurrentSession,
|
|
subscribe,
|
|
participantForClientId,
|
|
isPlayingRecording,
|
|
onWebsocketDisconnected,
|
|
inSession,
|
|
setMixerMode: setMixerModeState,
|
|
isMasterMixMode,
|
|
isPersonalMixMode,
|
|
getMixMode,
|
|
selfOpenedJamTracks,
|
|
isMetronomeOpen,
|
|
areControlsLockedForJamTrackRecording,
|
|
lockControlsforJamTrackRecording,
|
|
unlockControlsforJamTrackRecording,
|
|
|
|
// Alert handlers
|
|
onBackendMixerChanged,
|
|
onDeadUserRemove,
|
|
onWindowBackgrounded,
|
|
waitForSessionPageEnterDone,
|
|
onBroadcastStopped,
|
|
onBroadcastSuccess,
|
|
onBroadcastFailure,
|
|
onPlaybackStateChange,
|
|
|
|
// Event handlers from useSessionEnter
|
|
onWatchedInputs,
|
|
onMixersChanged,
|
|
|
|
// Session leave functions from useSessionLeave
|
|
setRecordingModel,
|
|
leaveSession,
|
|
handleLeaveSession,
|
|
isLeaving,
|
|
|
|
// Getters
|
|
getCurrentSession: () => currentSession,
|
|
getCurrentOrLastSession: () => currentOrLastSession,
|
|
getParticipant: (clientId) => participantsEverSeen[clientId],
|
|
setBackingTrack: setOpenBackingTrack,
|
|
getBackingTrack: () => openBackingTrack,
|
|
hasShownAudioMediaMixerHelp: () => shownAudioMediaMixerHelp,
|
|
markShownAudioMediaMixerHelp: () => setShownAudioMediaMixerHelp(true),
|
|
getMusicianAccess,
|
|
|
|
// Audio establishment
|
|
setAudioEstablished,
|
|
|
|
// Cleanup
|
|
ensureEnded,
|
|
|
|
// Constants
|
|
ALERT_TYPES,
|
|
EVENTS,
|
|
MIX_MODES,
|
|
|
|
// Functions from useSessionUtils
|
|
clearAudioTimeout,
|
|
FTUEPageEnter,
|
|
FTUEPageLeave,
|
|
SessionPageEnter,
|
|
SessionPageLeave,
|
|
setAutoOpenJamTrack,
|
|
grabAutoOpenJamTrack,
|
|
changeLatencyDataStructure,
|
|
scoreInfo,
|
|
createLatency,
|
|
joinSessionFromCustomUrlScheme,
|
|
LATENCY
|
|
};
|
|
}
|