This commit is contained in:
Nuwan 2025-09-30 00:27:48 +05:30
parent f1764420f5
commit c64b4548f2
22 changed files with 1384 additions and 675 deletions

View File

@ -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
REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms
REACT_APP_WEBSOCKET_GATEWAY_URL=ws://localhost:6767

View File

@ -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=
REACT_APP_BRAINTREE_TOKEN=
REACT_APP_WEBSOCKET_GATEWAY_URL=

View File

@ -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=

View File

@ -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=

View File

@ -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 (
<Card>
{ !isConnected && <div className='d-flex align-items-center'>Connecting to backend...</div>}
{!isConnected && <div className='d-flex align-items-center'>Connecting to backend...</div>}
<FalconCardHeader title={`Session ${sessionState.sessionId}`} titleClass="font-weight-bold">
<Button
color="primary"
className="me-2 mr-2 fs--1"
onClick={handleStartRecording}
disabled={!isConnected || isRecording}
>
{isRecording ? 'Recording...' : 'Start Recording'}
</Button>
<Button
color="secondary"
className="me-2 mr-2 fs--1"
onClick={handleStopRecording}
disabled={!isRecording}
>
Stop Recording
</Button>
<Button
color="success"
className="me-2 mr-2 fs--1"
onClick={handlePlayPause}
disabled={!isConnected}
>
{isPlaying ? 'Pause' : 'Play'}
</Button>
<Button
color="warning"
className="me-2 mr-2 fs--1"
onClick={handleStopPlayback}
disabled={!isPlaying}
>
Stop
</Button>
<Button
color="danger"
className="me-2 mr-2 fs--1"
onClick={() => {
// Manually disconnect for testing reconnection
fetch('http://localhost:8080/simulate-drop', { method: 'POST' })
.catch(() => console.log('Connection drop simulated'));
}}
>
Test Disconnect
</Button>
</FalconCardHeader>
<CardHeader className="bg-light border-bottom border-top py-2 border-3">
<div className="d-flex flex-nowrap overflow-auto" style={{ gap: '0.5rem' }}>
<Button className='btn-custom-outline' outline size="md">Settings</Button>
@ -184,13 +132,13 @@ const JKSessionScreen = () => {
<Button className='btn-custom-outline' outline size="md">Resync</Button>
</div>
</CardHeader>
<CardBody className="pl-4" style={{ backgroundColor: '#edf2f9f5' }}>
<div className='d-flex' style={{ gap: '1rem' }}>
<div className='audioInputs'>
<h5>Audio Inputs ({participants.length} participants)</h5>
<h5>Audio Inputs</h5>
<div className='d-flex' style={{ gap: '0.5rem', borderRight: '1px #ddd solid', paddingRight: '1rem' }}>
{participants.map((participant, index) => (
{/* {participants.map((participant, index) => (
<div key={participant.clientId} className='shadow-sm' style={{ border: '1px #ddd solid', width: '100px', height: '600px', backgroundColor: 'white' }}>
<div className='d-flex flex-column' style={{ height: '100%' }}>
<div className='p-2'>{participant.name}</div>
@ -200,14 +148,14 @@ const JKSessionScreen = () => {
<div className='p-2'>Controls</div>
</div>
</div>
))}
))} */}
</div>
</div>
<div className='sessionMix'>
<h5>Session Mix ({mixers.length} mixers)</h5>
<h5>Session Mix</h5>
<div className='d-flex' style={{ gap: '0.5rem' }}>
{mixers.slice(0, 4).map((mixer, index) => (
{/* {mixers.slice(0, 4).map((mixer, index) => (
<div key={mixer.id} className='shadow-sm' style={{ border: '1px #ddd solid', width: '100px', height: '600px', backgroundColor: 'white' }}>
<div className='d-flex flex-column align-items-center' style={{ height: '100%', padding: '10px' }}>
<div className='mb-2'>Mixer {index + 1}</div>
@ -217,10 +165,10 @@ const JKSessionScreen = () => {
<div className='mb-2'>Type: {mixer.group_id}</div>
</div>
</div>
))}
))} */}
</div>
</div>
<div className='attachMedia'>
<h5>Attach Media</h5>
<div className='d-flex flex-column' style={{ gap: '0.5rem' }}>
@ -231,14 +179,14 @@ const JKSessionScreen = () => {
</div>
</div>
</div>
{/* Connection Status Alerts */}
{showConnectionAlert && (
<div className='mt-4'>
<Alert color={
connectionStatus === ConnectionStatus.DISCONNECTED ? 'warning' :
connectionStatus === ConnectionStatus.RECONNECTING ? 'info' :
connectionStatus === ConnectionStatus.ERROR ? 'danger' : 'success'
connectionStatus === ConnectionStatus.RECONNECTING ? 'info' :
connectionStatus === ConnectionStatus.ERROR ? 'danger' : 'success'
}>
<div className='d-flex align-items-center'>
<div className='me-2'>
@ -285,9 +233,9 @@ const JKSessionScreen = () => {
<Badge
color={
connectionStatus === ConnectionStatus.CONNECTED ? 'success' :
connectionStatus === ConnectionStatus.CONNECTING ? 'warning' :
connectionStatus === ConnectionStatus.RECONNECTING ? 'info' :
connectionStatus === ConnectionStatus.ERROR ? 'danger' : 'secondary'
connectionStatus === ConnectionStatus.CONNECTING ? 'warning' :
connectionStatus === ConnectionStatus.RECONNECTING ? 'info' :
connectionStatus === ConnectionStatus.ERROR ? 'danger' : 'secondary'
}
className='me-2'
>
@ -314,17 +262,8 @@ const JKSessionScreen = () => {
<div className='col-md-3'>
<strong>Status:</strong> {connectionStatus}
</div>
<div className='col-md-3'>
<strong>Recording:</strong> {isRecording ? '🔴 Recording' : ' Not Recording'}
</div>
<div className='col-md-3'>
<strong>Playback:</strong> {isPlaying ? ' Playing' : ' Paused'}
</div>
</div>
<div className='row mt-2'>
<div className='col-md-3'>
<strong>Position:</strong> {Math.floor(currentPosition / 1000)}s
</div>
<div className='col-md-3'>
<strong>Reconnect Attempts:</strong> {reconnectAttempts}
</div>

View File

@ -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 (
<GlobalContext.Provider value={{}}>
{children}
</GlobalContext.Provider>
);
};

View File

@ -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 (
<JamClientContext.Provider value={proxyRef.current}>
{children}
</JamClientContext.Provider>
);
};
export const useJamClient = () => useContext(JamClientContext);

View File

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

View File

@ -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 (
<div>
<h3>JKMessageFactory Example</h3>
<p>Check the console for example message outputs.</p>
<p>The message factory is ready to be used in your React components for WebSocket communication.</p>
</div>
);
};
export default MessageFactoryExample;

View File

@ -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 (
<div>
<button onClick={handleJoinSession}>Join Session</button>
<button onClick={handleLeaveSession}>Leave Session</button>
</div>
);
};
export default JamSessionComponent;

View File

@ -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"
}

View File

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

View File

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

View File

@ -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;
}
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("&");
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 }) => {
<AppRoutesProvider>
<AppDataProvider>
<BrowserQueryProvider>
<ClientRoutes />
<JamClientProvider>
<ClientRoutes />
</JamClientProvider>
</BrowserQueryProvider>
</AppDataProvider>
</AppRoutesProvider>

View File

@ -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';

View File

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

View File

@ -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