339 lines
11 KiB
HTML
339 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>USDA Vision Camera Live Preview</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background-color: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
h1 {
|
|
color: #333;
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.camera-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.camera-card {
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
.camera-title {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
color: #333;
|
|
}
|
|
|
|
.camera-stream {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
height: auto;
|
|
border: 2px solid #ddd;
|
|
border-radius: 4px;
|
|
background-color: #000;
|
|
min-height: 200px;
|
|
display: block;
|
|
}
|
|
|
|
.camera-controls {
|
|
margin-top: 10px;
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: #007bff;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color: #545b62;
|
|
}
|
|
|
|
.btn-success {
|
|
background-color: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background-color: #1e7e34;
|
|
}
|
|
|
|
.btn-danger {
|
|
background-color: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background-color: #c82333;
|
|
}
|
|
|
|
.status {
|
|
margin-top: 10px;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.status-success {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
|
|
.status-error {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.status-info {
|
|
background-color: #d1ecf1;
|
|
color: #0c5460;
|
|
border: 1px solid #bee5eb;
|
|
}
|
|
|
|
.system-info {
|
|
margin-top: 30px;
|
|
padding: 15px;
|
|
background-color: #e9ecef;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.system-info h3 {
|
|
margin-top: 0;
|
|
color: #495057;
|
|
}
|
|
|
|
.api-info {
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
color: #6c757d;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="container">
|
|
<h1>🎥 USDA Vision Camera Live Preview</h1>
|
|
|
|
<div class="camera-grid" id="cameraGrid">
|
|
<!-- Camera cards will be dynamically generated -->
|
|
</div>
|
|
|
|
<div class="system-info">
|
|
<h3>📡 System Information</h3>
|
|
<div id="systemStatus">Loading system status...</div>
|
|
|
|
<h3>🔗 API Endpoints</h3>
|
|
<div class="api-info">
|
|
<p><strong>Live Stream:</strong> GET /cameras/{camera_name}/stream</p>
|
|
<p><strong>Start Stream:</strong> POST /cameras/{camera_name}/start-stream</p>
|
|
<p><strong>Stop Stream:</strong> POST /cameras/{camera_name}/stop-stream</p>
|
|
<p><strong>Camera Status:</strong> GET /cameras</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:8000';
|
|
let cameras = {};
|
|
|
|
// Initialize the page
|
|
async function init() {
|
|
await loadCameras();
|
|
await loadSystemStatus();
|
|
|
|
// Refresh status every 5 seconds
|
|
setInterval(loadSystemStatus, 5000);
|
|
}
|
|
|
|
// Load camera information
|
|
async function loadCameras() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/cameras`);
|
|
const data = await response.json();
|
|
cameras = data;
|
|
renderCameras();
|
|
} catch (error) {
|
|
console.error('Error loading cameras:', error);
|
|
showError('Failed to load camera information');
|
|
}
|
|
}
|
|
|
|
// Load system status
|
|
async function loadSystemStatus() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/system/status`);
|
|
const data = await response.json();
|
|
|
|
const statusDiv = document.getElementById('systemStatus');
|
|
statusDiv.innerHTML = `
|
|
<p><strong>System:</strong> ${data.status}</p>
|
|
<p><strong>Uptime:</strong> ${data.uptime}</p>
|
|
<p><strong>API Server:</strong> ${data.api_server_running ? '✅ Running' : '❌ Stopped'}</p>
|
|
<p><strong>Camera Manager:</strong> ${data.camera_manager_running ? '✅ Running' : '❌ Stopped'}</p>
|
|
<p><strong>MQTT Client:</strong> ${data.mqtt_client_connected ? '✅ Connected' : '❌ Disconnected'}</p>
|
|
`;
|
|
} catch (error) {
|
|
console.error('Error loading system status:', error);
|
|
document.getElementById('systemStatus').innerHTML = '<p style="color: red;">Failed to load system status</p>';
|
|
}
|
|
}
|
|
|
|
// Render camera cards
|
|
function renderCameras() {
|
|
const grid = document.getElementById('cameraGrid');
|
|
grid.innerHTML = '';
|
|
|
|
for (const [cameraName, cameraInfo] of Object.entries(cameras)) {
|
|
const card = createCameraCard(cameraName, cameraInfo);
|
|
grid.appendChild(card);
|
|
}
|
|
}
|
|
|
|
// Create a camera card
|
|
function createCameraCard(cameraName, cameraInfo) {
|
|
const card = document.createElement('div');
|
|
card.className = 'camera-card';
|
|
card.innerHTML = `
|
|
<div class="camera-title">${cameraName}</div>
|
|
<img class="camera-stream" id="stream-${cameraName}"
|
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPk5vIFN0cmVhbTwvdGV4dD48L3N2Zz4="
|
|
alt="Camera Stream">
|
|
<div class="camera-controls">
|
|
<button class="btn btn-success" onclick="startStream('${cameraName}')">Start Stream</button>
|
|
<button class="btn btn-danger" onclick="stopStream('${cameraName}')">Stop Stream</button>
|
|
<button class="btn btn-secondary" onclick="refreshStream('${cameraName}')">Refresh</button>
|
|
</div>
|
|
<div class="status status-info" id="status-${cameraName}">
|
|
Status: ${cameraInfo.status} | Recording: ${cameraInfo.is_recording ? 'Yes' : 'No'}
|
|
</div>
|
|
`;
|
|
return card;
|
|
}
|
|
|
|
// Start streaming for a camera
|
|
async function startStream(cameraName) {
|
|
try {
|
|
updateStatus(cameraName, 'Starting stream...', 'info');
|
|
|
|
// Start the stream
|
|
const response = await fetch(`${API_BASE}/cameras/${cameraName}/start-stream`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Set the stream source
|
|
const streamImg = document.getElementById(`stream-${cameraName}`);
|
|
streamImg.src = `${API_BASE}/cameras/${cameraName}/stream?t=${Date.now()}`;
|
|
|
|
updateStatus(cameraName, 'Stream started successfully', 'success');
|
|
} else {
|
|
const error = await response.text();
|
|
updateStatus(cameraName, `Failed to start stream: ${error}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error starting stream:', error);
|
|
updateStatus(cameraName, `Error starting stream: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Stop streaming for a camera
|
|
async function stopStream(cameraName) {
|
|
try {
|
|
updateStatus(cameraName, 'Stopping stream...', 'info');
|
|
|
|
const response = await fetch(`${API_BASE}/cameras/${cameraName}/stop-stream`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Clear the stream source
|
|
const streamImg = document.getElementById(`stream-${cameraName}`);
|
|
streamImg.src = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPk5vIFN0cmVhbTwvdGV4dD48L3N2Zz4=";
|
|
|
|
updateStatus(cameraName, 'Stream stopped successfully', 'success');
|
|
} else {
|
|
const error = await response.text();
|
|
updateStatus(cameraName, `Failed to stop stream: ${error}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error stopping stream:', error);
|
|
updateStatus(cameraName, `Error stopping stream: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Refresh stream for a camera
|
|
function refreshStream(cameraName) {
|
|
const streamImg = document.getElementById(`stream-${cameraName}`);
|
|
if (streamImg.src.includes('/stream')) {
|
|
streamImg.src = `${API_BASE}/cameras/${cameraName}/stream?t=${Date.now()}`;
|
|
updateStatus(cameraName, 'Stream refreshed', 'info');
|
|
} else {
|
|
updateStatus(cameraName, 'No active stream to refresh', 'error');
|
|
}
|
|
}
|
|
|
|
// Update status message
|
|
function updateStatus(cameraName, message, type) {
|
|
const statusDiv = document.getElementById(`status-${cameraName}`);
|
|
statusDiv.className = `status status-${type}`;
|
|
statusDiv.textContent = message;
|
|
}
|
|
|
|
// Show error message
|
|
function showError(message) {
|
|
alert(`Error: ${message}`);
|
|
}
|
|
|
|
// Initialize when page loads
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
</script>
|
|
</body>
|
|
|
|
</html> |