From c64b4548f281c3e4edbc03104bed29216838891b Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 30 Sep 2025 00:27:48 +0530 Subject: [PATCH] wip --- jam-ui/.env.development | 3 +- jam-ui/.env.development.example | 3 +- jam-ui/.env.production | 1 + jam-ui/.env.staging | 1 + .../src/components/client/JKSessionScreen.js | 213 ++++----- jam-ui/src/context/GlobalContext.js | 17 + jam-ui/src/context/JamClientContext.js | 19 + jam-ui/src/helpers/MessageFactory.js | 228 ++++++++++ jam-ui/src/helpers/MessageFactoryExample.js | 38 ++ jam-ui/src/helpers/asyncJamClientExample.js | 60 --- jam-ui/src/helpers/globals.js | 391 +++++++++++++++++ .../{asyncJamClient.js => jamClientProxy.js} | 50 ++- jam-ui/src/helpers/rest.js | 29 +- jam-ui/src/helpers/utils.js | 107 +++-- jam-ui/src/hooks/useClientWebsocket.js | 44 -- jam-ui/src/hooks/useJamClientService.js | 405 ++++++++++++++++++ jam-ui/src/hooks/useJamWebSocketServer.js | 355 --------------- jam-ui/src/hooks/useStun.js | 71 +++ jam-ui/src/layouts/JKClientLayout.js | 5 +- jam-ui/src/layouts/JKClientRoutes.js | 2 - ...entWebsocket.js => fakeJamClientServer.js} | 16 +- .../lib/jam_websockets/router.rb | 1 - 22 files changed, 1384 insertions(+), 675 deletions(-) create mode 100644 jam-ui/src/context/GlobalContext.js create mode 100644 jam-ui/src/context/JamClientContext.js create mode 100644 jam-ui/src/helpers/MessageFactory.js create mode 100644 jam-ui/src/helpers/MessageFactoryExample.js delete mode 100644 jam-ui/src/helpers/asyncJamClientExample.js create mode 100644 jam-ui/src/helpers/globals.js rename jam-ui/src/helpers/{asyncJamClient.js => jamClientProxy.js} (93%) delete mode 100644 jam-ui/src/hooks/useClientWebsocket.js create mode 100644 jam-ui/src/hooks/useJamClientService.js delete mode 100644 jam-ui/src/hooks/useJamWebSocketServer.js create mode 100644 jam-ui/src/hooks/useStun.js rename jam-ui/src/websockets/{fakeClientWebsocket.js => fakeJamClientServer.js} (98%) diff --git a/jam-ui/.env.development b/jam-ui/.env.development index f176ddd74..c48dd4fdc 100644 --- a/jam-ui/.env.development +++ b/jam-ui/.env.development @@ -13,4 +13,5 @@ REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4 PUBLIC_URL= REACT_APP_COOKIE_DOMAIN=.jamkazam.local REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-hvDV1xQxDw0HPaaRFP4KNE -REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms \ No newline at end of file +REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms +REACT_APP_WEBSOCKET_GATEWAY_URL=ws://localhost:6767 \ No newline at end of file diff --git a/jam-ui/.env.development.example b/jam-ui/.env.development.example index b68094efc..764a05d49 100644 --- a/jam-ui/.env.development.example +++ b/jam-ui/.env.development.example @@ -10,4 +10,5 @@ REACT_APP_ENV=development REACT_APP_COOKIE_DOMAIN=.jamkazam.com REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4 REACT_APP_RECURLY_PUBLIC_API_KEY= -REACT_APP_BRAINTREE_TOKEN= \ No newline at end of file +REACT_APP_BRAINTREE_TOKEN= +REACT_APP_WEBSOCKET_GATEWAY_URL= \ No newline at end of file diff --git a/jam-ui/.env.production b/jam-ui/.env.production index 928e98138..3d95e5ef0 100644 --- a/jam-ui/.env.production +++ b/jam-ui/.env.production @@ -11,3 +11,4 @@ REACT_APP_COOKIE_DOMAIN=.jamkazam.com REACT_APP_GOOGLE_ANALYTICS_ID=G-SPTNJRW7WB REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-hvDV1xQxDw0HPaaRFP4KNE REACT_APP_BRAINTREE_TOKEN=production_hc7z69yq_pwwc6zm3d478kfrh +REACT_APP_WEBSOCKET_GATEWAY_URL= \ No newline at end of file diff --git a/jam-ui/.env.staging b/jam-ui/.env.staging index dac5dbd88..7efd399dc 100644 --- a/jam-ui/.env.staging +++ b/jam-ui/.env.staging @@ -11,3 +11,4 @@ REACT_APP_COOKIE_DOMAIN=.staging.jamkazam.com REACT_APP_GOOGLE_ANALYTICS_ID=G-8W0GTL53NT REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-AjUHUfcLtIsPdtetD4mj2x REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms +REACT_APP_WEBSOCKET_GATEWAY_URL= \ No newline at end of file diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js index fa6f7d517..f22c4bf14 100644 --- a/jam-ui/src/components/client/JKSessionScreen.js +++ b/jam-ui/src/components/client/JKSessionScreen.js @@ -2,9 +2,8 @@ import React, { useEffect, useContext, useState } from 'react' import { Alert, Col, Row, Button, Card, CardBody, Modal, ModalHeader, ModalBody, ModalFooter, CardHeader, Badge } from 'reactstrap'; import FalconCardHeader from '../common/FalconCardHeader'; -import useJamWebSocketClient, { ConnectionStatus } from '../../hooks/useJamWebSocketClient' -import AppContext from '../../context/Context'; -import { getPageName } from '../../helpers/utils'; +import useJamClientService, { ConnectionStatus } from '../../hooks/useJamClientService' +import { useJamClient } from '../../context/JamClientContext'; const JKSessionScreen = () => { const { @@ -13,16 +12,8 @@ const JKSessionScreen = () => { reconnectAttempts, lastError, jamClient, - mixers, - participants, - isRecording, - isPlaying, - currentPosition - } = useJamWebSocketClient(); - const { isFluid, isVertical, navbarStyle } = useContext(AppContext); - const isKanban = getPageName('kanban'); + } = useJamClientService(process.env.REACT_APP_WEBSOCKET_GATEWAY_URL); - const [isBackendClientReady, setIsBackendClientReady] = useState(false); const [sessionState, setSessionState] = useState({ sessionId: 'session_12345', participants: [], @@ -34,7 +25,7 @@ const JKSessionScreen = () => { // Monitor connection status changes useEffect(() => { if (connectionStatus === ConnectionStatus.DISCONNECTED || - connectionStatus === ConnectionStatus.ERROR) { + connectionStatus === ConnectionStatus.ERROR) { setShowConnectionAlert(true); } else if (connectionStatus === ConnectionStatus.CONNECTED) { setShowConnectionAlert(false); @@ -42,25 +33,25 @@ const JKSessionScreen = () => { }, [connectionStatus]); useEffect(() => { - if (isConnected) { - setIsBackendClientReady(true); + if (!isConnected) return; - // Initialize session callbacks - jamClient.SessionRegisterCallback("HandleSessionCallback"); - jamClient.RegisterRecordingCallbacks( - "HandleRecordingStartResult", - "HandleRecordingStopResult", - "HandleRecordingStarted", - "HandleRecordingStopped", - "HandleRecordingAborted" - ); - jamClient.SessionSetConnectionStatusRefreshRate(1000); - jamClient.RegisterVolChangeCallBack("HandleVolumeChangeCallback"); - jamClient.setMetronomeOpenCallback("HandleMetronomeCallback"); + // Initialize session callbacks + console.log("__DEBUG__", jamClient) + //jamClient.SessionRegisterCallback("HandleSessionCallback"); + // jamClient.RegisterRecordingCallbacks( + // "HandleRecordingStartResult", + // "HandleRecordingStopResult", + // "HandleRecordingStarted", + // "HandleRecordingStopped", + // "HandleRecordingAborted" + // ); + // jamClient.SessionSetConnectionStatusRefreshRate(1000); + // jamClient.RegisterVolChangeCallBack("HandleVolumeChangeCallback"); + // jamClient.setMetronomeOpenCallback("HandleMetronomeCallback"); + + // Load initial session data + //loadSessionData(); - // Load initial session data - loadSessionData(); - } }, [isConnected]); const loadSessionData = async () => { @@ -68,108 +59,65 @@ const JKSessionScreen = () => { const audioConfigs = await jamClient.FTUEGetAllAudioConfigurations(); const controlState = await jamClient.SessionGetAllControlState(true); const sampleRate = await jamClient.GetSampleRate(); - + console.log('Session data loaded:', { audioConfigs, controlState, sampleRate }); } catch (error) { console.error('Error loading session data:', error); } }; - const handleStartRecording = () => { - jamClient.StartRecording({ recordingId: `rec_${Date.now()}` }); - }; + // const handleStartRecording = () => { + // jamClient.StartRecording({ recordingId: `rec_${Date.now()}` }); + // }; - const handleStopRecording = () => { - jamClient.StopRecording({}); - }; + // const handleStopRecording = () => { + // jamClient.StopRecording({}); + // }; - const handlePlayPause = () => { - if (isPlaying) { - jamClient.SessionPausePlay(); - } else { - jamClient.SessionStartPlay(); - } - }; + // const handlePlayPause = () => { + // if (isPlaying) { + // jamClient.SessionPausePlay(); + // } else { + // jamClient.SessionStartPlay(); + // } + // }; - const handleStopPlayback = () => { - jamClient.SessionStopPlay(); - }; + // const handleStopPlayback = () => { + // jamClient.SessionStopPlay(); + // }; - // Callback handlers (these would be implemented to handle WebSocket responses) - const HandleSessionCallback = (data) => { - console.log('Session callback:', data); - // Handle session events - }; + // // Callback handlers (these would be implemented to handle WebSocket responses) + // const HandleSessionCallback = (data) => { + // console.log('Session callback:', data); + // // Handle session events + // }; - const HandleRecordingStarted = (data) => { - console.log('Recording started:', data); - // Update recording state - }; + // const HandleRecordingStarted = (data) => { + // console.log('Recording started:', data); + // // Update recording state + // }; - const HandleRecordingStopped = (data) => { - console.log('Recording stopped:', data); - // Update recording state - }; + // const HandleRecordingStopped = (data) => { + // console.log('Recording stopped:', data); + // // Update recording state + // }; - const HandleVolumeChangeCallback = (mixerId, isLeft, value, isMuted) => { - console.log('Volume changed:', { mixerId, isLeft, value, isMuted }); - // Update mixer state - }; + // const HandleVolumeChangeCallback = (mixerId, isLeft, value, isMuted) => { + // console.log('Volume changed:', { mixerId, isLeft, value, isMuted }); + // // Update mixer state + // }; - const HandleBridgeCallback = (vuData) => { - console.log('Bridge callback:', vuData); - // Handle VU meter updates - }; + // const HandleBridgeCallback = (vuData) => { + // console.log('Bridge callback:', vuData); + // // Handle VU meter updates + // }; return ( - { !isConnected &&
Connecting to backend...
} + {!isConnected &&
Connecting to backend...
} - - - - - - +
@@ -184,13 +132,13 @@ const JKSessionScreen = () => {
- +
-
Audio Inputs ({participants.length} participants)
+
Audio Inputs
- {participants.map((participant, index) => ( + {/* {participants.map((participant, index) => (
{participant.name}
@@ -200,14 +148,14 @@ const JKSessionScreen = () => {
Controls
- ))} + ))} */}
- +
-
Session Mix ({mixers.length} mixers)
+
Session Mix
- {mixers.slice(0, 4).map((mixer, index) => ( + {/* {mixers.slice(0, 4).map((mixer, index) => (
Mixer {index + 1}
@@ -217,10 +165,10 @@ const JKSessionScreen = () => {
Type: {mixer.group_id}
- ))} + ))} */}
- +
Attach Media
@@ -231,14 +179,14 @@ const JKSessionScreen = () => {
- + {/* Connection Status Alerts */} {showConnectionAlert && (
@@ -285,9 +233,9 @@ const JKSessionScreen = () => { @@ -314,17 +262,8 @@ const JKSessionScreen = () => {
Status: {connectionStatus}
-
- Recording: {isRecording ? '🔴 Recording' : '⏹️ Not Recording'} -
-
- Playback: {isPlaying ? '▶️ Playing' : '⏸️ Paused'} -
-
- Position: {Math.floor(currentPosition / 1000)}s -
Reconnect Attempts: {reconnectAttempts}
diff --git a/jam-ui/src/context/GlobalContext.js b/jam-ui/src/context/GlobalContext.js new file mode 100644 index 000000000..702cd5f6e --- /dev/null +++ b/jam-ui/src/context/GlobalContext.js @@ -0,0 +1,17 @@ +import React, { createContext } from 'react'; +import { useStun } from '../hooks/useStun'; + + +// Create a global context +export const GlobalContext = createContext({}); + +export const GlobalProvider = ({ children }) => { + // Add global state or functions here as needed + + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/jam-ui/src/context/JamClientContext.js b/jam-ui/src/context/JamClientContext.js new file mode 100644 index 000000000..a83976ab1 --- /dev/null +++ b/jam-ui/src/context/JamClientContext.js @@ -0,0 +1,19 @@ +import React, { createContext, useContext, useRef } from 'react'; +import JamClientProxy from '../helpers/jamClientProxy'; + +const JamClientContext = createContext(null); + +export const JamClientProvider = ({ children }) => { + //assign an instance of JamClientProxy to a ref so that it persists across renders + const proxyRef = useRef(null); + const proxy = new JamClientProxy(null, console); // Pass appropriate parameters + proxyRef.current = proxy.init(); + return ( + + {children} + + ); +}; + +export const useJamClient = () => useContext(JamClientContext); + \ No newline at end of file diff --git a/jam-ui/src/helpers/MessageFactory.js b/jam-ui/src/helpers/MessageFactory.js new file mode 100644 index 000000000..e0fd51ea7 --- /dev/null +++ b/jam-ui/src/helpers/MessageFactory.js @@ -0,0 +1,228 @@ +/** + * Modern JavaScript Message Factory for JamKazam WebSocket Communication + * Based on the legacy AAB_message_factory.js, updated for ES6+ and React compatibility + */ + +// Message types for WebSocket communication +export const MessageType = { + LOGIN: "LOGIN", + LOGIN_ACK: "LOGIN_ACK", + LOGIN_MUSIC_SESSION: "LOGIN_MUSIC_SESSION", + LOGIN_MUSIC_SESSION_ACK: "LOGIN_MUSIC_SESSION_ACK", + LEAVE_MUSIC_SESSION: "LEAVE_MUSIC_SESSION", + LEAVE_MUSIC_SESSION_ACK: "LEAVE_MUSIC_SESSION_ACK", + HEARTBEAT: "HEARTBEAT", + HEARTBEAT_ACK: "HEARTBEAT_ACK", + SUBSCRIBE: "SUBSCRIBE", + UNSUBSCRIBE: "UNSUBSCRIBE", + SUBSCRIPTION_MESSAGE: "SUBSCRIPTION_MESSAGE", + SUBSCRIBE_BULK: "SUBSCRIBE_BULK", + USER_STATUS: "USER_STATUS", + + // Friend notifications + FRIEND_UPDATE: "FRIEND_UPDATE", + FRIEND_REQUEST: "FRIEND_REQUEST", + FRIEND_REQUEST_ACCEPTED: "FRIEND_REQUEST_ACCEPTED", + FRIEND_SESSION_JOIN: "FRIEND_SESSION_JOIN", + NEW_USER_FOLLOWER: "NEW_USER_FOLLOWER", + NEW_BAND_FOLLOWER: "NEW_BAND_FOLLOWER", + + // Session notifications + SESSION_INVITATION: "SESSION_INVITATION", + SESSION_ENDED: "SESSION_ENDED", + JOIN_REQUEST: "JOIN_REQUEST", + JOIN_REQUEST_APPROVED: "JOIN_REQUEST_APPROVED", + JOIN_REQUEST_REJECTED: "JOIN_REQUEST_REJECTED", + SESSION_JOIN: "SESSION_JOIN", + SESSION_DEPART: "SESSION_DEPART", + TRACKS_CHANGED: "TRACKS_CHANGED", + MUSICIAN_SESSION_JOIN: "MUSICIAN_SESSION_JOIN", + BAND_SESSION_JOIN: "BAND_SESSION_JOIN", + SCHEDULED_SESSION_INVITATION: "SCHEDULED_SESSION_INVITATION", + SCHEDULED_SESSION_RSVP: "SCHEDULED_SESSION_RSVP", + SCHEDULED_SESSION_RSVP_APPROVED: "SCHEDULED_SESSION_RSVP_APPROVED", + SCHEDULED_SESSION_RSVP_CANCELLED: "SCHEDULED_SESSION_RSVP_CANCELLED", + SCHEDULED_SESSION_RSVP_CANCELLED_ORG: "SCHEDULED_SESSION_RSVP_CANCELLED_ORG", + SCHEDULED_SESSION_CANCELLED: "SCHEDULED_SESSION_CANCELLED", + SCHEDULED_SESSION_RESCHEDULED: "SCHEDULED_SESSION_RESCHEDULED", + SCHEDULED_SESSION_REMINDER: "SCHEDULED_SESSION_REMINDER", + SCHEDULED_SESSION_COMMENT: "SCHEDULED_SESSION_COMMENT", + + SCHEDULED_JAMCLASS_INVITATION: "SCHEDULED_JAMCLASS_INVITATION", + LESSON_MESSAGE: "LESSON_MESSAGE", + + // Recording notifications + MUSICIAN_RECORDING_SAVED: "MUSICIAN_RECORDING_SAVED", + BAND_RECORDING_SAVED: "BAND_RECORDING_SAVED", + RECORDING_STARTED: "RECORDING_STARTED", + RECORDING_ENDED: "RECORDING_ENDED", + RECORDING_MASTER_MIX_COMPLETE: "RECORDING_MASTER_MIX_COMPLETE", + DOWNLOAD_AVAILABLE: "DOWNLOAD_AVAILABLE", + RECORDING_STREAM_MIX_COMPLETE: "RECORDING_STREAM_MIX_COMPLETE", + + // Band notifications + BAND_INVITATION: "BAND_INVITATION", + BAND_INVITATION_ACCEPTED: "BAND_INVITATION_ACCEPTED", + + // Text message + TEXT_MESSAGE: "TEXT_MESSAGE", + CHAT_MESSAGE: "CHAT_MESSAGE", + SEND_CHAT_MESSAGE: "SEND_CHAT_MESSAGE", + + // Broadcast notifications + SOURCE_UP_REQUESTED: "SOURCE_UP_REQUESTED", + SOURCE_DOWN_REQUESTED: "SOURCE_DOWN_REQUESTED", + SOURCE_UP: "SOURCE_UP", + SOURCE_DOWN: "SOURCE_DOWN", + + TEST_SESSION_MESSAGE: "TEST_SESSION_MESSAGE", + PING_REQUEST: "PING_REQUEST", + PING_ACK: "PING_ACK", + PEER_MESSAGE: "PEER_MESSAGE", + CLIENT_UPDATE: "CLIENT_UPDATE", + GENERIC_MESSAGE: "GENERIC_MESSAGE", + RELOAD: "RELOAD", + RESTART_APPLICATION: "RESTART_APPLICATION", + STOP_APPLICATION: "STOP_APPLICATION", + SERVER_BAD_STATE_RECOVERED: "SERVER_BAD_STATE_RECOVERED", + SERVER_GENERIC_ERROR: "SERVER_GENERIC_ERROR", + SERVER_REJECTION_ERROR: "SERVER_REJECTION_ERROR", + SERVER_BAD_STATE_ERROR: "SERVER_BAD_STATE_ERROR", + SERVER_DUPLICATE_CLIENT_ERROR: "SERVER_DUPLICATE_CLIENT_ERROR" +}; + +// Route prefixes for message routing +export const RouteToPrefix = { + CLIENT: "client", + SESSION: "session", + SERVER: "server", + USER: "user" +}; + +// Utility function to get a simple cookie value (replacement for $.cookie) +const getCookie = (name) => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return null; +}; + +// Helper function to create client container +const createClientContainer = (type, target, inner) => { + const typeField = type.toLowerCase(); + return { + type, + route_to: target, + [typeField]: inner + }; +}; + +// Helper function to create route to client +const routeToClient = (clientId) => `${RouteToPrefix.CLIENT}:${clientId}`; + +// Helper function to create route to session +const routeToSession = (sessionId) => `${RouteToPrefix.SESSION}:${sessionId}`; + +// Message Factory class +class MessageFactory { + // Ping the provided client_id + ping(clientId) { + const data = {}; + return createClientContainer(MessageType.PING_REQUEST, routeToClient(clientId), data); + } + + // Heartbeat message + heartbeat(lastNotificationSeen, lastNotificationSeenAt, active) { + const data = { + notification_seen: lastNotificationSeen, + notification_seen_at: lastNotificationSeenAt, + active + }; + return createClientContainer(MessageType.HEARTBEAT, RouteToPrefix.SERVER, data); + } + + // User Status update message + userStatus(active, status) { + const data = { active, status }; + return createClientContainer(MessageType.USER_STATUS, RouteToPrefix.SERVER, data); + } + + // Chat message (fixed implementation from original) + chatMessage(channel, message) { + const data = { channel, msg: message }; + return createClientContainer(MessageType.SEND_CHAT_MESSAGE, RouteToPrefix.SERVER, data); + } + + // Create a login message using user/pass + loginWithUserPass(username, password) { + const login = { username, password }; + return createClientContainer(MessageType.LOGIN, RouteToPrefix.SERVER, login); + } + + // Create a login message using token + // reconnect_music_session_id is optional + loginWithToken(token, reconnectMusicSessionId, clientType) { + const login = { + token, + client_id: getCookie("client_id"), + client_type: clientType + }; + if (reconnectMusicSessionId) { + login.reconnect_music_session_id = reconnectMusicSessionId; + } + return createClientContainer(MessageType.LOGIN, RouteToPrefix.SERVER, login); + } + + // Logout message + logout() { + const logout = {}; + return createClientContainer("LOGOUT", RouteToPrefix.SERVER, logout); + } + + // Create a login message using only the client_id (for latency_tester) + loginWithClientId(clientId, clientType) { + if (clientType !== 'latency_tester') { + throw new Error("client_type must be latency_tester in loginWithClientId"); + } + const login = { client_id: clientId, client_type: clientType }; + return createClientContainer(MessageType.LOGIN, RouteToPrefix.SERVER, login); + } + + // Create a music session login message + loginMusicSession(musicSession) { + const loginMusicSession = { music_session: musicSession }; + return createClientContainer(MessageType.LOGIN_MUSIC_SESSION, RouteToPrefix.SERVER, loginMusicSession); + } + + // Client-to-client message + clientP2PMessage(senderClientId, receiverClientId, message) { + const peerMessage = { message }; + const result = createClientContainer(MessageType.PEER_MESSAGE, routeToClient(receiverClientId), peerMessage); + result.from = senderClientId; + return result; + } + + // Subscribe to notifications + subscribe(type, id) { + const subscribeMsg = { type, id }; + return createClientContainer(MessageType.SUBSCRIBE, RouteToPrefix.SERVER, subscribeMsg); + } + + // Subscribe to multiple types/ids + subscribeBulk(types, ids) { + const subscribeBulkMsg = { types, ids }; + return createClientContainer(MessageType.SUBSCRIBE_BULK, RouteToPrefix.SERVER, subscribeBulkMsg); + } + + // Unsubscribe from notifications + unsubscribe(type, id) { + const unsubscribeMsg = { type, id }; + return createClientContainer(MessageType.UNSUBSCRIBE, RouteToPrefix.SERVER, unsubscribeMsg); + } +} + +// Export a singleton instance for easy use +export const messageFactory = new MessageFactory(); + +// Also export the class for custom instantiation if needed +export default MessageFactory; diff --git a/jam-ui/src/helpers/MessageFactoryExample.js b/jam-ui/src/helpers/MessageFactoryExample.js new file mode 100644 index 000000000..0bb9a1e9f --- /dev/null +++ b/jam-ui/src/helpers/MessageFactoryExample.js @@ -0,0 +1,38 @@ +/** + * Example usage of the JKMessageFactory in a React component + * This demonstrates how to import and use the modern message factory + */ + +import React, { useEffect } from 'react'; +import { messageFactory, MessageType } from './MessageFactory'; + +// Example React component using the message factory +const MessageFactoryExample = () => { + useEffect(() => { + // Example: Create a heartbeat message + const heartbeatMessage = messageFactory.heartbeat(123, '2023-01-01T00:00:00Z', true); + console.log('Heartbeat message:', heartbeatMessage); + + // Example: Create a login message with token + const loginMessage = messageFactory.loginWithToken('some-token', null, 'web'); + console.log('Login message:', loginMessage); + + // Example: Create a chat message + const chatMessage = messageFactory.chatMessage('general', 'Hello world!'); + console.log('Chat message:', chatMessage); + + // Example: Subscribe to notifications + const subscribeMessage = messageFactory.subscribe('session', 456); + console.log('Subscribe message:', subscribeMessage); + }, []); + + return ( +
+

JKMessageFactory Example

+

Check the console for example message outputs.

+

The message factory is ready to be used in your React components for WebSocket communication.

+
+ ); +}; + +export default MessageFactoryExample; diff --git a/jam-ui/src/helpers/asyncJamClientExample.js b/jam-ui/src/helpers/asyncJamClientExample.js deleted file mode 100644 index 9588bafb6..000000000 --- a/jam-ui/src/helpers/asyncJamClientExample.js +++ /dev/null @@ -1,60 +0,0 @@ -// Example usage of the ported AsyncJamClient in a React application - -import React, { useState, useEffect } from 'react'; -import AsyncJamClient from './asyncJamClient'; - -// Example React hook for using AsyncJamClient -export const useAsyncJamClient = (logger, context = window) => { - const [asyncJamClient, setAsyncJamClient] = useState(null); - - useEffect(() => { - // Initialize the AsyncJamClient - const client = new AsyncJamClient(null, logger, context); - const proxy = client.init(); - setAsyncJamClient(proxy); - - // Cleanup on unmount - return () => { - // Add cleanup logic if needed - }; - }, [logger, context]); - - return asyncJamClient; -}; - -// Example React component using AsyncJamClient -const JamSessionComponent = () => { - const logger = console; // or your custom logger - const asyncJamClient = useAsyncJamClient(logger); - - const handleJoinSession = async () => { - if (asyncJamClient) { - try { - const result = await asyncJamClient.JoinSession('sessionId'); - console.log('Joined session:', result); - } catch (error) { - console.error('Failed to join session:', error); - } - } - }; - - const handleLeaveSession = async () => { - if (asyncJamClient) { - try { - await asyncJamClient.LeaveSession(); - console.log('Left session'); - } catch (error) { - console.error('Failed to leave session:', error); - } - } - }; - - return ( -
- - -
- ); -}; - -export default JamSessionComponent; diff --git a/jam-ui/src/helpers/globals.js b/jam-ui/src/helpers/globals.js new file mode 100644 index 000000000..e4b0cb9b8 --- /dev/null +++ b/jam-ui/src/helpers/globals.js @@ -0,0 +1,391 @@ +export const MIDI_TRACK = 100 + +export const CLIENT_ROLE = { + CHILD: 0, + PARENT: 1 +} + +export const OS = { + WIN32: "Win32", + OSX: "MacOSX", + UNIX: "Unix" +}; + +export const ASSIGNMENT = { + CHAT: -2, + OUTPUT: -1, + UNASSIGNED: 0, + TRACK1: 1, + TRACK2: 2 +}; + +export const VOICE_CHAT = { + NO_CHAT: "0", + CHAT: "1" +}; + +export const AVAILABILITY_US = "United States"; +export const MASTER_TRACK = "Master"; + +export const EVENTS = { + DIALOG_CLOSED: 'dialog_closed', + SHOW_SIGNUP: 'show_signup', + SHOW_SIGNIN: 'show_signin', + RSVP_SUBMITTED: 'rsvp_submitted', + RSVP_CANCELED: 'rsvp_canceled', + USER_UPDATED: 'user_updated', + SESSION_STARTED: 'session_started', + SESSION_ENDED: 'session_stopped', + FILE_MANAGER_CMD_START: 'file_manager_cmd_start', + FILE_MANAGER_CMD_STOP: 'file_manager_cmd_stop', + FILE_MANAGER_CMD_PROGRESS: 'file_manager_cmd_progress', + FILE_MANAGER_CMD_ASAP_UPDATE: 'file_manager_cmd_asap_update', + MIXER_MODE_CHANGED: 'mixer_mode_changed', + MUTE_SELECTED: 'mute_selected', + SUBSCRIBE_NOTIFICATION: 'subscribe_notification', + CONNECTION_UP: 'connection_up', + CONNECTION_DOWN: 'connection_down', + SCREEN_CHANGED: 'screen_changed', + JAMTRACK_DOWNLOADER_STATE_CHANGED: 'jamtrack_downloader_state', + METRONOME_PLAYBACK_MODE_SELECTED: 'metronome_playback_mode_selected', + CHECKOUT_SIGNED_IN: 'checkout_signed_in', + CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in', + PREVIEW_PLAYED: 'preview_played', + VST_OPERATION_SELECTED: 'vst_operation_selected', + VST_EFFECT_SELECTED: 'vst_effect_selected', + LESSON_SESSION_ACTION: 'lesson_session_action', + JAMBLASTER_ACTION: 'jamblaster_action' +}; + +export const PLAYBACK_MONITOR_MODE = { + MEDIA_FILE: 'MEDIA_FILE', + JAMTRACK: 'JAMTRACK', + METRONOME: 'METRONOME', + BROWSER_MEDIA: 'BROWSER_MEDIA' +} +export const ALERT_NAMES = { + NO_EVENT: 0, + BACKEND_ERROR: 1, //generic error - eg P2P message error + BACKEND_MIXER_CHANGE: 2, //event that controls have been regenerated + + //network related + PACKET_JTR: 3, + PACKET_LOSS: 4, + PACKET_LATE: 5, + + JTR_QUEUE_DEPTH: 6, + + NETWORK_JTR: 7, + NETWORK_PING: 8, + + BITRATE_THROTTLE_WARN: 9, + BANDWIDTH_LOW: 10, + + //IO related events + INPUT_IO_RATE: 11, + INPUT_IO_JTR: 12, + OUTPUT_IO_RATE: 13, + OUTPUT_IO_JTR: 14, + + // CPU load related + CPU_LOAD: 15, + DECODE_VIOLATIONS: 16, + + LAST_THRESHOLD: 17, + + WIFI_NETWORK_ALERT: 18, //user or peer is using wifi + NO_VALID_AUDIO_CONFIG: 19, // alert the user to popup a config + AUDIO_DEVICE_NOT_PRESENT: 20, // the audio device is not connected + RECORD_PLAYBACK_STATE: 21, // record/playback events have occurred + RUN_UPDATE_CHECK_BACKGROUND: 22, //this is auto check - do + RUN_UPDATE_CHECK_INTERACTIVE: 23, //this is initiated by user + STUN_EVENT: 24, // system completed stun test... come get the result + DEAD_USER_WARN_EVENT: 25, //the backend is not receiving audio from this peer + DEAD_USER_REMOVE_EVENT: 26, //the backend is removing the user from session as no audio is coming from this peer + WINDOW_CLOSE_BACKGROUND_MODE: 27, //the user has closed the window and the client is now in background mode + WINDOW_OPEN_FOREGROUND_MODE: 28, //the user has opened the window and the client is now in forground mode/ + SESSION_LIVEBROADCAST_FAIL: 29, //error of some sort - so can't broadcast + SESSION_LIVEBROADCAST_ACTIVE: 30, //active + SESSION_LIVEBROADCAST_STOPPED: 31, //stopped by server/user + SESSION_LIVEBROADCAST_PINNED: 32, //node pinned by user + SESSION_LIVEBROADCAST_UNPINNED: 33, //node unpinned by user + BACKEND_STATUS_MSG: 34, //status/informational message + LOCAL_NETWORK_VARIANCE_HIGH: 35,//the ping time via a hairpin for the user network is unnaturally high or variable. + //indicates problem with user computer stack or network itself (wifi, antivirus etc) + LOCAL_NETWORK_LATENCY_HIGH: 36, + RECORDING_CLOSE: 37, //update and remove tracks from front-end + PEER_REPORTS_NO_AUDIO_RECV: 38, //letting front-end know audio is not being received from a user in session + SHOW_PREFERENCES: 39, // tell frontend to show preferences dialog + USB_CONNECTED: 40, // tell frontend that a USB device was connected + USB_DISCONNECTED: 41, // tell frontend that a USB device was disconnected + JAM_TRACK_SERVER_ERROR: 42, //error talking with server + BAD_INTERVAL_RATE: 43, //the audio gear is calling back at rate that does not match the expected interval + FIRST_AUDIO_PACKET: 44,// we are receiving audio from peer + NETWORK_PORT_MANGLED: 45, // packet from peer indicates network port is being mangled + NO_GLOBAL_CLOCK_SERVER: 46, //can't reach global clock NTP server + GLOBAL_CLOCK_SYNCED: 47, //clock synced + RECORDING_DONE: 48, //the recording writer thread is done + VIDEO_WINDOW_OPENED: 49, //video window opened + VIDEO_WINDOW_CLOSED: 50, + VST_CHANGED: 51, // VST state changed + SAMPLERATE_CONFIGURATION_BAD: 52, + SHOW_NETWORK_TEST: 53, + LAST_ALERT: 54 +} +// recreate eThresholdType enum from MixerDialog.h +export const ALERT_TYPES = { + 0: { "title": "", "message": "" }, // NO_EVENT, + 1: { "title": "", "message": "" }, // BACKEND_ERROR: generic error - eg P2P message error + 2: { "title": "", "message": "" }, // BACKEND_MIXER_CHANGE, - event that controls have been regenerated + 3: { "title": "High Packet Jitter", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // PACKET_JTR, + 4: { "title": "High Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // PACKET_LOSS + 5: { "title": "High Packet Late", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // PACKET_LATE, + 6: { "title": "Large Jitter Queue", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // JTR_QUEUE_DEPTH, + 7: { "title": "High Network Jitter", "message": "Your network connection is currently experiencing network jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // NETWORK_JTR, + 8: { "title": "High Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // NETWORK_PING, + 9: { "title": "Bandwidth Throttled", "message": "The available bandwidth on your network has diminished, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // BITRATE_THROTTLE_WARN, + 10: { "title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // BANDWIDTH_LOW + + // IO related events + 11: { "title": "Variable Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // INPUT_IO_RATE + 12: { "title": "High Input Jitter", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // INPUT_IO_JTR, + 13: { "title": "Variable Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // OUTPUT_IO_RATE + 14: { "title": "High Output Jitter", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // OUTPUT_IO_JTR, + + // CPU load related + 15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // CPU_LOAD + 16: { "title": "Decode Violations", "message": "" }, // DECODE_VIOLATIONS, + 17: { "title": "", "message": "" }, // LAST_THRESHOLD + 18: { "title": "Wifi Alert", "message": "" }, // WIFI_NETWORK_ALERT, //user or peer is using wifi + 19: { "title": "No Audio Configuration", "message": "You cannot join the session because you do not have a valid audio configuration." }, // NO_VALID_AUDIO_CONFIG, + 20: { "title": "", "message": "" }, // AUDIO_DEVICE_NOT_PRESENT, // the audio device is not connected + //20: {"title": "Audio Device Not Present", "message": ""}, // AUDIO_DEVICE_NOT_PRESENT, // the audio device is not connected + 21: { "title": "", "message": "" }, // RECORD_PLAYBACK_STATE, // record/playback events have occurred + 22: { "title": "", "message": "" }, // RUN_UPDATE_CHECK_BACKGROUND, //this is auto check - do + 23: { "title": "", "message": "" }, // RUN_UPDATE_CHECK_INTERACTIVE, //this is initiated by user + 24: { "title": "", "message": "" }, // STUN_EVENT, // system completed stun test... come get the result + 25: { "title": "No Audio", "message": "Your system is no longer transmitting audio. Other session members are unable to hear you." }, // DEAD_USER_WARN_EVENT, //the backend is not receiving audio from this peer + 26: { "title": "No Audio", "message": "Your system is no longer transmitting audio. Other session members are unable to hear you." }, // DEAD_USER_REMOVE_EVENT, //the backend is removing the user from session as no audio is coming from this peer + 27: { "title": "", "message": "" }, // WINDOW_CLOSE_BACKGROUND_MODE, //the user has closed the window and the client is now in background mode + 28: { "title": "", "message": "" }, // WINDOW_OPEN_FOREGROUND_MODE, //the user has opened the window and the client is now in forground mode/ + + 29: { "title": "Failed to Broadcast", "message": "" }, // SESSION_LIVEBROADCAST_FAIL, //error of some sort - so can't broadcast + 30: { "title": "", "message": "" }, // SESSION_LIVEBROADCAST_ACTIVE, //active + 31: { "title": "", "message": "" }, // SESSION_LIVEBROADCAST_STOPPED, //stopped by server/user + 32: { "title": "Client Pinned", "message": "This client will be the source of a broadcast." }, // SESSION_LIVEBROADCAST_PINNED, //node pinned by user + 33: { "title": "Client No Longer Pinned", "message": "This client is no longer designated as the source of the broadcast." }, // SESSION_LIVEBROADCAST_UNPINNED, //node unpinned by user + + 34: { "title": "", "message": "" }, // BACKEND_STATUS_MSG, //status/informational message + 35: { "title": "LAN Unpredictable", "message": "Your local network is adding considerable variance to transmit times. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // LOCAL_NETWORK_VARIANCE_HIGH,//the ping time via a hairpin for the user network is unnaturally high or variable. + + //indicates problem with user computer stack or network itself (wifi, antivirus etc) + 36: { "title": "LAN High Latency", "message": "Your local network is adding considerable latency. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // LOCAL_NETWORK_LATENCY_HIGH, + 37: { "title": "", "message": "" }, // RECORDING_CLOSE, //update and remove tracks from front-end + 38: { "title": "No Audio Sent", "message": "" }, // PEER_REPORTS_NO_AUDIO_RECV, //update and remove tracks from front-end + 39: { "title": "", "message": "" }, // SHOW_PREFERENCES, //show preferences dialog + 40: { "title": "", "message": "" }, // USB_CONNECTED + 41: { "title": "", "message": "" }, // USB_DISCONNECTED, // tell frontend that a USB device was disconnected + 42: { "title": "", "message": "" }, // JAM_TRACK_SERVER_ERROR + 43: { "title": "", "message": "" }, // BAD_INTERVAL_RATE + 44: { "title": "", "message": "" }, // FIRST_AUDIO_PACKET + 45: { "title": "", "message": "" }, // NETWORK_PORT_MANGLED + 46: { "title": "", "message": "" }, // NO_GLOBAL_CLOCK_SERVER + 47: { "title": "", "message": "" }, // GLOBAL_CLOCK_SYNCED + 48: { "title": "", "message": "" }, // RECORDING_DONE + 49: { "title": "", "message": "" }, // VIDEO_WINDOW_OPENED + 50: { "title": "", "message": "" }, // VIDEO_WINDOW_CLOSED + 51: { "title": "", "message": "" }, // VST_CHANGED + 52: { "title": "", "message": "" }, // SAMPLERATE_CONFIGURATION_BAD + 53: { "title": "", "message": "" }, // SHOW_NETWORK_TEST + 54: { "title": "", "message": "" } // LAST_ALERT +}; + + +export const MAX_TRACKS = 6; +export const MAX_OUTPUTS = 2; + +// TODO: store these client_id values in instruments table, or store +// server_id as the client_id to prevent maintenance nightmares. As it's +// set up now, we will have to deploy each time we add new instruments. +export const server_to_client_instrument_map = { + "Acoustic Guitar": { "client_id": 10, "server_id": "acoustic guitar" }, + "Bass Guitar": { "client_id": 20, "server_id": "bass guitar" }, + "Computer": { "client_id": 30, "server_id": "computer" }, + "Drums": { "client_id": 40, "server_id": "drums" }, + "Percussion": { "client_id": 41, "server_id": "percussion" }, + "Electric Guitar": { "client_id": 50, "server_id": "electric guitar" }, + "Keyboard": { "client_id": 60, "server_id": "keyboard" }, + "Piano": { "client_id": 61, "server_id": "piano" }, + "Double Bass": { "client_id": 62, "server_id": "double bass" }, + "Voice": { "client_id": 70, "server_id": "voice" }, + "Flute": { "client_id": 80, "server_id": "flute" }, + "Clarinet": { "client_id": 90, "server_id": "clarinet" }, + "Saxophone": { "client_id": 100, "server_id": "saxophone" }, + "Trumpet": { "client_id": 110, "server_id": "trumpet" }, + "Violin": { "client_id": 120, "server_id": "violin" }, + "Trombone": { "client_id": 130, "server_id": "trombone" }, + "Banjo": { "client_id": 140, "server_id": "banjo" }, + "Harmonica": { "client_id": 150, "server_id": "harmonica" }, + "Accordion": { "client_id": 160, "server_id": "accordion" }, + "French Horn": { "client_id": 170, "server_id": "french horn" }, + "Euphonium": { "client_id": 180, "server_id": "euphonium" }, + "Tuba": { "client_id": 190, "server_id": "tuba" }, + "Oboe": { "client_id": 200, "server_id": "oboe" }, + "Ukulele": { "client_id": 210, "server_id": "ukulele" }, + "Cello": { "client_id": 220, "server_id": "cello" }, + "Viola": { "client_id": 230, "server_id": "viola" }, + "Mandolin": { "client_id": 240, "server_id": "mandolin" }, + "Other": { "client_id": 250, "server_id": "other" } +}; + + + + +export const client_to_server_instrument_map = { + 10: { "server_id": "acoustic guitar" }, + 20: { "server_id": "bass guitar" }, + 30: { "server_id": "computer" }, + 40: { "server_id": "drums" }, + 41: { "server_id": "percussion" }, + 50: { "server_id": "electric guitar" }, + 60: { "server_id": "keyboard" }, + 61: { "server_id": "piano" }, + 62: { "server_id": "double bass" }, + 70: { "server_id": "voice" }, + 80: { "server_id": "flute" }, + 90: { "server_id": "clarinet" }, + 100: { "server_id": "saxophone" }, + 110: { "server_id": "trumpet" }, + 120: { "server_id": "violin" }, + 130: { "server_id": "trombone" }, + 140: { "server_id": "banjo" }, + 150: { "server_id": "harmonica" }, + 160: { "server_id": "accordion" }, + 170: { "server_id": "french horn" }, + 180: { "server_id": "euphonium" }, + 190: { "server_id": "tuba" }, + 200: { "server_id": "oboe" }, + 210: { "server_id": "ukulele" }, + 220: { "server_id": "cello" }, + 230: { "server_id": "viola" }, + 240: { "server_id": "mandolin" }, + 250: { "server_id": "other" } +}; + +export const entityToPrintable = { + music_session: "music session", + slot: "Requested time" +} + +export const AUDIO_DEVICE_BEHAVIOR = { + MacOSX_builtin: { + display: 'MacOSX Built-In', + shortName: 'Built-In', + videoURL: "https://www.youtube.com/watch?v=7-9PW50ygHk", + showKnobs: true, + showASIO: false + }, + MacOSX_interface: { + display: 'MacOSX external interface', + shortName: 'External', + videoURL: "https://www.youtube.com/watch?v=7BLld6ogm14", + showKnobs: true, + showASIO: false + }, + Win32_wdm: { + display: 'Windows WDM', + shortName: 'WDM', + videoURL: "https://www.youtube.com/watch?v=L36UBkAV14c", + showKnobs: true, + showASIO: false + }, + Win32_asio: { + display: 'Windows ASIO', + shortName: 'ASIO', + videoURL: "https://www.youtube.com/watch?v=PGUmISTVVMY", + showKnobs: true, + showASIO: true + }, + Win32_asio4all: { + display: 'Windows ASIO4ALL', + shortName: 'ASIO4ALL', + videoURL: "https://www.youtube.com/watch?v=PGUmISTVVMY", + showKnobs: true, + showASIO: true + }, + Linux: { + display: 'Linux', + shortName: 'linux', + videoURL: undefined, + showKnobs: true, + showASIO: false + } +} + +export const MIX_MODES = { + MASTER: true, + PERSONAL: false +} + +/** NAMED_MESSAGES means messages that we show to the user (dialogs/banners/whatever), that we have formally named */ +export const NAMED_MESSAGES = { + MASTER_VS_PERSONAL_MIX: 'master_vs_personal_mix', + HOWTO_USE_VIDEO_NOSHOW: 'how-to-use-video', + CONFIGURE_VIDEO_NOSHOW: 'configure-video', + TEACHER_MUSICIAN_PROFILE: 'teacher-musician-profile' +} + +export const ChannelGroupIds = { + "MasterGroup": 0, + "MonitorGroup": 1, + "MasterCatGroup": 2, + "MonitorCatGroup": 3, + "AudioInputMusicGroup": 4, + "AudioInputChatGroup": 5, + "MediaTrackGroup": 6, + "StreamOutMusicGroup": 7, + "StreamOutChatGroup": 8, + "StreamOutMediaGroup": 9, + "UserMusicInputGroup": 10, + "UserChatInputGroup": 11, + "UserMediaInputGroup": 12, + "PeerAudioInputMusicGroup": 13, + "PeerMediaTrackGroup": 14, + "JamTrackGroup": 15, + "MetronomeGroup": 16, + "MidiInputMusicGroup": 17, + "PeerMidiInputMusicGroup": 18, + "UsbInputMusicGroup": 19, + "PeerUsbInputMusicGroup": 20 +}; + +export const ChannelGroupLookup = { + 0: "MasterGroup", + 1: "MonitorGroup", + 2: "MasterCatGroup", + 3: "MonitorCatGroup", + 4: "AudioInputMusicGroup", + 5: "AudioInputChatGroup", + 6: "MediaTrackGroup", + 7: "StreamOutMusicGroup", + 8: "StreamOutChatGroup", + 9: "StreamOutMediaGroup", + 10: "UserMusicInputGroup", + 11: "UserChatInputGroup", + 12: "UserMediaInputGroup", + 13: "PeerAudioInputMusicGroup", + 14: "PeerMediaTrackGroup", + 15: "JamTrackGroup", + 16: "MetronomeGroup", + 17: "MidiInputMusicGroup", + 18: "PeerMidiInputMusicGroup" +} +export const CategoryGroupIds = { + "AudioInputMusic": "AudioInputMusic", + "AudioInputChat": "AudioInputChat", + "UserMusic": "UserMusic", + "UserChat": "UserChat", + "UserMedia": "UserMedia", + "MediaTrack": "MediaTrack", + "Metronome": "Metronome" +} diff --git a/jam-ui/src/helpers/asyncJamClient.js b/jam-ui/src/helpers/jamClientProxy.js similarity index 93% rename from jam-ui/src/helpers/asyncJamClient.js rename to jam-ui/src/helpers/jamClientProxy.js index c2a234ff8..f23f3be9f 100644 --- a/jam-ui/src/helpers/asyncJamClient.js +++ b/jam-ui/src/helpers/jamClientProxy.js @@ -1,7 +1,7 @@ -// Modern ES6+ version of asyncJamClient.js for React compatibility -// Ported from web/app/assets/javascripts/asyncJamClient.js +// Modern ES6+ version of jamClientProxy.js for React compatibility +// Ported from web/app/assets/javascripts/jamClientProxy.js -import QWebChannel from 'qwebchannel' +import { QWebChannel } from 'qwebchannel' class Deferred { constructor(request_id) { @@ -13,7 +13,7 @@ class Deferred { } } -class AsyncJamClient { +class JamClientProxy { constructor(app, logger, context = window) { this.app = app; this.logger = logger; @@ -257,11 +257,9 @@ class AsyncJamClient { } setupWebSocketConnection() { - // Assuming QWebChannel is available globally or imported - // eslint-disable-next-line no-undef - if (typeof QWebChannel !== 'undefined') { - const socket = new WebSocket('ws://localhost:12345'); // Adjust URL as needed - + const socket = new WebSocket('ws://localhost:3060'); + socket.onopen = () => { + console.log("QWebChannel socket opened"); new QWebChannel(socket, channel => { this.jkfrontendchannel = channel.objects.jkfrontendchannel; @@ -277,6 +275,7 @@ class AsyncJamClient { if (deferred) deferred.resolve(null); } else { let msg = JSON.parse(message); + console.log("[jamClientProxy] Message received via QWebChannel: ", msg); let req_id = msg.request_id; let response = msg.response; let evt_id = msg.event_id; @@ -288,7 +287,7 @@ class AsyncJamClient { if (this.skipLogMethods.length > 0 && this.skipLogMethods.includes(methodName)) { // Skip logging } else if (this.displayLogMethod.includes(methodName)) { - this.logger.log('[asyncJamClient] Message received via QWebChannel: ', msg); + this.logger.log('[jamClientProxy] Message received via QWebChannel: ', msg); } deferred.resolve(response); @@ -299,17 +298,22 @@ class AsyncJamClient { } }); } catch (e) { - this.logger.log('[asyncJamClient] Error when receiving message via QWebChannel'); + this.logger.log('[jamClientProxy] Error when receiving message via QWebChannel'); if (deferred) { deferred.reject(e.message); deferred = null; } // Note: Bugsnag integration would need to be handled separately - console.error('asyncJamClient error:', e); + console.error('jamClientProxy error:', e); } } }); + } + + socket.onclose = () => { }; // Handle close if needed + + socket.onerror = error => { }; // Handle error if needed } handleEvent(evt_id, response) { @@ -318,13 +322,13 @@ class AsyncJamClient { switch (evt_id.toString()) { case '3006': // execute_script if (!response['execute_script'].match('HandleBridgeCallback2')) { - // this.logger.log(`[asyncJamClient] 3006 execute_script: ${response['execute_script']}`); + // this.logger.log(`[jamClientProxy] 3006 execute_script: ${response['execute_script']}`); } try { // eslint-disable-next-line no-eval eval(response['execute_script']); } catch (error) { - this.logger.log(`[asyncJamClient] error: execute_script: ${response['execute_script']}`); + this.logger.log(`[jamClientProxy] error: execute_script: ${response['execute_script']}`); this.logger.log(error); } break; @@ -337,13 +341,13 @@ class AsyncJamClient { this.context.JK.JamServer.sendP2PMessage(clientId, msg); } } catch (error) { - this.logger.log(`[asyncJamClient] error: sendP2PMessage: ${response['message']}`); + this.logger.log(`[jamClientProxy] error: sendP2PMessage: ${response['message']}`); this.logger.log(error); } break; case '3010': // JKVideoSession - this.logger.log(`[asyncJamClient] 3010 JKVideoSession: ${response['JKVideoSession']['connect']}`); + this.logger.log(`[jamClientProxy] 3010 JKVideoSession: ${response['JKVideoSession']['connect']}`); const vidConnect = response['JKVideoSession']['connect']; if (this.context.ExternalVideoActions) { this.context.ExternalVideoActions.setVideoEnabled(vidConnect); @@ -354,7 +358,7 @@ class AsyncJamClient { break; case '3011': // AudioFormatChangeEvent - this.logger.log(`[asyncJamClient] 3011 AudioFormatChangeEvent: ${response['AudioFormat']}`); + this.logger.log(`[jamClientProxy] 3011 AudioFormatChangeEvent: ${response['AudioFormat']}`); const audioFormat = response['AudioFormat']; if (this.context.RecordingActions) { this.context.RecordingActions.audioRecordingFormatChanged(`.${audioFormat}`); @@ -381,7 +385,7 @@ class AsyncJamClient { window.location.href = httpUrl; } } else { - this.logger.log(`[asyncJamClient] invalid customUrl: ${httpUrl}`); + this.logger.log(`[jamClientProxy] invalid customUrl: ${httpUrl}`); } } } @@ -436,7 +440,7 @@ class AsyncJamClient { if (this.skipLogMethods.length > 0 && this.skipLogMethods.includes(prop)) { // Skip logging } else if (this.displayLogMethod.includes(prop)) { - this.logger.log('[asyncJamClient] diverting to backend:', prop, appMessage); + this.logger.log('[jamClientProxy] diverting to backend:', prop, appMessage); } if (this.jkfrontendchannel) { @@ -445,12 +449,12 @@ class AsyncJamClient { this.deferredQueue.push(deferred); return deferred.promise; } catch (e) { - this.logger.error('[asyncJamClient] Native app not connected', e.message); + this.logger.error('[jamClientProxy] Native app not connected', e.message); deferred.reject('Native app not connected'); return deferred.promise; } } else { - this.logger.info('[asyncJamClient] jkfrontendchannel is not ready yet'); + this.logger.info('[jamClientProxy] jkfrontendchannel is not ready yet'); deferred.reject('frontendchannel is not ready yet'); return deferred.promise; } @@ -466,7 +470,7 @@ class AsyncJamClient { }; const proxy = new Proxy(this, handler); - this.logger.log('[asyncJamClient] Connected to WebChannel, ready to send/receive messages!'); + this.logger.log('[jamClientProxy] Connected to WebChannel, ready to send/receive messages!'); return proxy; } @@ -476,4 +480,4 @@ class AsyncJamClient { } } -export default AsyncJamClient; +export default JamClientProxy; diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js index 1643d9b0b..b83358fff 100644 --- a/jam-ui/src/helpers/rest.js +++ b/jam-ui/src/helpers/rest.js @@ -251,8 +251,8 @@ export const createSession = (options = {}) => { method: 'post', body: JSON.stringify(options) }) - .then(response => resolve(response)) - .catch(error => reject(error)) + .then(response => resolve(response)) + .catch(error => reject(error)) }) } @@ -784,4 +784,29 @@ export const updateEmail = (userId, email, current_password) => { .then(response => resolve(response)) .catch(error => reject(error)); }); +} + +// function updateUdpReachable(options) { +// var id = getId(options); + +// return $.ajax({ +// type: "POST", +// dataType: "json", +// contentType: 'application/json', +// url: "/api/users/" + id + "/udp_reachable", +// data: JSON.stringify(options), +// processData: false +// }); +// } + +export const updateUdpReachable = (options = {}) => { + const { id, ...rest } = options; + return new Promise((resolve, reject) => { + apiFetch(`/users/${id}/udp_reachable`, { + method: 'POST', + body: JSON.stringify(rest) + }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); } \ No newline at end of file diff --git a/jam-ui/src/helpers/utils.js b/jam-ui/src/helpers/utils.js index 0e2a6c606..49b10c722 100644 --- a/jam-ui/src/helpers/utils.js +++ b/jam-ui/src/helpers/utils.js @@ -81,11 +81,11 @@ export const numberFormatter = (number, fixed = 2) => { ? (Math.abs(Number(number)) / 1.0e9).toFixed(fixed) + 'B' : // Six Zeroes for Millions Math.abs(Number(number)) >= 1.0e6 - ? (Math.abs(Number(number)) / 1.0e6).toFixed(fixed) + 'M' - : // Three Zeroes for Thousands - Math.abs(Number(number)) >= 1.0e3 - ? (Math.abs(Number(number)) / 1.0e3).toFixed(fixed) + 'K' - : Math.abs(Number(number)).toFixed(fixed); + ? (Math.abs(Number(number)) / 1.0e6).toFixed(fixed) + 'M' + : // Three Zeroes for Thousands + Math.abs(Number(number)) >= 1.0e3 + ? (Math.abs(Number(number)) / 1.0e3).toFixed(fixed) + 'K' + : Math.abs(Number(number)).toFixed(fixed); }; //=============================== @@ -203,21 +203,21 @@ export const capitalize = str => (str.charAt(0).toUpperCase() + str.slice(1)).re export const titleize = (str) => { return str.replace( - /\w\S*/g, - (txt) => { - return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); - } + /\w\S*/g, + (txt) => { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + } ); - } +} - export const truncate = (str, length=100) => { - if(!str) return; +export const truncate = (str, length = 100) => { + if (!str) return; if (str.length <= length) { return str; } else { return `${str.substring(0, length)}...`; } - } +} export const routesSlicer = ({ routes, columns = 3, rows }) => { const routesCollection = []; @@ -256,15 +256,15 @@ export const copyToClipBoard = textFieldRef => { export const currencyFormat = (num) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' } - ).format(num); + ).format(num); } const days = new Array("Sun", "Mon", "Tue", - "Wed", "Thu", "Fri", "Sat"); + "Wed", "Thu", "Fri", "Sat"); const months = new Array("January", "February", "March", - "April", "May", "June", "July", "August", "September", - "October", "November", "December"); + "April", "May", "June", "July", "August", "September", + "October", "November", "December"); export const monthName = (monthNumber) => { return months[monthNumber]; @@ -272,7 +272,7 @@ export const monthName = (monthNumber) => { export const formatDateShort = (dateString) => { const date = dateString instanceof Date ? dateString : new Date(dateString); - return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear(); + return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear(); } // returns Fri May 20, 2013 @@ -287,7 +287,7 @@ export const formatDate = (dateString, options = {}) => { return (suppressDay ? '' : (days[date.getDay()] + ' ')) + months[date.getMonth()] + ' ' + padString(date.getDate(), 2) + ', ' + date.getFullYear(); } -export const groupByKey = (list, key) => list.reduce((hash, obj) => ({...hash, [obj[key]]:( hash[obj[key]] || [] ).concat(obj)}), {}) +export const groupByKey = (list, key) => list.reduce((hash, obj) => ({ ...hash, [obj[key]]: (hash[obj[key]] || []).concat(obj) }), {}) const padString = (str, max) => { @@ -302,9 +302,9 @@ const padString = (str, max) => { export const detectOS = () => { let platform; - if(navigator.platform) { + if (navigator.platform) { platform = navigator.platform.toLowerCase(); - }else if(navigator.userAgentData){ + } else if (navigator.userAgentData) { platform = navigator.userAgentData.platform.toLowerCase(); } @@ -336,25 +336,52 @@ export const isAppleSilicon = () => { } export const formatUtcTime = (date, change) => { - if (change) { - date.setMinutes(Math.ceil(date.getMinutes() / 30) * 30); + if (change) { + date.setMinutes(Math.ceil(date.getMinutes() / 30) * 30); + } + var h12h = date.getHours(); + var m12h = date.getMinutes(); + var ampm; + if (h12h >= 0 && h12h < 12) { + if (h12h === 0) { + h12h = 12; // 0 becomes 12 } - var h12h = date.getHours(); - var m12h = date.getMinutes(); - var ampm; - if (h12h >= 0 && h12h < 12) { - if (h12h === 0) { - h12h = 12; // 0 becomes 12 - } - ampm = "AM"; + ampm = "AM"; + } + else { + if (h12h > 12) { + h12h -= 12; // 13-23 becomes 1-11 } - else { - if (h12h > 12) { - h12h -= 12; // 13-23 becomes 1-11 - } - ampm = "PM"; - } - var timeString = ("00" + h12h).slice(-2) + ":" + ("00" + m12h).slice(-2) + " " + ampm; + ampm = "PM"; + } + var timeString = ("00" + h12h).slice(-2) + ":" + ("00" + m12h).slice(-2) + " " + ampm; - return timeString; - } \ No newline at end of file + return timeString; +} + +// http://stackoverflow.com/a/8809472/834644 +export const generateUUID = function () { + var d = new Date().getTime(); + var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16); + }); + return uuid; +}; + +export const toQueryString = (obj, prefix) => { + const str = []; + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + const k = prefix ? `${prefix}[${key}]` : key; + const v = obj[key]; + if (typeof v === "object" && v !== null) { + str.push(toQueryString(v, k)); + } else { + str.push(encodeURIComponent(k) + "=" + encodeURIComponent(v)); + } + } + } + return str.join("&"); +} \ No newline at end of file diff --git a/jam-ui/src/hooks/useClientWebsocket.js b/jam-ui/src/hooks/useClientWebsocket.js deleted file mode 100644 index 7a678a6a1..000000000 --- a/jam-ui/src/hooks/useClientWebsocket.js +++ /dev/null @@ -1,44 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -export default function useClientWebSocket(url) { - const [isConnected, setIsConnected] = useState(false); - const [messages, setMessages] = useState([]); - const ws = useRef(null); - - useEffect(() => { - ws.current = new WebSocket(url); - - ws.current.onopen = () => { - console.log('Connected to WebSocket'); - setIsConnected(true); - }; - - ws.current.onmessage = (event) => { - const data = JSON.parse(event.data); - setMessages(prev => [...prev, data]); - }; - - ws.current.onclose = () => { - console.log('WebSocket disconnected'); - setIsConnected(false); - }; - - ws.current.onerror = (error) => { - console.error('WebSocket error:', error); - }; - - return () => { - if (ws.current) { - ws.current.close(); - } - }; - }, [url]); - - const sendMessage = (type, params = {}) => { - if (ws.current && ws.current.readyState === WebSocket.OPEN) { - ws.current.send(JSON.stringify({ type, params })); - } - }; - - return { isConnected, messages, sendMessage }; -} diff --git a/jam-ui/src/hooks/useJamClientService.js b/jam-ui/src/hooks/useJamClientService.js new file mode 100644 index 000000000..de1d37e8e --- /dev/null +++ b/jam-ui/src/hooks/useJamClientService.js @@ -0,0 +1,405 @@ +// jam-ui/src/hooks/useJamWebSocketClient.js +import { useEffect, useRef, useState, useCallback } from 'react'; +import { generateUUID, getCookieValue, createCookie } from '../helpers/utils'; +import { messageFactory, MessageType } from '../helpers/MessageFactory'; +import { useJamClient } from '../context/JamClientContext'; +import { useStun } from './useStun'; + +export const ConnectionStatus = { + DISCONNECTED: 'disconnected', + CONNECTING: 'connecting', + CONNECTED: 'connected', + RECONNECTING: 'reconnecting', + ERROR: 'error' +}; + +export default function useJamClientService(url) { + const [connectionStatus, setConnectionStatus] = useState(ConnectionStatus.DISCONNECTED); + const [reconnectAttempts, setReconnectAttempts] = useState(0); + const [lastError, setLastError] = useState(null); + const [hasReachedMaxAttempts, setHasReachedMaxAttempts] = useState(false); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + const ws = useRef(null); + const callbacks = useRef({}); + const requestId = useRef(0); + const reconnectTimeoutRef = useRef(null); + const reconnectIntervalRef = useRef(null); + const currentAttemptRef = useRef(0); + const maxReconnectAttempts = 10; + const baseReconnectDelay = 1000; // 1 second + const maxReconnectDelay = 30000; // 30 seconds + const shouldReconnect = useRef(true); + const server = useRef({}); + + const jamClient = useJamClient(); + console.log("useJamClientService jamClient", jamClient) + //server.current.jamClient = jamClient; + const stun = useStun(server.current); + + // Calculate exponential backoff delay + const getReconnectDelay = useCallback((attempt) => { + const delay = Math.min(baseReconnectDelay * Math.pow(2, attempt), maxReconnectDelay); + return delay + Math.random() * 1000; // Add jitter + }, []); + + // Connect to WebSocket + const connect = useCallback(async () => { + if (ws.current && (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING)) { + return; // Already connected or connecting + } + + setConnectionStatus(ConnectionStatus.CONNECTING); + console.log('_WEBSOCKET_ Attempting to connect to WebSocket...'); + + try { + const clientType = 'client'; //TODO: make dynamic + console.log("_WEBSOCKET_ jamClient?.clientID: ", jamClient.clientID()); + const clientId = getCookieValue('client_id') || await jamClient.clientID() || ''; + + // Gather necessary parameters before establishing the connection + // Use Promise.all to wait for all async calls to complete + const operatingModePromise = await jamClient.getOperatingMode(); + const macHashPromise = await jamClient.SessionGetMacHash(); + const osStringPromise = await jamClient.GetDetailedOS(); + + const allPromises = [ + operatingModePromise, + macHashPromise, + osStringPromise, + ]; + + Promise.all(allPromises).then(async ([operatingMode, machine, osString]) => { + const channelId = generateUUID(); + const rememberToken = getCookieValue('remember_token'); + + if (!rememberToken) { + console.log("_WEBSOCKET_ No remember_token found, user needs to log in."); + //TODO: trigger login UI + } + + machine = machine?.all || ''; + + const params = { + channel_id: channelId, + token: rememberToken, + client_type: clientType, + client_id: clientId, + machine: machine, + os: osString, + product: "JamClientModern", + udp_reachable: await stun.sync(), + } + + const queryString = new URLSearchParams(params) + const fullUrl = `${url}?${queryString}`; + + console.log('_WEBSOCKET_ Connecting to WebSocket URL:', fullUrl); + + ws.current = new WebSocket(fullUrl); + + ws.current.channelId = channelId; + + ws.current.onopen = () => { + console.log('_WEBSOCKET_ Connected to JamKazam WebSocket'); + setConnectionStatus(ConnectionStatus.CONNECTED); + setReconnectAttempts(0); + currentAttemptRef.current = 0; + setLastError(null); + setHasReachedMaxAttempts(false); // Reset the flag when we successfully connect + + // Re-initialize session after reconnection + if (reconnectAttempts > 0) { + console.log('_WEBSOCKET_ Reconnected successfully, reinitializing session...'); + // You can add session recovery logic here + } + + console.log("_WEBSOCKET_ remember_token: ", rememberToken); + if (rememberToken) { + rememberLogin(rememberToken); + } else { + //no remember token, show login screen + console.log("_WEBSOCKET_ No remember_token found, user needs to log in."); + //TODO: trigger login UI + } + }; + + ws.current.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + const { type, header, payload } = message; + console.log('_WEBSOCKET_ Message received:', message); + + //Handle response callbacks + if (message.id && callbacks.current[MessageType.RESPONSE]) { + callbacks.current[MessageType.RESPONSE].forEach(callback => callback(header, payload)); + } + + // Handle event callbacks + if (type && callbacks.current[type]) { + callbacks.current[type].forEach(callback => callback(header, payload)); + } + } catch (error) { + console.error('_WEBSOCKET_ Error parsing message:', error); + } + }; + + ws.current.onclose = (event) => { + console.log('_WEBSOCKET_ JamKazam WebSocket disconnected:', event.code, event.reason); + setConnectionStatus(ConnectionStatus.DISCONNECTED); + + // Attempt reconnection if it wasn't intentional and we haven't reached max attempts + if (shouldReconnect.current && !event.wasClean && !hasReachedMaxAttempts) { + scheduleReconnect(); + } + }; + + ws.current.onerror = (error) => { + console.error('JamKazam WebSocket error:', error); + setConnectionStatus(ConnectionStatus.ERROR); + setLastError(error); + + // Attempt reconnection if we haven't reached max attempts + if (shouldReconnect.current && !hasReachedMaxAttempts) { + scheduleReconnect(); + } + }; + }) + + } catch (error) { + console.error('_WEBSOCKET_ Failed to create WebSocket connection:', error); + setConnectionStatus(ConnectionStatus.ERROR); + setLastError(error); + scheduleReconnect(); + } + }, [url, reconnectAttempts]); + + const rememberLogin = useCallback((rememberToken) => { + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + const loginMessage = messageFactory.loginWithToken(rememberToken, null, 'browser'); + ws.current.send(JSON.stringify(loginMessage)); + console.log('_WEBSOCKET_ Sent remember login message', loginMessage); + } else { + console.warn('_WEBSOCKET_ Cannot send remember login, WebSocket not connected'); + } + }, []); + + // Schedule reconnection with exponential backoff + const scheduleReconnect = useCallback(() => { + if (!shouldReconnect.current) return; + + const attempt = currentAttemptRef.current; + if (attempt >= maxReconnectAttempts) { + console.log(`_WEBSOCKET_ Max reconnection attempts reached (${attempt})`); + setConnectionStatus(ConnectionStatus.ERROR); + setHasReachedMaxAttempts(true); + return; + } + + const delay = getReconnectDelay(attempt); + console.log(`_WEBSOCKET_ Scheduling reconnection in ${delay}ms (attempt ${attempt + 1}/${maxReconnectAttempts})`); + + setConnectionStatus(ConnectionStatus.RECONNECTING); + const newAttempt = attempt + 1; + setReconnectAttempts(newAttempt); + currentAttemptRef.current = newAttempt; + + // Clear any existing timeout to prevent overlapping reconnect attempts + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + } + + reconnectTimeoutRef.current = setTimeout(() => { + connect(); + }, delay); + }, [maxReconnectAttempts, getReconnectDelay, connect]); + + // Disconnect WebSocket + const disconnect = useCallback(() => { + shouldReconnect.current = false; + + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + + if (ws.current) { + ws.current.close(1000, 'Client disconnecting'); + } + + setConnectionStatus(ConnectionStatus.DISCONNECTED); + }, []); + + const sendMessage = useCallback((type, params = {}) => { + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + const message = { + type, + params, + id: ++requestId.current + }; + ws.current.send(JSON.stringify(message)); + return message.id; + } + return null; + }, []); + + const sendRequest = useCallback((type, params = {}) => { + return new Promise((resolve, reject) => { + const id = sendMessage(type, params); + if (id) { + const timeout = setTimeout(() => { + reject(new Error('Request timeout')); + }, 5000); + + const responseHandler = (event) => { + try { + const data = JSON.parse(event.data); + if (data.id === id) { + clearTimeout(timeout); + ws.current.removeEventListener('message', responseHandler); + resolve(data); + } + } catch (error) { + console.error('Error parsing response:', error); + } + }; + + ws.current.addEventListener('message', responseHandler); + } else { + reject(new Error('WebSocket not connected')); + } + }); + }, [sendMessage]); + + + const registerMessageCallback = useCallback((messageType, callback) => { + if (!callbacks.current[messageType]) { + callbacks.current[messageType] = [] + } + callbacks.current[messageType].push(callback); + }); + + const unregisterMessageCallback = useCallback((messageType, callback) => { + if (callbacks.current[messageType]) { + // Remove specific callback from array + callbacks.current[messageType] = callbacks.current[messageType].filter(cb => cb !== callback); + } + // If no callbacks remain for this type, delete the array + if (callbacks.current[messageType] && callbacks.current[messageType].length === 0) { + delete callbacks.current[messageType]; + } + + }) + + //user logged in to session screen. register login callback with the server + useEffect(() => { + registerLoginAck() + registerServerRejection() + }, []) + + const registerLoginAck = useCallback(() => { + // Register the login acknowledgment callback + console.log("registering login ack callback") + registerMessageCallback(MessageType.LOGIN_ACK, loggedIn) + }, []); + + const registerServerRejection = useCallback(() => { + // Register the login acknowledgment callback + console.log("registering server rejection callback") + registerMessageCallback(MessageType.SERVER_REJECTION, serverRejection) + }, []); + + const loggedIn = ((header, payload) => { + console.log("_WEBSOCKET_ logged in callback from websocket", header, payload); + server.current.ws = ws.current; + server.current.signedIn = true; + server.current.clientId = payload.client_id; + server.current.publicIp = payload.public_ip; + + jamClient.clientID = payload.client_id; + + //clearConnectTimeout(); + //heartbeatStateReset(); + + try { + const msg = { + user_id: payload.user_id, + token: payload.token, + username: payload.username, + arses: payload.arses, + client_id_int: payload.client_id_int, + subscription: payload.subscription + } + if (payload.connection_policy) { + try { + msg.policy = JSON.parse(payload.connection_policy) + } + catch (e) { + msg.policy = null + console.log("unable to parse connection policy", e) + } + } + console.log("logged with new msg", msg) + jamClient.OnLoggedIn(msg); // ACTS AS CONTINUATION + } catch (error) { + console.log("fallback to old callback", error) + jamClient.OnLoggedIn(payload.user_id, payload.token, payload.username); // ACTS AS CONTINUATION + } + setIsLoggedIn(true); + + // set clientId cookie if not already set + if (!getCookieValue('client_id')) { + createCookie('client_id', payload.client_id, 365); + } + }); + + const serverRejection = ((header, payload) => { + console.log("server rejection callback from websocket", header, payload); + alert("Server rejected connection: " + payload.reason); + jamClient.OnServerRejection(payload.reason); + }); + + //Initialize connection + useEffect(() => { + connect(); + + return () => { + disconnect(); + }; + }, []); + + // Cleanup on unmount + useEffect(() => { + return () => { + shouldReconnect.current = false; + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + } + if (ws.current) { + ws.current.close(); + } + }; + }, []); + + // Legacy compatibility - keep isConnected for existing code + const isConnected = connectionStatus === ConnectionStatus.CONNECTED; + + return { + // Legacy properties for backward compatibility + isConnected, + + // New connection management properties + connectionStatus, + reconnectAttempts, + lastError, + + // Methods + jamClient, + sendMessage, + sendRequest, + connect, + disconnect, + + isLoggedIn, + }; +} diff --git a/jam-ui/src/hooks/useJamWebSocketServer.js b/jam-ui/src/hooks/useJamWebSocketServer.js deleted file mode 100644 index 6dd193bcf..000000000 --- a/jam-ui/src/hooks/useJamWebSocketServer.js +++ /dev/null @@ -1,355 +0,0 @@ -// jam-ui/src/hooks/useJamWebSocketClient.js -import { useEffect, useRef, useState, useCallback } from 'react'; - -export const ConnectionStatus = { - DISCONNECTED: 'disconnected', - CONNECTING: 'connecting', - CONNECTED: 'connected', - RECONNECTING: 'reconnecting', - ERROR: 'error' -}; - -export default function useJamWebSocketClient(url = 'ws://localhost:8080') { - const [connectionStatus, setConnectionStatus] = useState(ConnectionStatus.DISCONNECTED); - const [messages, setMessages] = useState([]); - const [sessionData, setSessionData] = useState(null); - const [mixers, setMixers] = useState([]); - const [participants, setParticipants] = useState([]); - const [isRecording, setIsRecording] = useState(false); - const [isPlaying, setIsPlaying] = useState(false); - const [currentPosition, setCurrentPosition] = useState(0); - const [reconnectAttempts, setReconnectAttempts] = useState(0); - const [lastError, setLastError] = useState(null); - const [hasReachedMaxAttempts, setHasReachedMaxAttempts] = useState(false); - - const ws = useRef(null); - const callbacks = useRef({}); - const requestId = useRef(0); - const reconnectTimeoutRef = useRef(null); - const reconnectIntervalRef = useRef(null); - const currentAttemptRef = useRef(0); - const maxReconnectAttempts = 10; - const baseReconnectDelay = 1000; // 1 second - const maxReconnectDelay = 30000; // 30 seconds - const shouldReconnect = useRef(true); - - // Calculate exponential backoff delay - const getReconnectDelay = useCallback((attempt) => { - const delay = Math.min(baseReconnectDelay * Math.pow(2, attempt), maxReconnectDelay); - return delay + Math.random() * 1000; // Add jitter - }, []); - - // Connect to WebSocket - const connect = useCallback(() => { - if (ws.current && (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING)) { - return; // Already connected or connecting - } - - setConnectionStatus(ConnectionStatus.CONNECTING); - console.log('_WEBSOCKET_ Attempting to connect to WebSocket...'); - - try { - ws.current = new WebSocket(url); - - ws.current.onopen = () => { - console.log('_WEBSOCKET_ Connected to JamKazam WebSocket'); - setConnectionStatus(ConnectionStatus.CONNECTED); - setReconnectAttempts(0); - currentAttemptRef.current = 0; - setLastError(null); - setHasReachedMaxAttempts(false); // Reset the flag when we successfully connect - - // Re-initialize session after reconnection - if (reconnectAttempts > 0) { - console.log('_WEBSOCKET_ Reconnected successfully, reinitializing session...'); - // You can add session recovery logic here - } - }; - - ws.current.onmessage = (event) => { - // try { - // const data = JSON.parse(event.data); - // setMessages(prev => [...prev, data]); - - // // Handle message based on type - // if (messageHandlers[data.type]) { - // messageHandlers[data.type](data.data || data); - // } - - // // Call registered callbacks - // if (callbacks.current[data.type]) { - // callbacks.current[data.type](data.data || data); - // } - // } catch (error) { - // console.error('Error parsing message:', error); - // } - }; - - ws.current.onclose = (event) => { - console.log('_WEBSOCKET_ JamKazam WebSocket disconnected:', event.code, event.reason); - setConnectionStatus(ConnectionStatus.DISCONNECTED); - - // Attempt reconnection if it wasn't intentional and we haven't reached max attempts - if (shouldReconnect.current && !event.wasClean && !hasReachedMaxAttempts) { - scheduleReconnect(); - } - }; - - ws.current.onerror = (error) => { - console.error('JamKazam WebSocket error:', error); - setConnectionStatus(ConnectionStatus.ERROR); - setLastError(error); - - // Attempt reconnection if we haven't reached max attempts - if (shouldReconnect.current && !hasReachedMaxAttempts) { - scheduleReconnect(); - } - }; - - } catch (error) { - console.error('_WEBSOCKET_ Failed to create WebSocket connection:', error); - setConnectionStatus(ConnectionStatus.ERROR); - setLastError(error); - scheduleReconnect(); - } - }, [url, reconnectAttempts]); - - // Schedule reconnection with exponential backoff - const scheduleReconnect = useCallback(() => { - if (!shouldReconnect.current) return; - - const attempt = currentAttemptRef.current; - if (attempt >= maxReconnectAttempts) { - console.log(`_WEBSOCKET_ Max reconnection attempts reached (${attempt})`); - setConnectionStatus(ConnectionStatus.ERROR); - setHasReachedMaxAttempts(true); - return; - } - - const delay = getReconnectDelay(attempt); - console.log(`_WEBSOCKET_ Scheduling reconnection in ${delay}ms (attempt ${attempt + 1}/${maxReconnectAttempts})`); - - setConnectionStatus(ConnectionStatus.RECONNECTING); - const newAttempt = attempt + 1; - setReconnectAttempts(newAttempt); - currentAttemptRef.current = newAttempt; - - // Clear any existing timeout to prevent overlapping reconnect attempts - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - } - - reconnectTimeoutRef.current = setTimeout(() => { - connect(); - }, delay); - }, [maxReconnectAttempts, getReconnectDelay, connect]); - - // Disconnect WebSocket - const disconnect = useCallback(() => { - shouldReconnect.current = false; - - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - reconnectTimeoutRef.current = null; - } - - if (ws.current) { - ws.current.close(1000, 'Client disconnecting'); - } - - setConnectionStatus(ConnectionStatus.DISCONNECTED); - }, []); - - // Message handler system - // const messageHandlers = { - // connection: (data) => { - // setConnectionStatus(data.status === 'connected' ? ConnectionStatus.CONNECTED : ConnectionStatus.DISCONNECTED); - // }, - - // SessionGetAllControlState: (data) => { - // setMixers(data); - // }, - - // FTUEGetAllAudioConfigurations: (data) => { - // // Handle FTUE audio configurations - // return ['default']; // Example response - // }, - - // ConnectionStatusUpdate: (data) => { - // // Handle connection status updates - // if (callbacks.current.connectionStatusCallback) { - // callbacks.current.connectionStatusCallback(data); - // } - // }, - - // PlaybackPositionUpdate: (data) => { - // setCurrentPosition(data.position); - // if (callbacks.current.playbackPositionCallback) { - // callbacks.current.playbackPositionCallback(data); - // } - // }, - - // // Add more message handlers... - // }; - - const sendMessage = useCallback((type, params = {}) => { - if (ws.current && ws.current.readyState === WebSocket.OPEN) { - const message = { - type, - params, - id: ++requestId.current - }; - ws.current.send(JSON.stringify(message)); - return message.id; - } - return null; - }, []); - - const sendRequest = useCallback((type, params = {}) => { - return new Promise((resolve, reject) => { - const id = sendMessage(type, params); - if (id) { - const timeout = setTimeout(() => { - reject(new Error('Request timeout')); - }, 5000); - - const responseHandler = (event) => { - try { - const data = JSON.parse(event.data); - if (data.id === id) { - clearTimeout(timeout); - ws.current.removeEventListener('message', responseHandler); - resolve(data); - } - } catch (error) { - console.error('Error parsing response:', error); - } - }; - - ws.current.addEventListener('message', responseHandler); - } else { - reject(new Error('WebSocket not connected')); - } - }); - }, [sendMessage]); - - // Initialize connection on mount - useEffect(() => { - connect(); - - return () => { - disconnect(); - }; - }, []); // Empty dependency array - only run once on mount - - // Cleanup on unmount - useEffect(() => { - return () => { - shouldReconnect.current = false; - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - } - if (ws.current) { - ws.current.close(); - } - }; - }, []); - - // API methods that mimic the old jamClient interface - // const jamClient = { - // // Session Management - // JoinSession: (params) => sendMessage('JoinSession', params), - // LeaveSession: (params) => sendMessage('LeaveSession', params), - // SessionGetAllControlState: (isMaster) => sendRequest('SessionGetAllControlState', { isMaster }), - // SessionSetControlState: (mixerId, mode) => sendMessage('SessionSetControlState', { mixerId, mode }), - // SessionRegisterCallback: (callbackName) => { - // // Register callback for session events - // callbacks.current.sessionCallback = callbackName; - // }, - - // // Recording - // StartRecording: (params) => sendMessage('StartMediaRecording', params), - // StopRecording: (params) => sendMessage('FrontStopRecording', params), - // RegisterRecordingCallbacks: (startCb, stopCb, startedCb, stoppedCb, abortedCb) => { - // callbacks.current.recordingStartCallback = startCb; - // callbacks.current.recordingStopCallback = stopCb; - // callbacks.current.recordingStartedCallback = startedCb; - // callbacks.current.recordingStoppedCallback = stoppedCb; - // callbacks.current.recordingAbortedCallback = abortedCb; - // }, - - // // Playback - // SessionStartPlay: (mode) => sendMessage('SessionStartPlay', { mode }), - // SessionStopPlay: () => sendMessage('SessionStopPlay'), - // SessionPausePlay: () => sendMessage('SessionPausePlay'), - // SessionCurrrentPlayPosMs: () => sendRequest('SessionCurrrentPlayPosMs'), - - // // Audio Configuration - // FTUEGetAllAudioConfigurations: () => sendRequest('FTUEGetAllAudioConfigurations'), - // FTUEGetGoodAudioConfigurations: () => sendRequest('FTUEGetGoodAudioConfigurations'), - - // // System Info - // GetSampleRate: () => sendRequest('GetSampleRate'), - // IsAudioStarted: () => sendRequest('IsAudioStarted'), - - // // Connection Status - // SessionSetConnectionStatusRefreshRate: (rate) => sendMessage('SessionSetConnectionStatusRefreshRate', { rate }), - - // // User Management - // SessionSetUserName: (clientId, name) => sendMessage('SessionSetUserName', { clientId, name }), - - // // VST - // VSTListVsts: () => sendRequest('VSTListVsts'), - // VSTLoad: (params) => sendMessage('VSTLoad', params), - - // // Network - // NetworkTestResult: () => sendRequest('NetworkTestResult'), - // GetNetworkTestScore: () => sendRequest('GetNetworkTestScore'), - - // // Callbacks - // RegisterVolChangeCallBack: (callback) => { - // callbacks.current.volumeChangeCallback = callback; - // }, - - // setMetronomeOpenCallback: (callback) => { - // callbacks.current.metronomeCallback = callback; - // }, - - // // Register generic callback for bridge messages - // HandleBridgeCallback: (vuData) => { - // // Handle VU meter updates and other bridge messages - // vuData.forEach(vuInfo => { - // if (callbacks.current.bridgeCallback) { - // callbacks.current.bridgeCallback([vuInfo]); - // } - // }); - // } - // }; - - // Legacy compatibility - keep isConnected for existing code - const isConnected = connectionStatus === ConnectionStatus.CONNECTED; - - return { - // Legacy properties for backward compatibility - isConnected, - messages, - sessionData, - mixers, - participants, - isRecording, - isPlaying, - currentPosition, - - // New connection management properties - connectionStatus, - reconnectAttempts, - lastError, - - // Methods - //jamClient, - sendMessage, - sendRequest, - connect, - disconnect - }; -} diff --git a/jam-ui/src/hooks/useStun.js b/jam-ui/src/hooks/useStun.js new file mode 100644 index 000000000..34a0c5869 --- /dev/null +++ b/jam-ui/src/hooks/useStun.js @@ -0,0 +1,71 @@ +import { useEffect, useRef, useCallback } from "react"; +import { useJamClient } from "../context/JamClientContext"; +import { ALERT_NAMES } from "../helpers/globals"; +import { updateUdpReachable } from "../helpers/rest"; + +// Assumes context.JK, context.jamClient, and jQuery are globally available +// You may want to pass these as parameters or import them as needed + +export function useStun(app) { + const jamClient = useJamClient(); + const udpBlockedRef = useRef(null); + + // Syncs the UDP blocked state and optionally calls a callback when changed + const sync = useCallback(async (changed) => { + + const result = await jamClient.NetworkTestResult(); + + if (result) { + if ( + udpBlockedRef.current === null || + result.remote_udp_blocked !== udpBlockedRef.current + ) { + // Log the result + if (result.remote_udp_blocked) { + console.debug("NO STUN: " + JSON.stringify(result)); + } else { + console.debug("STUN capable: " + JSON.stringify(result)); + } + + udpBlockedRef.current = result.remote_udp_blocked; + + if (changed) changed(result.remote_udp_blocked); + } + } + + return udpBlockedRef.current; + }, [jamClient]); + + // Handles STUN events and updates the server + useEffect(() => { + function handleStunEvent() { + console.debug("handling stun event..."); + sync((blocked) => { + if (app?.clientId) { + updateUdpReachable({ + client_id: app.clientId, + udp_reachable: !blocked, + }); + } + }); + } + + // Register event listener + if (window?.JK?.onBackendEvent && ALERT_NAMES?.STUN_EVENT) { + window.JK.onBackendEvent( + ALERT_NAMES.STUN_EVENT, + "everywhere", + handleStunEvent + ); + } + + // Cleanup (if your event system supports it) + return () => { + // No built-in way to remove the event in the original code + // Add cleanup logic here if available + }; + }, [app, sync]); + + // Optionally, return sync or udpBlockedRef if needed by the component + return { sync, udpBlockedRef }; +} diff --git a/jam-ui/src/layouts/JKClientLayout.js b/jam-ui/src/layouts/JKClientLayout.js index 423fdf9a3..5b1eeaa46 100644 --- a/jam-ui/src/layouts/JKClientLayout.js +++ b/jam-ui/src/layouts/JKClientLayout.js @@ -5,6 +5,7 @@ import UserAuth from '../context/UserAuth'; import { BrowserQueryProvider } from '../context/BrowserQuery'; import { AppDataProvider } from '../context/AppDataContext'; import { AppRoutesProvider } from '../context/AppRoutesContext'; +import { JamClientProvider } from '../context/JamClientContext'; const JKClientLayout = ({ location }) => { @@ -14,7 +15,9 @@ const JKClientLayout = ({ location }) => { - + + + diff --git a/jam-ui/src/layouts/JKClientRoutes.js b/jam-ui/src/layouts/JKClientRoutes.js index 1d07135f4..c09d79a27 100644 --- a/jam-ui/src/layouts/JKClientRoutes.js +++ b/jam-ui/src/layouts/JKClientRoutes.js @@ -8,8 +8,6 @@ import NavbarTop from '../components/navbar/JKNavbarTop'; import NavbarVertical from '../components/navbar/JKNavbarVertical'; import Footer from '../components/footer/JKFooter'; -import AsyncJamClientExample from '../helpers/asyncJamClientExample'; - // Import your page components here import JKSessionScreen from '../components/client/JKSessionScreen'; diff --git a/jam-ui/src/websockets/fakeClientWebsocket.js b/jam-ui/src/websockets/fakeJamClientServer.js similarity index 98% rename from jam-ui/src/websockets/fakeClientWebsocket.js rename to jam-ui/src/websockets/fakeJamClientServer.js index d0ade5794..0fba0cd45 100644 --- a/jam-ui/src/websockets/fakeClientWebsocket.js +++ b/jam-ui/src/websockets/fakeJamClientServer.js @@ -7,8 +7,8 @@ const http = require('http'); * Handles all the internal system functions and provides mock responses */ -class FakeClientWebSocket { - constructor(port = 8080) { +class FakeJamClientServer { + constructor(port = 3060) { this.port = port; this.server = null; this.wss = null; @@ -765,21 +765,21 @@ class FakeClientWebSocket { } // Create and start the server -const fakeClient = new FakeClientWebSocket(8080); -fakeClient.start(); -fakeClient.startPeriodicUpdates(); +const fakeServer = new FakeJamClientServer(3060); +fakeServer.start(); +fakeServer.startPeriodicUpdates(); // Handle graceful shutdown process.on('SIGINT', () => { console.log('\nShutting down Fake Client WebSocket server...'); - fakeClient.stop(); + fakeServer.stop(); process.exit(0); }); process.on('SIGTERM', () => { console.log('\nShutting down Fake Client WebSocket server...'); - fakeClient.stop(); + fakeServer.stop(); process.exit(0); }); -module.exports = FakeClientWebSocket; +module.exports = FakeJamClientServer; diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 5f1d5a740..fb8b8c9ad 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -532,7 +532,6 @@ module JamWebsockets @largest_message_user = client.user_id end - # extract the message safely websocket_comm(client, nil) do if client.encode_json