| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Live Fitness Trainer Test</title> |
| <style> |
| body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; margin: 0; padding: 20px; background-color: #f4f4f4; } |
| #controls { margin-bottom: 20px; padding: 15px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } |
| label, select, button { font-size: 1em; margin: 5px; } |
| button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } |
| button:disabled { background-color: #ccc; } |
| button:hover:not(:disabled) { background-color: #0056b3; } |
| #videoContainer { display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; margin-bottom: 20px; } |
| video { border: 2px solid #007bff; transform: scaleX(-1); border-radius: 8px; background-color: #000; } |
| #feedbackArea { border: 1px solid #ccc; padding: 15px; width: 320px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } |
| #feedbackArea h3 { margin-top: 0; color: #007bff; } |
| #feedbackArea p { margin: 8px 0; } |
| #feedbackArea span { font-weight: bold; color: #333; } |
| .hidden { display: none; } |
| </style> |
| </head> |
| <body> |
| <h1>Live Fitness Trainer Test</h1> |
|
|
| <div id="controls"> |
| <label for="exerciseType">Exercise:</label> |
| <select id="exerciseType"> |
| <option value="squat">Squat</option> |
| <option value="push_up">Push Up</option> |
| <option value="hammer_curl">Hammer Curl</option> |
| </select> |
| <button id="startTrainerBtn">Start Trainer</button> |
| <button id="stopTrainerBtn" disabled>Stop Trainer</button> |
| </div> |
|
|
| <div id="videoContainer"> |
| <div> |
| <h3>Your Webcam</h3> |
| <video id="userVideo" width="320" height="240" autoplay playsinline></video> |
| </div> |
| <div id="feedbackArea"> |
| <h3>Feedback & Status</h3> |
| <p>Session ID: <span id="sessionIdDisplay">-</span></p> |
| <p>API Status: <span id="apiStatus">Idle</span></p> |
| <hr> |
| <p>Reps: <span id="reps">0</span></p> |
| <p>Stage: <span id="stage">N/A</span></p> |
| <p>Feedback: <span id="feedback">N/A</span></p> |
| </div> |
| </div> |
|
|
| <script> |
| const videoElement = document.getElementById('userVideo'); |
| |
| const canvasElement = document.createElement('canvas'); |
| const context = canvasElement.getContext('2d', { willReadFrequently: true }); |
| |
| const startBtn = document.getElementById('startTrainerBtn'); |
| const stopBtn = document.getElementById('stopTrainerBtn'); |
| const exerciseTypeSelect = document.getElementById('exerciseType'); |
| |
| const sessionIdDisplay = document.getElementById('sessionIdDisplay'); |
| const repsDisplay = document.getElementById('reps'); |
| const stageDisplay = document.getElementById('stage'); |
| const feedbackDisplay = document.getElementById('feedback'); |
| const apiStatusDisplay = document.getElementById('apiStatus'); |
| |
| let currentSessionId = null; |
| let streamActive = false; |
| let animationFrameId = null; |
| |
| |
| const API_URL_TRACK = 'http://127.0.0.1:5000/api/track_exercise_stream'; |
| const API_URL_END_SESSION = 'http://127.0.0.1:5000/api/end_exercise_session'; |
| |
| function generateSessionId() { |
| if (typeof crypto.randomUUID === 'function') { |
| return crypto.randomUUID(); |
| } |
| |
| return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { |
| var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); |
| return v.toString(16); |
| }); |
| } |
| |
| startBtn.addEventListener('click', async () => { |
| currentSessionId = generateSessionId(); |
| sessionIdDisplay.textContent = currentSessionId; |
| const selectedExerciseType = exerciseTypeSelect.value; |
| console.log(`Starting session: ${currentSessionId} for ${selectedExerciseType}`); |
| apiStatusDisplay.textContent = "Initializing camera..."; |
| feedbackDisplay.textContent = "N/A"; |
| repsDisplay.textContent = "0"; |
| stageDisplay.textContent = "N/A"; |
| |
| |
| try { |
| const stream = await navigator.mediaDevices.getUserMedia({ video: true }); |
| videoElement.srcObject = stream; |
| |
| videoElement.onloadedmetadata = () => { |
| videoElement.play().then(() => { |
| canvasElement.width = videoElement.videoWidth; |
| canvasElement.height = videoElement.videoHeight; |
| streamActive = true; |
| startBtn.disabled = true; |
| stopBtn.disabled = false; |
| exerciseTypeSelect.disabled = true; |
| apiStatusDisplay.textContent = "Camera active. Starting stream..."; |
| sendFrameLoop(); |
| }).catch(playError => { |
| console.error("Error playing video:", playError); |
| apiStatusDisplay.textContent = "Error playing video."; |
| alert("Error playing video: " + playError.message); |
| }); |
| }; |
| } catch (err) { |
| console.error("Error accessing webcam:", err); |
| apiStatusDisplay.textContent = "Error accessing webcam."; |
| alert("Could not access webcam: " + err.message); |
| } |
| }); |
| |
| stopBtn.addEventListener('click', async () => { |
| streamActive = false; |
| if (animationFrameId) { |
| cancelAnimationFrame(animationFrameId); |
| animationFrameId = null; |
| } |
| apiStatusDisplay.textContent = "Stopping session..."; |
| |
| if (videoElement.srcObject) { |
| videoElement.srcObject.getTracks().forEach(track => track.stop()); |
| videoElement.srcObject = null; |
| } |
| |
| if (currentSessionId) { |
| console.log(`Ending session: ${currentSessionId}`); |
| try { |
| const response = await fetch(API_URL_END_SESSION, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ session_id: currentSessionId }) |
| }); |
| const result = await response.json(); |
| console.log('End session response:', result.message); |
| apiStatusDisplay.textContent = "Session ended."; |
| } catch (error) { |
| console.error('Error ending session:', error); |
| apiStatusDisplay.textContent = "Error ending session."; |
| } |
| } |
| |
| currentSessionId = null; |
| sessionIdDisplay.textContent = "-"; |
| startBtn.disabled = false; |
| stopBtn.disabled = true; |
| exerciseTypeSelect.disabled = false; |
| |
| }); |
| |
| async function sendFrameLoop() { |
| if (!streamActive || !currentSessionId || !videoElement.srcObject || videoElement.paused || videoElement.ended || videoElement.readyState < 3) { |
| |
| if(streamActive) { |
| animationFrameId = requestAnimationFrame(sendFrameLoop); |
| } else { |
| apiStatusDisplay.textContent = "Stream stopped or video not ready."; |
| } |
| return; |
| } |
| |
| context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height); |
| |
| const imageDataBase64 = canvasElement.toDataURL('image/jpeg', 0.7).split(',')[1]; |
| const selectedExerciseType = exerciseTypeSelect.value; |
| |
| try { |
| |
| const response = await fetch(API_URL_TRACK, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| session_id: currentSessionId, |
| exercise_type: selectedExerciseType, |
| image: imageDataBase64, |
| frame_width: canvasElement.width, |
| frame_height: canvasElement.height |
| }) |
| }); |
| |
| if (!response.ok) { |
| const errorData = await response.json(); |
| console.error('API Error:', errorData.error); |
| feedbackDisplay.textContent = `API Error: ${errorData.error}`; |
| apiStatusDisplay.textContent = `API Error.`; |
| } else { |
| const result = await response.json(); |
| apiStatusDisplay.textContent = "Frame processed."; |
| |
| if (result.success && result.landmarks_detected) { |
| const data = result.data; |
| |
| repsDisplay.textContent = data.counter !== undefined ? data.counter : `${data.counter_left || 0} (L) / ${data.counter_right || 0} (R)`; |
| stageDisplay.textContent = data.stage !== undefined ? data.stage : `${data.stage_left || 'N/A'} (L) / ${data.stage_right || 'N/A'} (R)`; |
| feedbackDisplay.textContent = data.feedback !== undefined ? data.feedback : `${data.feedback_left || ''} ${data.feedback_right || ''}`.trim() || "Processing..."; |
| } else if (result.success && !result.landmarks_detected) { |
| console.log('No landmarks detected in this frame.'); |
| feedbackDisplay.textContent = 'No landmarks detected. Adjust position?'; |
| } else { |
| feedbackDisplay.textContent = 'Error processing frame or unexpected response.'; |
| } |
| } |
| } catch (error) { |
| console.error('Network or other error sending frame:', error); |
| feedbackDisplay.textContent = "Network error. Is Flask server running?"; |
| apiStatusDisplay.textContent = "Network error."; |
| |
| |
| } |
| |
| if (streamActive) { |
| animationFrameId = requestAnimationFrame(sendFrameLoop); |
| } |
| } |
| |
| |
| window.addEventListener('beforeunload', () => { |
| if (currentSessionId) { |
| |
| |
| |
| const payload = JSON.stringify({ session_id: currentSessionId }); |
| navigator.sendBeacon(API_URL_END_SESSION, payload); |
| console.log('Attempted to end session on page unload via Beacon API.'); |
| } |
| }); |
| |
| </script> |
| </body> |
| </html> |