liveTrain / live_test.html
pjxcharya's picture
Create live_test.html
067e5d2 verified
<!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; } /* Flip video for mirror effect */
#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');
// It's good practice to create the canvas in memory if it's not needed in the DOM
const canvasElement = document.createElement('canvas');
const context = canvasElement.getContext('2d', { willReadFrequently: true }); // Added willReadFrequently for performance
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; // To control the loop
// API Endpoints - ensure your Flask app is running on this address
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();
}
// Basic fallback for older browsers
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"; // Reset feedback
repsDisplay.textContent = "0";
stageDisplay.textContent = "N/A";
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
videoElement.srcObject = stream;
// Ensure onloadedmetadata is used, as it waits for video dimensions
videoElement.onloadedmetadata = () => {
videoElement.play().then(() => { // Ensure play is successful
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; // Signal to stop the loop
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
apiStatusDisplay.textContent = "Stopping session...";
if (videoElement.srcObject) {
videoElement.srcObject.getTracks().forEach(track => track.stop());
videoElement.srcObject = null; // Release camera
}
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; // Reset session ID
sessionIdDisplay.textContent = "-";
startBtn.disabled = false;
stopBtn.disabled = true;
exerciseTypeSelect.disabled = false;
// Optionally reset feedback displays here or leave them with last known state
});
async function sendFrameLoop() {
if (!streamActive || !currentSessionId || !videoElement.srcObject || videoElement.paused || videoElement.ended || videoElement.readyState < 3) {
// video.readyState < 3 means not enough data to play the current frame
if(streamActive) { // If we intended to stream, try again shortly
animationFrameId = requestAnimationFrame(sendFrameLoop);
} else {
apiStatusDisplay.textContent = "Stream stopped or video not ready.";
}
return;
}
context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
// Use a lower quality for faster encoding if network is slow, e.g., 0.5 or 0.6
const imageDataBase64 = canvasElement.toDataURL('image/jpeg', 0.7).split(',')[1];
const selectedExerciseType = exerciseTypeSelect.value;
try {
// apiStatusDisplay.textContent = "Sending frame..."; // This can be too flashy
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."; // Reset status on success
if (result.success && result.landmarks_detected) {
const data = result.data;
// console.log('API Data:', data); // For debugging
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.";
// Consider stopping the loop on persistent network errors
// For now, we'll let it keep trying if streamActive is true
}
if (streamActive) { // Continue the loop only if stream is supposed to be active
animationFrameId = requestAnimationFrame(sendFrameLoop);
}
}
// Gracefully handle page unload
window.addEventListener('beforeunload', () => {
if (currentSessionId) { // If a session is active
// This is a synchronous request, not ideal, but best effort for cleanup
// Modern browsers might block this or ignore it.
// A more robust solution would be a server-side timeout for inactive sessions.
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>