pjxcharya commited on
Commit
12c125d
·
verified ·
1 Parent(s): e7a54b8

Update static/js/websocket_trainer.js

Browse files
Files changed (1) hide show
  1. static/js/websocket_trainer.js +297 -296
static/js/websocket_trainer.js CHANGED
@@ -1,330 +1,331 @@
1
- // Assume socket.io client is already included in the HTML.
2
- // For standalone testing, you might connect like this:
3
- // const socket = io.connect('http://localhost:5000');
4
- const socket = io();
5
-
6
- // --- Global Variables ---
7
- let currentExerciseType = null;
8
- let videoStream = null;
9
- let isSessionActive = false;
10
- let animationFrameId = null;
11
-
12
- // --- HTML Element References (will be defined in HTML) ---
13
- const videoElement = document.getElementById('videoElement');
14
- const canvasElement = document.createElement('canvas'); // Offscreen canvas
15
- const startButton = document.getElementById('startButton');
16
- const stopButton = document.getElementById('stopButton');
17
- const exerciseTypeSelect = document.getElementById('exerciseTypeSelect');
18
-
19
- // UI Displays for feedback
20
- const repsDisplay = document.getElementById('repsDisplay'); // For general rep count
21
- const feedbackDisplay = document.getElementById('feedbackDisplay'); // For general feedback
22
- const stageDisplay = document.getElementById('stageDisplay'); // For exercise stage (e.g., "up", "down")
23
-
24
- // Specific displays for Hammer Curl (as it has left/right)
25
- const repsLeftDisplay = document.getElementById('repsLeftDisplay');
26
- const repsRightDisplay = document.getElementById('repsRightDisplay');
27
- const feedbackLeftDisplay = document.getElementById('feedbackLeftDisplay');
28
- const feedbackRightDisplay = document.getElementById('feedbackRightDisplay');
29
- const stageLeftDisplay = document.getElementById('stageLeftDisplay');
30
- const stageRightDisplay = document.getElementById('stageRightDisplay');
31
- const angleDisplay = document.getElementById('angleDisplay'); // General angle display
32
-
33
- // --- Utility Functions ---
34
- function clearUIFeedback() {
35
- if (repsDisplay) repsDisplay.textContent = 'Reps: 0';
36
- if (feedbackDisplay) feedbackDisplay.textContent = 'Feedback: -';
37
- if (stageDisplay) stageDisplay.textContent = 'Stage: -';
38
- if (angleDisplay) angleDisplay.textContent = 'Angle: -';
39
-
40
- if (repsLeftDisplay) repsLeftDisplay.textContent = 'Reps Left: 0';
41
- if (repsRightDisplay) repsRightDisplay.textContent = 'Reps Right: 0';
42
- if (feedbackLeftDisplay) feedbackLeftDisplay.textContent = 'Feedback Left: -';
43
- if (feedbackRightDisplay) feedbackRightDisplay.textContent = 'Feedback Right: -';
44
- if (stageLeftDisplay) stageLeftDisplay.textContent = 'Stage Left: -';
45
- if (stageRightDisplay) stageRightDisplay.textContent = 'Stage Right: -';
46
 
47
- // Hide or show elements based on exercise type if necessary
48
- const hammerCurlElements = document.querySelectorAll('.hammer-curl-specific');
49
- hammerCurlElements.forEach(el => el.style.display = 'none');
50
- const genericElements = document.querySelectorAll('.generic-exercise-specific');
51
- genericElements.forEach(el => el.style.display = 'block');
52
- }
53
-
54
- function updateGenericUI(data) {
55
- if (repsDisplay) repsDisplay.textContent = `Reps: ${data.counter || 0}`;
56
- if (feedbackDisplay) feedbackDisplay.textContent = `Feedback: ${data.feedback || '-'}`;
57
- if (stageDisplay) stageDisplay.textContent = `Stage: ${data.stage || '-'}`;
58
- if (angleDisplay && data.angle_left !== undefined && data.angle_right !== undefined) {
59
- angleDisplay.textContent = `Angles L: ${Math.round(data.angle_left)}, R: ${Math.round(data.angle_right)}`;
60
- } else if (angleDisplay && data.angle_body_left !== undefined && data.angle_body_right !== undefined) {
61
- angleDisplay.textContent = `Body Angles L: ${Math.round(data.angle_body_left)}, R: ${Math.round(data.angle_body_right)}`;
 
 
 
 
 
 
 
 
 
 
62
  }
63
- }
64
-
65
- function updateHammerCurlUI(data) {
66
- if (repsLeftDisplay) repsLeftDisplay.textContent = `Reps Left: ${data.counter_left || 0}`;
67
- if (repsRightDisplay) repsRightDisplay.textContent = `Reps Right: ${data.counter_right || 0}`;
68
- if (feedbackLeftDisplay) feedbackLeftDisplay.textContent = `Feedback Left: ${data.feedback_left || '-'}`;
69
- if (feedbackRightDisplay) feedbackRightDisplay.textContent = `Feedback Right: ${data.feedback_right || '-'}`;
70
- if (stageLeftDisplay) stageLeftDisplay.textContent = `Stage Left: ${data.stage_left || '-'}`;
71
- if (stageRightDisplay) stageRightDisplay.textContent = `Stage Right: ${data.stage_right || '-'}`;
72
- if (angleDisplay && data.angle_left_curl !== undefined && data.angle_right_curl !== undefined) {
73
- angleDisplay.textContent = `Curl Angles L: ${Math.round(data.angle_left_curl)}, R: ${Math.round(data.angle_right_curl)}`;
74
  }
75
- }
76
-
77
- function setupUIForExercise(exerciseType) {
78
- const hammerCurlElements = document.querySelectorAll('.hammer-curl-specific');
79
- const genericElements = document.querySelectorAll('.generic-exercise-specific');
80
-
81
- if (exerciseType === 'hammer_curl') {
82
- hammerCurlElements.forEach(el => el.style.display = 'block'); // or 'inline', 'flex' etc.
83
- genericElements.forEach(el => el.style.display = 'none');
84
- } else {
85
- hammerCurlElements.forEach(el => el.style.display = 'none');
86
- genericElements.forEach(el => el.style.display = 'block'); // or 'inline', 'flex' etc.
87
  }
88
- }
89
 
90
 
91
- // --- Webcam and Frame Sending ---
92
- async function startWebcam() {
93
- try {
94
- videoStream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: false });
95
- if (videoElement) {
96
- videoElement.srcObject = videoStream;
97
- videoElement.onloadedmetadata = () => {
98
- videoElement.play();
99
- // Set canvas dimensions once video is playing
100
- if (canvasElement) {
101
- canvasElement.width = videoElement.videoWidth;
102
- canvasElement.height = videoElement.videoHeight;
103
- }
104
- // Start sending frames only after session is confirmed active by server
105
- };
106
  }
107
- } catch (error) {
108
- console.error("Error accessing webcam:", error);
109
- alert("Could not access webcam. Please ensure permissions are granted.");
110
- isSessionActive = false; // Ensure session is marked as inactive
111
- if (startButton) startButton.disabled = false;
112
- if (stopButton) stopButton.disabled = true;
113
- }
114
- }
115
 
116
- function stopWebcam() {
117
- if (videoStream) {
118
- videoStream.getTracks().forEach(track => track.stop());
119
- videoStream = null;
120
- }
121
- if (videoElement) {
122
- videoElement.srcObject = null;
123
- }
124
- if (animationFrameId) {
125
- cancelAnimationFrame(animationFrameId);
126
- animationFrameId = null;
127
- }
128
- console.log("Webcam stopped.");
129
- }
130
-
131
- async function sendFrameLoop() {
132
- // Assuming logger object is defined, e.g.:
133
- // const logger = {
134
- // log: (message) => console.log(`[Trainer Log] ${message}`),
135
- // error: (message) => console.error(`[Trainer Error] ${message}`),
136
- // info: (message) => console.info(`[Trainer Info] ${message}`)
137
- // };
138
- // And variables like isSessionActive, videoElement, canvasElement, context, currentExerciseType, socket, animationFrameId are defined in the broader scope.
139
- // If logger is not globally available, use console.log/console.info directly.
140
-
141
- console.log(`[sendFrameLoop] Called. isSessionActive: ${isSessionActive}, videoElement.srcObject: ${!!videoElement.srcObject}, video.paused: ${videoElement.paused}, video.ended: ${videoElement.ended}, video.readyState: ${videoElement.readyState}`);
142
-
143
- if (!isSessionActive || !videoElement.srcObject || videoElement.paused || videoElement.ended || videoElement.readyState < 3) { // Added HESITATION_THRESHOLD_FRAMES
144
- console.info(`[sendFrameLoop] Stopping frame loop due to conditions:
145
- isSessionActive: ${isSessionActive},
146
- videoElement.srcObject: ${!!videoElement.srcObject},
147
- video.paused: ${videoElement.paused},
148
- video.ended: ${videoElement.ended},
149
- video.readyState: ${videoElement.readyState}`);
150
-
151
- if (animationFrameId) {
152
- cancelAnimationFrame(animationFrameId);
153
- animationFrameId = null;
154
- }
155
- return;
156
- }
157
 
158
- // Draw current video frame to canvas
159
- try {
160
- // Get context here if it's not already global or if canvas might be recreated.
161
- // If canvasElement is persistent and context is stable, it can be initialized once.
162
- const context = canvasElement.getContext('2d');
163
- if (canvasElement.width > 0 && canvasElement.height > 0) {
164
- context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
165
- const imageDataBase64 = canvasElement.toDataURL('image/jpeg', 0.7).split(',')[1]; // Get base64 string
166
 
167
- if (socket && socket.connected) {
168
- // console.log(`[sendFrameLoop] Sending frame for exercise: ${currentExerciseType}`); // Can be too verbose
169
- socket.emit('process_frame', {
170
- image: imageDataBase64,
171
- exercise_type: currentExerciseType,
172
- frame_width: canvasElement.width,
173
- frame_height: canvasElement.height
174
- });
175
- } else {
176
- console.error("[sendFrameLoop] Socket not connected. Cannot send frame.");
177
- }
178
- } else {
179
- console.warn("[sendFrameLoop] Canvas dimensions are zero, skipping frame draw/send.");
180
- }
181
- } catch (e) {
182
- console.error("[sendFrameLoop] Error in drawing or sending frame:", e);
183
- }
184
-
185
- // Loop to send next frame
186
- animationFrameId = requestAnimationFrame(sendFrameLoop);
187
- }
188
 
189
- // --- Socket.IO Event Handlers ---
190
- socket.on('connect', () => {
191
- console.log('Connected to server with SID:', socket.id);
192
- });
 
 
 
193
 
194
- socket.on('connection_ack', (data) => {
195
- console.log('Server Acknowledged Connection:', data);
196
- // You could use data.sid from server if needed, but socket.id is usually sufficient client-side
197
- });
 
 
198
 
199
- socket.on('session_started', (data) => {
200
- console.log('Session started on server:', data.session_id, "for exercise:", data.exercise_type);
201
- isSessionActive = true;
202
- currentExerciseType = data.exercise_type; // Ensure currentExerciseType is what server confirmed
203
- setupUIForExercise(currentExerciseType);
204
-
205
- if (startButton) startButton.disabled = true;
206
- if (stopButton) stopButton.disabled = false;
207
-
208
- if (videoStream && videoElement && !videoElement.paused) { // If webcam already started
209
- if (animationFrameId) cancelAnimationFrame(animationFrameId); // Clear previous loop if any
210
- sendFrameLoop();
211
- } else {
212
- startWebcam().then(() => { // Start webcam if not already, then start loop
213
- if (isSessionActive) { // Check again in case webcam failed
214
- if (animationFrameId) cancelAnimationFrame(animationFrameId);
215
- sendFrameLoop();
216
  }
217
  });
218
- }
219
- });
220
 
221
- socket.on('session_error', (data) => {
222
- console.error('Session error:', data.error);
223
- alert('Session Error: ' + data.error);
224
- isSessionActive = false;
225
- if (animationFrameId) cancelAnimationFrame(animationFrameId);
226
- animationFrameId = null;
227
- stopWebcam(); // Stop webcam on session error
228
- clearUIFeedback();
229
- if (startButton) startButton.disabled = false;
230
- if (stopButton) stopButton.disabled = true;
231
- });
232
 
233
- socket.on('exercise_update', (data) => {
234
- // console.log('Exercise update received:', data); // Can be very verbose
235
- if (!isSessionActive) return;
 
 
236
 
237
- if (data.success) {
238
- if (data.landmarks_detected) {
239
- if (currentExerciseType === 'hammer_curl') {
240
- updateHammerCurlUI(data.data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  } else {
242
- updateGenericUI(data.data);
 
243
  }
244
- } else {
245
- if (feedbackDisplay) feedbackDisplay.textContent = 'Feedback: No landmarks detected. Adjust position?';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  }
247
- } else {
248
- // This case might indicate an error in processing even if landmarks were detected (e.g. Python error)
249
- console.error('Exercise update indicated failure:', data.message || 'Unknown error');
250
- if (feedbackDisplay) feedbackDisplay.textContent = `Feedback: Error - ${data.message || 'Processing error'}`;
251
  }
252
- });
253
 
254
- socket.on('frame_error', (data) => {
255
- console.error('Frame processing error from server:', data.error);
256
- if (feedbackDisplay) feedbackDisplay.textContent = `Error: ${data.error}`;
257
- // Consider if we should stop the session or allow user to try again
258
- });
 
 
 
259
 
260
- socket.on('disconnect', () => {
261
- console.log('Disconnected from server.');
262
- isSessionActive = false;
263
- if (animationFrameId) cancelAnimationFrame(animationFrameId);
264
- animationFrameId = null;
265
- stopWebcam();
266
- clearUIFeedback();
267
- if (startButton) startButton.disabled = false;
268
- if (stopButton) stopButton.disabled = true;
269
- alert("Disconnected from server. Please start a new session.");
270
- });
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
- // --- UI Event Listeners ---
274
- if (startButton) {
275
- startButton.addEventListener('click', () => {
276
- if (exerciseTypeSelect) {
277
- currentExerciseType = exerciseTypeSelect.value;
278
- if (!currentExerciseType) {
279
- alert("Please select an exercise type.");
280
- return;
 
 
 
281
  }
282
- } else {
283
- alert("Exercise type selector not found.");
284
  return;
285
  }
286
 
287
- console.log(`Starting exercise: ${currentExerciseType}`);
288
- socket.emit('start_exercise_session', { exercise_type: currentExerciseType });
289
-
290
- // Disable start button, enable stop button
291
- startButton.disabled = true;
292
- stopButton.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
- // Webcam and sendFrameLoop will now be started upon 'session_started' confirmation
295
- // This ensures we don't start sending frames before the server is ready for this session.
296
- // Initial UI setup for the selected exercise:
297
- setupUIForExercise(currentExerciseType);
298
- clearUIFeedback(); // Clear previous feedback
299
- });
300
- }
301
-
302
- if (stopButton) {
303
- stopButton.addEventListener('click', () => {
304
- console.log("Stop button clicked. Ending session.");
305
- isSessionActive = false; // Mark session as inactive immediately
306
-
307
- // The server's 'disconnect' handler will clean up the session for this socket.id
308
- // If more explicit cleanup is needed, an event like 'stop_exercise_session' could be emitted.
309
- // socket.emit('stop_exercise_session'); // Example if specific server-side stop logic is added
310
 
311
- stopWebcam();
312
- clearUIFeedback();
313
 
314
- if (startButton) startButton.disabled = false;
315
- if (stopButton) stopButton.disabled = true;
316
-
317
- // No need to explicitly disconnect socket here, allow user to start new session.
318
- // If you want to fully disconnect: socket.disconnect();
319
- });
320
- }
321
-
322
- // Initialize UI
323
- window.addEventListener('DOMContentLoaded', () => {
324
- clearUIFeedback();
325
- if (stopButton) stopButton.disabled = true;
326
- if (startButton) startButton.disabled = false;
327
- // Ensure correct UI elements are visible/hidden initially
328
- setupUIForExercise(exerciseTypeSelect ? exerciseTypeSelect.value : 'squat'); // Default to squat or first selected
329
  });
330
- console.log("WebSocket trainer script loaded. Waiting for DOM and user interaction.");
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const videoElement = document.getElementById('userVideo');
3
+ const canvasElement = document.createElement('canvas'); // Offscreen canvas
4
+ const context = canvasElement.getContext('2d');
5
+
6
+ const startButton = document.getElementById('startTrainerBtn');
7
+ const stopButton = document.getElementById('stopTrainerBtn');
8
+ const exerciseTypeSelect = document.getElementById('exerciseType');
9
+
10
+ const repsDisplay = document.getElementById('repsDisplay');
11
+ const stageDisplay = document.getElementById('stageDisplay');
12
+ const feedbackDisplay = document.getElementById('feedbackDisplay');
13
+ const angleLeftDisplay = document.getElementById('angleLeftDisplay');
14
+ const angleRightDisplay = document.getElementById('angleRightDisplay');
15
+
16
+ const repsLeftHcDisplay = document.getElementById('repsLeftHc');
17
+ const repsRightHcDisplay = document.getElementById('repsRightHc');
18
+ const stageLeftHcDisplay = document.getElementById('stageLeftHc');
19
+ const stageRightHcDisplay = document.getElementById('stageRightHc');
20
+ const feedbackLeftHcDisplay = document.getElementById('feedbackLeftHc');
21
+ const feedbackRightHcDisplay = document.getElementById('feedbackRightHc');
22
+ const angleLeftCurlHcDisplay = document.getElementById('angleLeftCurlHc');
23
+ const angleRightCurlHcDisplay = document.getElementById('angleRightCurlHc');
24
+
25
+ const squatPushupUIDiv = document.getElementById('squatPushupUI');
26
+ const hammerCurlUIDiv = document.getElementById('hammerCurlUI');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ const apiStatusDisplay = document.getElementById('apiStatus'); // Assuming this is still used for general status
29
+ const sessionIdDisplay = document.getElementById('sessionIdDisplay'); // Assuming this is still used
30
+
31
+ let socket = null;
32
+ let currentExerciseType = '';
33
+ let isSessionActive = false;
34
+ let animationFrameId = null;
35
+ let stream = null; // To keep track of the media stream
36
+
37
+ const logger = {
38
+ log: (message) => console.log(`[Trainer Log] ${message}`),
39
+ error: (message) => console.error(`[Trainer Error] ${message}`),
40
+ info: (message) => console.info(`[Trainer Info] ${message}`)
41
+ };
42
+
43
+ logger.log("WebSocket trainer script loaded. Waiting for DOM and user interaction.");
44
+
45
+ function setupUIForExercise(exerciseType) {
46
+ if (exerciseType === 'hammer_curl') {
47
+ if (squatPushupUIDiv) squatPushupUIDiv.style.display = 'none';
48
+ if (hammerCurlUIDiv) hammerCurlUIDiv.style.display = 'block';
49
+ } else {
50
+ if (squatPushupUIDiv) squatPushupUIDiv.style.display = 'block';
51
+ if (hammerCurlUIDiv) hammerCurlUIDiv.style.display = 'none';
52
+ }
53
  }
54
+
55
+ function updateGenericUI(data) {
56
+ if (repsDisplay) repsDisplay.textContent = data.counter !== undefined ? data.counter : 'N/A';
57
+ if (stageDisplay) stageDisplay.textContent = data.stage || 'N/A';
58
+ if (feedbackDisplay) feedbackDisplay.textContent = data.feedback || 'N/A';
59
+ if (angleLeftDisplay) angleLeftDisplay.textContent = data.angle_left !== undefined ? data.angle_left.toFixed(2) : 'N/A';
60
+ if (angleRightDisplay) angleRightDisplay.textContent = data.angle_right !== undefined ? data.angle_right.toFixed(2) : 'N/A';
 
 
 
 
61
  }
62
+
63
+ function updateHammerCurlUI(data) {
64
+ if (repsLeftHcDisplay) repsLeftHcDisplay.textContent = data.counter_left !== undefined ? data.counter_left : 'N/A';
65
+ if (repsRightHcDisplay) repsRightHcDisplay.textContent = data.counter_right !== undefined ? data.counter_right : 'N/A';
66
+ if (stageLeftHcDisplay) stageLeftHcDisplay.textContent = data.stage_left || 'N/A';
67
+ if (stageRightHcDisplay) stageRightHcDisplay.textContent = data.stage_right || 'N/A';
68
+ if (feedbackLeftHcDisplay) feedbackLeftHcDisplay.textContent = data.feedback_left || 'N/A';
69
+ if (feedbackRightHcDisplay) feedbackRightHcDisplay.textContent = data.feedback_right || 'N/A';
70
+ if (angleLeftCurlHcDisplay) angleLeftCurlHcDisplay.textContent = data.angle_left_curl !== undefined ? data.angle_left_curl.toFixed(2) : 'N/A';
71
+ if (angleRightCurlHcDisplay) angleRightCurlHcDisplay.textContent = data.angle_right_curl !== undefined ? data.angle_right_curl.toFixed(2) : 'N/A';
 
 
72
  }
 
73
 
74
 
75
+ function initializeSocket() {
76
+ if (socket && socket.connected) {
77
+ logger.info("Socket already connected.");
78
+ return;
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
+ socket = io({
81
+ reconnection: true,
82
+ reconnectionAttempts: 5,
83
+ reconnectionDelay: 1000,
84
+ });
 
 
 
85
 
86
+ socket.on('connect', () => {
87
+ logger.info(`Connected to server with SID: ${socket.id}`);
88
+ apiStatusDisplay.textContent = "Connected to server.";
89
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ socket.on('disconnect', (reason) => {
92
+ logger.info(`Disconnected from server: ${reason}`);
93
+ apiStatusDisplay.textContent = "Disconnected. Please refresh.";
94
+ stopTrainer(false); // Stop without emitting to server as it's already disconnected
95
+ });
 
 
 
96
 
97
+ socket.on('connect_error', (error) => {
98
+ logger.error(`Connection Error: ${error.message}`);
99
+ apiStatusDisplay.textContent = "Connection Error.";
100
+ });
101
+
102
+ socket.on('connection_ack', (data) => {
103
+ logger.info(`Server Acknowledged Connection: ${JSON.stringify(data)}`);
104
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ socket.on('session_started', (data) => {
107
+ logger.info(`Session started on server: ${data.session_id} for exercise: ${data.exercise_type}`);
108
+ isSessionActive = true;
109
+ apiStatusDisplay.textContent = `Session for ${data.exercise_type} started.`;
110
+ // Now that session is confirmed, start the frame loop
111
+ sendFrameLoop();
112
+ });
113
 
114
+ socket.on('session_error', (data) => {
115
+ logger.error(`Session error: ${data.error}`);
116
+ apiStatusDisplay.textContent = `Session Error: ${data.error}`;
117
+ alert(`Session Error: ${data.error}`);
118
+ stopTrainer(false); // Stop client side operations
119
+ });
120
 
121
+ socket.on('exercise_update', (data) => {
122
+ if (data.success) {
123
+ if (data.landmarks_detected) {
124
+ // logger.log('Exercise Update:', data.data); // Can be verbose
125
+ if (currentExerciseType === 'hammer_curl') {
126
+ updateHammerCurlUI(data.data);
127
+ } else {
128
+ updateGenericUI(data.data);
129
+ }
130
+ } else {
131
+ feedbackDisplay.textContent = data.message || 'No landmarks detected.';
132
+ }
133
+ } else {
134
+ logger.error(`Exercise update error: ${data.message || 'Unknown error'}`);
135
+ feedbackDisplay.textContent = `Error: ${data.message || 'Unknown error'}`;
 
 
136
  }
137
  });
 
 
138
 
139
+ socket.on('frame_error', (data) => {
140
+ logger.error(`Frame processing error from server: ${data.error}`);
141
+ feedbackDisplay.textContent = `Server Error: ${data.error}`;
142
+ });
143
+ }
 
 
 
 
 
 
144
 
145
+ async function startTrainer() {
146
+ if (isSessionActive) {
147
+ logger.info("Session already active. Please stop current session first.");
148
+ return;
149
+ }
150
 
151
+ currentExerciseType = exerciseTypeSelect.value;
152
+ setupUIForExercise(currentExerciseType);
153
+ logger.info(`Attempting to start exercise: ${currentExerciseType}`);
154
+ apiStatusDisplay.textContent = "Starting session...";
155
+
156
+ if (!socket || !socket.connected) {
157
+ logger.info("Socket not connected. Initializing socket connection...");
158
+ initializeSocket();
159
+ // Wait for connection before proceeding
160
+ // This can be handled by having the 'session_started' event trigger sendFrameLoop
161
+ }
162
+
163
+ // Emit start_exercise_session once socket is connected
164
+ // The 'connect' event listener will now handle the initial connection_ack
165
+ // We will emit 'start_exercise_session' after connection is confirmed
166
+ const attemptStartSession = () => {
167
+ if (socket && socket.connected) {
168
+ logger.info(`Socket connected, emitting start_exercise_session for ${currentExerciseType}`);
169
+ socket.emit('start_exercise_session', { exercise_type: currentExerciseType });
170
  } else {
171
+ logger.info("Waiting for socket connection to start session...");
172
+ setTimeout(attemptStartSession, 500); // Retry after a short delay
173
  }
174
+ };
175
+
176
+ attemptStartSession();
177
+
178
+
179
+ try {
180
+ stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
181
+ videoElement.srcObject = stream;
182
+ videoElement.onloadedmetadata = () => {
183
+ logger.info("Video metadata loaded.");
184
+ canvasElement.width = videoElement.videoWidth;
185
+ canvasElement.height = videoElement.videoHeight;
186
+ videoElement.play().then(() => {
187
+ logger.info("Video playback started.");
188
+ // sendFrameLoop() is now called upon 'session_started' event from server
189
+ startButton.disabled = true; // Corrected from startBtn to startButton
190
+ stopButton.disabled = false; // Corrected from stopBtn to stopButton
191
+ exerciseTypeSelect.disabled = true;
192
+ }).catch(playError => {
193
+ logger.error(`Error playing video: ${playError}`);
194
+ apiStatusDisplay.textContent = "Error playing video.";
195
+ alert(`Error playing video: ${playError.message}`);
196
+ stopTrainer(false); // Clean up if play fails
197
+ });
198
+ };
199
+ videoElement.oncanplay = () => {
200
+ logger.info("Video can play.");
201
+ };
202
+ videoElement.onplaying = () => {
203
+ logger.info("Video is now playing.");
204
+ };
205
+ videoElement.onpause = () => {
206
+ logger.info("Video paused.");
207
+ };
208
+ videoElement.onended = () => {
209
+ logger.info("Video ended.");
210
+ };
211
+
212
+ } catch (err) {
213
+ logger.error(`Error accessing webcam: ${err}`);
214
+ apiStatusDisplay.textContent = "Error accessing webcam.";
215
+ alert(`Could not access webcam: ${err.message}`);
216
  }
 
 
 
 
217
  }
 
218
 
219
+ function stopTrainer(emitToServer = true) {
220
+ logger.info("Stop trainer called.");
221
+ isSessionActive = false;
222
+ if (animationFrameId) {
223
+ cancelAnimationFrame(animationFrameId);
224
+ animationFrameId = null;
225
+ logger.info("Frame loop cancelled.");
226
+ }
227
 
228
+ if (stream) {
229
+ stream.getTracks().forEach(track => track.stop());
230
+ logger.info("Webcam tracks stopped.");
231
+ }
232
+ videoElement.srcObject = null;
 
 
 
 
 
 
233
 
234
+ if (socket && socket.connected && emitToServer) {
235
+ // No specific 'end_exercise_session' event needed as server handles 'disconnect'
236
+ // but if you want explicit cleanup, you can add one.
237
+ // socket.emit('end_exercise_session', { session_id: socket.id });
238
+ logger.info("Signaling server for session end (via disconnect).");
239
+ }
240
+
241
+ // If you want to fully disconnect and clean up the socket on stop:
242
+ // if (socket) {
243
+ // socket.disconnect();
244
+ // socket = null;
245
+ // logger.info("Socket disconnected.");
246
+ // }
247
+
248
+
249
+ apiStatusDisplay.textContent = "Trainer stopped.";
250
+ startButton.disabled = false; // Corrected from startBtn to startButton
251
+ stopButton.disabled = true; // Corrected from stopBtn to stopButton
252
+ exerciseTypeSelect.disabled = false;
253
+
254
+ // Reset UI elements
255
+ if (repsDisplay) repsDisplay.textContent = '0';
256
+ if (stageDisplay) stageDisplay.textContent = 'N/A';
257
+ if (feedbackDisplay) feedbackDisplay.textContent = 'N/A';
258
+ if (angleLeftDisplay) angleLeftDisplay.textContent = '0';
259
+ if (angleRightDisplay) angleRightDisplay.textContent = '0';
260
+ if (repsLeftHcDisplay) repsLeftHcDisplay.textContent = '0';
261
+ if (repsRightHcDisplay) repsRightHcDisplay.textContent = '0';
262
+ if (stageLeftHcDisplay) stageLeftHcDisplay.textContent = 'N/A';
263
+ if (stageRightHcDisplay) stageRightHcDisplay.textContent = 'N/A';
264
+ if (feedbackLeftHcDisplay) feedbackLeftHcDisplay.textContent = 'N/A';
265
+ if (feedbackRightHcDisplay) feedbackRightHcDisplay.textContent = 'N/A';
266
+ if (angleLeftCurlHcDisplay) angleLeftCurlHcDisplay.textContent = '0';
267
+ if (angleRightCurlHcDisplay) angleRightCurlHcDisplay.textContent = '0';
268
+ if (sessionIdDisplay) sessionIdDisplay.textContent = '-';
269
+ }
270
 
271
+ async function sendFrameLoop() {
272
+ if (!isSessionActive || !videoElement.srcObject || videoElement.paused || videoElement.ended || videoElement.readyState < videoElement.HAVE_FUTURE_DATA) {
273
+ logger.info(`Stopping frame loop. Conditions: isSessionActive=${isSessionActive}, srcObject=${!!videoElement.srcObject}, paused=${videoElement.paused}, ended=${videoElement.ended}, readyState=${videoElement.readyState}`);
274
+ if (animationFrameId) {
275
+ cancelAnimationFrame(animationFrameId);
276
+ animationFrameId = null;
277
+ }
278
+ // If session is supposed to be active but video state is bad, consider stopping the session.
279
+ if (isSessionActive && (videoElement.paused || videoElement.ended || videoElement.readyState < videoElement.HAVE_FUTURE_DATA)) {
280
+ logger.warn("Video stream issue detected, stopping session from client-side.");
281
+ // stopTrainer(); // This might cause a loop if called from here. Better to just stop sending.
282
  }
 
 
283
  return;
284
  }
285
 
286
+ try {
287
+ if (canvasElement.width > 0 && canvasElement.height > 0) {
288
+ context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
289
+ const imageDataBase64 = canvasElement.toDataURL('image/jpeg', 0.7).split(',')[1];
290
+
291
+ if (socket && socket.connected) {
292
+ socket.emit('process_frame', {
293
+ image: imageDataBase64,
294
+ exercise_type: currentExerciseType,
295
+ frame_width: canvasElement.width,
296
+ frame_height: canvasElement.height
297
+ });
298
+ } else {
299
+ logger.error("Socket not connected in sendFrameLoop. Cannot send frame.");
300
+ stopTrainer(false); // Stop if socket disconnected
301
+ }
302
+ } else {
303
+ logger.warn("[sendFrameLoop] Canvas dimensions are zero or video not ready, skipping frame draw/send.");
304
+ }
305
+ } catch (error) {
306
+ logger.error("Error in sendFrameLoop:", error);
307
+ stopTrainer(false); // Stop on error
308
+ }
309
 
310
+ if (isSessionActive) { // Check again before queuing next frame
311
+ animationFrameId = requestAnimationFrame(sendFrameLoop);
312
+ }
313
+ }
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
+ // Initialize Socket.IO connection when the script loads
316
+ initializeSocket();
317
 
318
+ startButton.addEventListener('click', startTrainer);
319
+ stopButton.addEventListener('click', () => stopTrainer(true));
320
+
321
+ // Initial UI setup based on default selected exercise
322
+ if (exerciseTypeSelect) {
323
+ setupUIForExercise(exerciseTypeSelect.value);
324
+ exerciseTypeSelect.addEventListener('change', (event) => {
325
+ if (!isSessionActive) { // Only allow changing exercise type if session is not active
326
+ currentExerciseType = event.target.value;
327
+ setupUIForExercise(currentExerciseType);
328
+ }
329
+ });
330
+ }
 
 
331
  });