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