init commit

This commit is contained in:
2025-12-03 18:55:22 +09:00
commit f05e8e728f
12 changed files with 6325 additions and 0 deletions

729
templates/stream.html Normal file
View File

@@ -0,0 +1,729 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stream: {{ client.id }} - Video Streaming Server</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #667eea;
--secondary: #764ba2;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--dark: #1f2937;
--light: #f9fafb;
--gray: #6b7280;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a1a;
color: white;
height: 100vh;
overflow: hidden;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background: rgba(0, 0, 0, 0.8);
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.back-btn {
display: flex;
align-items: center;
gap: 8px;
color: white;
text-decoration: none;
padding: 8px 16px;
background: rgba(255,255,255,0.1);
border-radius: 5px;
transition: background 0.3s;
}
.back-btn:hover {
background: rgba(255,255,255,0.2);
}
.client-info h2 {
font-size: 18px;
margin-bottom: 5px;
}
.client-info p {
font-size: 12px;
color: #aaa;
}
.header-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
background: rgba(255,255,255,0.1);
color: white;
}
.btn:hover {
background: rgba(255,255,255,0.2);
transform: translateY(-2px);
}
.btn-primary {
background: var(--primary);
}
.btn-danger {
background: var(--danger);
}
/* Main Content */
.main-content {
display: flex;
height: calc(100vh - 70px);
}
/* Video Container */
.video-container {
flex: 3;
display: flex;
flex-direction: column;
background: #000;
position: relative;
}
.video-placeholder {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #666;
}
.video-placeholder i {
font-size: 100px;
margin-bottom: 20px;
opacity: 0.3;
}
.video-placeholder h3 {
font-size: 24px;
margin-bottom: 10px;
}
.video-placeholder p {
font-size: 14px;
text-align: center;
max-width: 400px;
}
.video-stats {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 5px;
font-size: 12px;
}
/* Controls Panel */
.controls-panel {
flex: 1;
max-width: 300px;
background: rgba(30, 30, 30, 0.95);
padding: 20px;
overflow-y: auto;
border-left: 1px solid rgba(255,255,255,0.1);
}
.controls-section {
margin-bottom: 30px;
}
.controls-section h3 {
font-size: 16px;
margin-bottom: 15px;
color: #ddd;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 10px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 5px;
color: #aaa;
font-size: 12px;
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
}
input[type="range"] {
flex: 1;
height: 5px;
-webkit-appearance: none;
background: rgba(255,255,255,0.1);
border-radius: 5px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
}
.slider-value {
min-width: 30px;
text-align: right;
font-size: 12px;
color: #ddd;
}
.btn-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 10px;
}
.btn-control {
padding: 10px;
text-align: center;
}
.btn-control i {
font-size: 20px;
margin-bottom: 5px;
}
.btn-control span {
display: block;
font-size: 11px;
}
/* Connection Info */
.info-box {
background: rgba(0, 0, 0, 0.3);
border-radius: 5px;
padding: 15px;
margin-bottom: 15px;
}
.info-item {
margin-bottom: 8px;
font-size: 12px;
}
.info-item label {
display: inline-block;
width: 100px;
color: #aaa;
}
/* Status Indicator */
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
}
.status-connected {
background: var(--success);
box-shadow: 0 0 10px var(--success);
}
.status-disconnected {
background: var(--danger);
box-shadow: 0 0 10px var(--danger);
}
/* WebSocket Status */
.ws-status {
position: fixed;
bottom: 10px;
right: 10px;
padding: 5px 10px;
border-radius: 3px;
font-size: 11px;
background: rgba(0, 0, 0, 0.7);
}
.ws-connected {
color: var(--success);
}
.ws-disconnected {
color: var(--danger);
}
/* Loading */
.loading {
text-align: center;
padding: 50px;
color: #666;
}
/* Responsive */
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.controls-panel {
max-width: none;
height: 300px;
}
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<div class="header-left">
<a href="/room/{{ client.room_id }}" class="back-btn">
<i class="fas fa-arrow-left"></i>
Back to Room
</a>
<div class="client-info">
<h2>Streaming: {{ client.id[:8] }}...</h2>
<p>
<span class="status-indicator status-connected"></span>
Connected from {{ client.ip_address }}
</p>
</div>
</div>
<div class="header-actions">
<button class="btn btn-danger" onclick="disconnectClient()">
<i class="fas fa-sign-out-alt"></i> Disconnect
</button>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Video Container -->
<div class="video-container">
<div class="video-placeholder" id="videoPlaceholder">
<i class="fas fa-video"></i>
<h3>Waiting for Video Stream</h3>
<p>Video stream will appear here once the client starts streaming.</p>
<p>Use the controls on the right to adjust video settings.</p>
</div>
<div class="video-stats" id="videoStats" style="display: none;">
<div>Resolution: <span id="statsResolution">640x480</span></div>
<div>Frame Rate: <span id="statsFps">30</span> FPS</div>
<div>Quality: <span id="statsQuality">85</span>%</div>
</div>
</div>
<!-- Controls Panel -->
<div class="controls-panel">
<!-- Connection Info -->
<div class="info-box">
<div class="info-item">
<label>Client ID:</label>
<span>{{ client.id }}</span>
</div>
<div class="info-item">
<label>Room:</label>
<span>{{ room.name if room else 'Unknown' }}</span>
</div>
<div class="info-item">
<label>Connected:</label>
<span>{{ client.connected_at }}</span>
</div>
<div class="info-item">
<label>Streaming:</label>
<span id="streamingStatus">{{ "Yes" if client.is_streaming else "No" }}</span>
</div>
</div>
<!-- Video Controls -->
<div class="controls-section">
<h3>Video Controls</h3>
<div class="control-group">
<label>Quality</label>
<div class="slider-container">
<input type="range" id="qualitySlider" min="10" max="100" value="{{ client.video_settings.quality }}">
<span class="slider-value" id="qualityValue">{{ client.video_settings.quality }}%</span>
</div>
</div>
<div class="control-group">
<label>Brightness</label>
<div class="slider-container">
<input type="range" id="brightnessSlider" min="-100" max="100" value="0">
<span class="slider-value" id="brightnessValue">0</span>
</div>
</div>
<div class="control-group">
<label>Contrast</label>
<div class="slider-container">
<input type="range" id="contrastSlider" min="0.5" max="2" step="0.1" value="1">
<span class="slider-value" id="contrastValue">1.0</span>
</div>
</div>
<div class="btn-group">
<button class="btn btn-control" onclick="sendCommand('rotate', {angle: 90})">
<i class="fas fa-redo"></i>
<span>Rotate 90°</span>
</button>
<button class="btn btn-control" onclick="sendCommand('rotate', {angle: 180})">
<i class="fas fa-sync-alt"></i>
<span>Rotate 180°</span>
</button>
<button class="btn btn-control" onclick="sendCommand('flip', {direction: 1})">
<i class="fas fa-arrows-alt-v"></i>
<span>Flip Vertical</span>
</button>
<button class="btn btn-control" onclick="sendCommand('flip', {direction: 0})">
<i class="fas fa-arrows-alt-h"></i>
<span>Flip Horizontal</span>
</button>
<button class="btn btn-control" onclick="sendCommand('grayscale', {})">
<i class="fas fa-adjust"></i>
<span>Grayscale</span>
</button>
<button class="btn btn-control" onclick="sendCommand('reset', {})">
<i class="fas fa-undo"></i>
<span>Reset All</span>
</button>
</div>
</div>
<!-- Stream Information -->
<div class="controls-section">
<h3>Stream Information</h3>
<div class="info-box">
<p>This client is connected via WebSocket and streaming video data.</p>
<p>Use the controls above to adjust the video stream in real-time.</p>
<p>Changes are applied to the video processor on the server side.</p>
</div>
</div>
</div>
</div>
<!-- WebSocket Status -->
<div class="ws-status" id="wsStatus">
<span class="status-indicator status-disconnected"></span>
<span id="wsStatusText">Connecting...</span>
</div>
<script>
let ws = null;
let clientId = '{{ client.id }}';
let sessionId = getCookie('session_id');
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
// Initialize WebSocket connection
function connectWebSocket() {
if (!sessionId) {
console.error('No session ID found');
return;
}
const wsUrl = `ws://${window.location.hostname}:{{ server_port }}/ws/admin/${sessionId}`;
try {
ws = new WebSocket(wsUrl);
ws.onopen = function() {
console.log('WebSocket connected');
updateWsStatus(true);
reconnectAttempts = 0;
// Request to watch this client
sendCommand('watch_client', { client_id: clientId });
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onclose = function() {
console.log('WebSocket disconnected');
updateWsStatus(false);
// Try to reconnect
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
}
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
updateWsStatus(false);
};
} catch (error) {
console.error('Error creating WebSocket:', error);
updateWsStatus(false);
}
}
// Update WebSocket status display
function updateWsStatus(connected) {
const wsStatus = document.getElementById('wsStatus');
const wsStatusText = document.getElementById('wsStatusText');
if (connected) {
wsStatus.className = 'ws-status';
wsStatusText.innerHTML = '<span class="status-indicator status-connected"></span> Connected';
} else {
wsStatus.className = 'ws-status';
wsStatusText.innerHTML = `<span class="status-indicator status-disconnected"></span> Disconnected (${reconnectAttempts}/${maxReconnectAttempts})`;
}
}
// Handle incoming WebSocket messages
function handleWebSocketMessage(data) {
console.log('Received:', data);
switch (data.type) {
case 'stream_started':
showNotification('Stream started successfully');
updateStreamingStatus(true);
break;
case 'stream_info':
showNotification(data.message);
break;
case 'control_response':
if (data.success) {
showNotification('Command executed successfully');
} else {
showNotification('Command failed', 'error');
}
break;
case 'stats_update':
// Update any stats if needed
break;
}
}
// Send command to WebSocket
function sendCommand(type, data) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
showNotification('WebSocket not connected', 'error');
return;
}
const command = {
type: 'control_client',
client_id: clientId,
command: {
type: type,
...data
}
};
ws.send(JSON.stringify(command));
console.log('Sent command:', command);
}
// Update streaming status
function updateStreamingStatus(streaming) {
document.getElementById('streamingStatus').textContent = streaming ? 'Yes' : 'No';
if (streaming) {
document.getElementById('videoStats').style.display = 'block';
document.getElementById('videoPlaceholder').innerHTML = `
<i class="fas fa-sync-alt fa-spin"></i>
<h3>Streaming Live Video</h3>
<p>Video is being streamed from the client.</p>
<p>Use controls to adjust the stream.</p>
`;
}
}
// Show notification
function showNotification(message, type = 'success') {
// Create notification element
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: ${type === 'error' ? '#ef4444' : '#10b981'};
color: white;
border-radius: 5px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
notification.textContent = message;
document.body.appendChild(notification);
// Remove after 3 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-in';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// Disconnect client
async function disconnectClient() {
if (confirm('Are you sure you want to disconnect this client?')) {
try {
const response = await fetch(`/api/disconnect-client/${clientId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
showNotification('Client disconnected successfully');
setTimeout(() => {
window.location.href = `/room/{{ client.room_id }}`;
}, 1000);
} else {
showNotification('Error: ' + (result.error || 'Failed to disconnect client'), 'error');
}
} catch (error) {
showNotification('Error: ' + error.message, 'error');
}
}
}
// Initialize sliders
function initializeSliders() {
// Quality slider
const qualitySlider = document.getElementById('qualitySlider');
const qualityValue = document.getElementById('qualityValue');
qualitySlider.addEventListener('input', function() {
qualityValue.textContent = this.value + '%';
document.getElementById('statsQuality').textContent = this.value;
});
qualitySlider.addEventListener('change', function() {
sendCommand('adjust_quality', { quality: parseInt(this.value) });
});
// Brightness slider
const brightnessSlider = document.getElementById('brightnessSlider');
const brightnessValue = document.getElementById('brightnessValue');
brightnessSlider.addEventListener('input', function() {
brightnessValue.textContent = this.value;
});
brightnessSlider.addEventListener('change', function() {
sendCommand('brightness', { value: parseInt(this.value) });
});
// Contrast slider
const contrastSlider = document.getElementById('contrastSlider');
const contrastValue = document.getElementById('contrastValue');
contrastSlider.addEventListener('input', function() {
contrastValue.textContent = parseFloat(this.value).toFixed(1);
});
contrastSlider.addEventListener('change', function() {
sendCommand('contrast', { value: parseFloat(this.value) });
});
}
// Initialize
window.addEventListener('load', function() {
if (!sessionId) {
window.location.href = '/';
return;
}
connectWebSocket();
initializeSliders();
// Add CSS animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
});
</script>
</body>
</html>