wip
This commit is contained in:
parent
f1764420f5
commit
c64b4548f2
|
|
@ -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
|
||||
|
|
@ -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=
|
||||
|
|
@ -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=
|
||||
|
|
@ -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=
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
@ -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("&");
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue