jam-cloud/jam-ui/src/hooks/useSessionModel.js

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
};
}