Compare commits

...

6 Commits

Author SHA1 Message Date
awe
3a4d9fe45d fix block 2025-11-21 16:48:51 +03:00
awe
eeec3582c9 fix? 2025-11-21 16:31:38 +03:00
awe
49a54616b9 Merge branch 'master' of ssh://git.radiophotonics.ru:2222/awe/radar_frontend 2025-11-21 16:21:17 +03:00
awe
3dc93f147b working version 2025-11-21 16:05:37 +03:00
b957fd7631 Merge remote-tracking branch 'refs/remotes/origin/master' 2025-11-20 16:46:06 +03:00
80c9590f86 requirents upd 2025-11-20 16:07:36 +03:00
8 changed files with 419 additions and 238 deletions

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
*.venv *.venv
*.pyc *.pyc
*segment*
*playlist*

Submodule beacon_track updated: f3775924eb...ce0d728eee

View File

@ -5,7 +5,7 @@ This application reads JPEG frames from shared memory and streams them
to web browsers via Server-Sent Events (SSE) or WebSocket (H.264 stream). to web browsers via Server-Sent Events (SSE) or WebSocket (H.264 stream).
""" """
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, StreamingResponse, Response from fastapi.responses import HTMLResponse, StreamingResponse, Response
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@ -15,7 +15,6 @@ import time
import base64 import base64
import json import json
import os import os
from typing import Set
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
# Global state # Global state
@ -26,11 +25,10 @@ last_frame_header = None
frame_lock = asyncio.Lock() frame_lock = asyncio.Lock()
# Video streaming state # Video streaming state
active_websocket_clients: Set[WebSocket] = set()
streaming_task = None streaming_task = None
streaming_active = False streaming_active = False
TCP_HOST = '127.0.0.1' VIDEO_FIFO_PATH = '/tmp/beacon_video_stream' # Named pipe for H.264 stream
TCP_PORT = 8888 video_fifo_file = None # Keep FIFO open to prevent C++ blocking
async def init_reader(): async def init_reader():
@ -46,23 +44,80 @@ async def init_reader():
reader = None reader = None
async def init_video_fifo():
"""Initialize video FIFO - open it to unblock C++ writer"""
global video_fifo_file
# Wait for C++ to create FIFO
for i in range(30):
if os.path.exists(VIDEO_FIFO_PATH):
break
print(f"Waiting for FIFO to be created by C++ ({i+1}/30)...")
await asyncio.sleep(1)
if not os.path.exists(VIDEO_FIFO_PATH):
print(f"WARNING: Video FIFO not found at {VIDEO_FIFO_PATH}")
return
try:
print(f"Opening FIFO {VIDEO_FIFO_PATH} for reading (non-blocking mode)...")
loop = asyncio.get_event_loop()
# Open file in NON-BLOCKING mode to prevent deadlock
# GStreamer filesink opens FIFO lazily (only when first frame arrives)
# Using O_NONBLOCK prevents Python from blocking while waiting for C++ writer
def open_fifo():
fd = os.open(VIDEO_FIFO_PATH, os.O_RDONLY | os.O_NONBLOCK)
return os.fdopen(fd, 'rb', buffering=0)
video_fifo_file = await loop.run_in_executor(None, open_fifo)
print(f"FIFO opened successfully in non-blocking mode")
except Exception as e:
print(f"Failed to open video FIFO: {e}")
async def cleanup_reader(): async def cleanup_reader():
"""Clean up shared memory reader""" """Clean up shared memory reader"""
global reader global reader, video_fifo_file
async with reader_lock: async with reader_lock:
if reader: if reader:
reader.close() reader.close()
reader = None reader = None
# Close video FIFO
if video_fifo_file:
try:
video_fifo_file.close()
print("Video FIFO closed")
except:
pass
video_fifo_file = None
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Lifespan context manager for startup and shutdown events""" """Lifespan context manager for startup and shutdown events"""
global streaming_task, streaming_active
# Startup # Startup
await init_reader() await init_reader()
await init_video_fifo()
# Start HLS streaming task
streaming_task = asyncio.create_task(stream_video_from_fifo())
print("Started HLS video streaming task")
print("FastAPI application started") print("FastAPI application started")
yield yield
# Shutdown # Shutdown
streaming_active = False
if streaming_task:
try:
await asyncio.wait_for(streaming_task, timeout=5.0)
except asyncio.TimeoutError:
print("HLS streaming task did not stop gracefully")
await cleanup_reader() await cleanup_reader()
print("FastAPI application shutdown") print("FastAPI application shutdown")
@ -207,123 +262,254 @@ async def latest_frame():
return Response(content="No frame available", status_code=404) return Response(content="No frame available", status_code=404)
async def stream_video_from_tcp(): async def stream_video_from_fifo():
""" """
Read MPEG-TS video stream from TCP socket and broadcast via WebSocket. Read raw H.264 from FIFO and convert to HLS format via ffmpeg.
This runs as a background task. This runs as a background task and generates HLS playlist + segments.
""" """
global streaming_active global streaming_active, video_fifo_file
import subprocess
print(f"Starting video stream reader from TCP: {TCP_HOST}:{TCP_PORT}") print(f"Starting HLS video stream generator from FIFO: {VIDEO_FIFO_PATH}")
# Wait a bit for C++ application to start TCP server ffmpeg_process = None
await asyncio.sleep(2) hls_dir = "hls_output"
# Create HLS output directory if it doesn't exist
if not os.path.exists(hls_dir):
os.makedirs(hls_dir)
try: try:
# Connect to TCP server # Use the globally opened FIFO file
print(f"Connecting to TCP server {TCP_HOST}:{TCP_PORT}...") if video_fifo_file is None:
print("ERROR: FIFO not opened during startup")
return
reader, writer = await asyncio.open_connection(TCP_HOST, TCP_PORT) fifo = video_fifo_file
print("Connected to TCP server, starting stream...") print("Connected to FIFO, starting ffmpeg H.264->HLS converter...")
# Start ffmpeg to convert raw H.264 to HLS format
# -probesize 32 -analyzeduration 0: minimal probing for low latency startup
# -f h264: input format is raw H.264 byte-stream (Annex B from GStreamer)
# -r 30: input framerate (from config.ini StreamFps)
# -i pipe:0: read from stdin
# -c:v copy: copy video codec without re-encoding (use hardware-encoded H.264)
# -f hls: output HLS format
# -hls_time 1: 1 second per segment (low latency)
# -hls_list_size 10: keep 10 segments in playlist (more buffer for clients)
# -hls_delete_threshold 3: delete segments only after 3 new ones created
# -hls_flags delete_segments+append_list+omit_endlist: live streaming flags
# -hls_segment_type mpegts: use MPEG-TS segments
# -hls_segment_filename: use %d for unlimited numbering
# -start_number 0: start segment numbering from 0
ffmpeg_process = subprocess.Popen(
['ffmpeg',
'-probesize', '32',
'-analyzeduration', '0',
'-f', 'h264',
'-r', '30', # Must match GStreamer StreamFps from config.ini
'-i', 'pipe:0',
'-c:v', 'copy',
'-f', 'hls',
'-hls_time', '1',
'-hls_list_size', '10',
'-hls_delete_threshold', '3',
'-hls_flags', 'delete_segments+append_list+omit_endlist',
'-hls_segment_type', 'mpegts',
'-hls_segment_filename', f'{hls_dir}/segment_%d.ts',
'-start_number', '0',
f'{hls_dir}/playlist.m3u8'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
streaming_active = True streaming_active = True
chunk_size = 32768 # 32KB chunks for MPEG-TS chunk_size = 65536 # 64KB chunks
loop = asyncio.get_event_loop()
bytes_written = 0
print(f"Reading raw H.264 from FIFO and generating HLS segments...")
async def read_from_fifo():
"""Read from FIFO and pipe to ffmpeg stdin"""
nonlocal bytes_written
writer_connected = False
while streaming_active and active_websocket_clients:
try: try:
# Read chunk from TCP while streaming_active:
data = await asyncio.wait_for(reader.read(chunk_size), timeout=5.0) def read_chunk():
try:
return fifo.read(chunk_size)
except BlockingIOError:
# No data available yet (writer not connected or no data)
return None
if not data: data = await loop.run_in_executor(None, read_chunk)
print("No data from TCP, stream may have ended")
break
# Broadcast binary data to all connected WebSocket clients if data is None:
disconnected_clients = set() # BlockingIOError: no data available, wait a bit
for client in active_websocket_clients.copy(): if not writer_connected:
print("Waiting for C++ to start writing to FIFO...")
await asyncio.sleep(0.1)
continue
if not data:
# Empty data: EOF (writer closed the FIFO)
if writer_connected:
print("FIFO EOF reached - C++ stopped writing")
else:
print("FIFO closed before C++ started writing")
break
# Got data! Mark writer as connected
if not writer_connected:
writer_connected = True
print("C++ started writing to FIFO successfully")
if ffmpeg_process and ffmpeg_process.stdin:
try:
ffmpeg_process.stdin.write(data)
ffmpeg_process.stdin.flush()
bytes_written += len(data)
# Log progress every 1MB
if bytes_written % (1024 * 1024) < chunk_size:
print(f"Written {bytes_written / 1024 / 1024:.2f} MB to ffmpeg for HLS encoding")
except Exception as e:
print(f"Error writing to ffmpeg stdin: {e}")
break
finally:
if ffmpeg_process and ffmpeg_process.stdin:
try: try:
await client.send_bytes(data) ffmpeg_process.stdin.close()
except Exception as e: print("Closed ffmpeg stdin")
print(f"Error sending to client: {e}") except:
disconnected_clients.add(client) pass
# Remove disconnected clients async def read_ffmpeg_stderr():
active_websocket_clients.difference_update(disconnected_clients) """Read and log ffmpeg stderr for debugging"""
line_count = 0
try:
while streaming_active:
def read_err():
line = ffmpeg_process.stderr.readline()
return line.decode('utf-8', errors='ignore') if line else None
except asyncio.TimeoutError: line = await loop.run_in_executor(None, read_err)
# No data in 5 seconds, continue waiting if not line:
continue break
line_count += 1
# Log first 30 lines to see HLS setup, then only errors/warnings
if line_count <= 30:
# Skip empty lines and progress updates
if line.strip() and not line.startswith('frame='):
print(f"FFmpeg[{line_count}]: {line.strip()}")
elif 'error' in line.lower() or 'warning' in line.lower():
print(f"FFmpeg: {line.strip()}")
except Exception as e: except Exception as e:
print(f"Error reading from TCP: {e}") print(f"Error reading ffmpeg stderr: {e}")
break
writer.close() # Run both tasks concurrently
await writer.wait_closed() await asyncio.gather(
read_from_fifo(),
read_ffmpeg_stderr(),
return_exceptions=True
)
except ConnectionRefusedError: except FileNotFoundError:
print(f"ERROR: Could not connect to TCP server {TCP_HOST}:{TCP_PORT}") print(f"ERROR: Could not open FIFO {VIDEO_FIFO_PATH}")
print("Make sure C++ application is running first") print("Make sure C++ application is running first")
except Exception as e: except Exception as e:
print(f"ERROR in video streaming: {e}") print(f"ERROR in video streaming: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
finally: finally:
# Cleanup ffmpeg process
if ffmpeg_process:
try:
ffmpeg_process.terminate()
ffmpeg_process.wait(timeout=2)
except:
try:
ffmpeg_process.kill()
except:
pass
streaming_active = False streaming_active = False
print("Video streaming stopped") print("Video streaming stopped")
@app.websocket("/ws/video") @app.get("/hls/playlist.m3u8")
async def websocket_video_endpoint(websocket: WebSocket): async def get_hls_playlist():
"""WebSocket endpoint for video streaming""" """Serve HLS playlist manifest"""
global streaming_task, streaming_active playlist_path = "hls_output/playlist.m3u8"
await websocket.accept() if not os.path.exists(playlist_path):
print(f"Client connected to video stream: {id(websocket)}") return Response(content="HLS playlist not ready yet", status_code=404)
# Add client to active set
active_websocket_clients.add(websocket)
# Start streaming task if not already running
if not streaming_active:
streaming_task = asyncio.create_task(stream_video_from_tcp())
print("Started video streaming task")
try: try:
# Keep connection alive and handle client messages with open(playlist_path, 'r') as f:
while True: content = f.read()
# Wait for client messages (like ping/pong or control messages)
try:
data = await asyncio.wait_for(websocket.receive_text(), timeout=1.0)
# Handle client messages if needed
if data == "ping":
await websocket.send_text("pong")
except asyncio.TimeoutError:
# No message received, continue
continue
except WebSocketDisconnect: return Response(
print(f"Client disconnected from video stream: {id(websocket)}") content=content,
media_type="application/vnd.apple.mpegurl",
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
"Access-Control-Allow-Origin": "*"
}
)
except Exception as e: except Exception as e:
print(f"WebSocket error: {e}") print(f"Error serving HLS playlist: {e}")
finally: return Response(content=f"Error: {e}", status_code=500)
# Remove client from active set
active_websocket_clients.discard(websocket)
# Stop streaming if no more clients
if not active_websocket_clients: @app.get("/hls/{segment_name}")
streaming_active = False async def get_hls_segment(segment_name: str):
print("No more clients, stopping stream") """Serve HLS video segments"""
# Security: only allow .ts files
if not segment_name.endswith('.ts'):
return Response(content="Invalid segment name", status_code=400)
segment_path = f"hls_output/{segment_name}"
if not os.path.exists(segment_path):
return Response(content="Segment not found", status_code=404)
try:
with open(segment_path, 'rb') as f:
content = f.read()
return Response(
content=content,
media_type="video/MP2T",
headers={
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*"
}
)
except Exception as e:
print(f"Error serving HLS segment {segment_name}: {e}")
return Response(content=f"Error: {e}", status_code=500)
@app.get("/health") @app.get("/health")
async def health(): async def health():
"""Health check endpoint""" """Health check endpoint"""
hls_playlist_exists = os.path.exists("hls_output/playlist.m3u8")
return { return {
"status": "healthy", "status": "healthy",
"active_websocket_clients": len(active_websocket_clients),
"streaming_active": streaming_active, "streaming_active": streaming_active,
"tcp_endpoint": f"{TCP_HOST}:{TCP_PORT}" "hls_playlist_ready": hls_playlist_exists,
"video_fifo": VIDEO_FIFO_PATH
} }
@ -332,7 +518,7 @@ if __name__ == '__main__':
print("Starting FastAPI server on http://0.0.0.0:5000") print("Starting FastAPI server on http://0.0.0.0:5000")
print(f"Video stream will be available at ws://0.0.0.0:5000/ws/video") print(f"Video stream will be available at ws://0.0.0.0:5000/ws/video")
print(f"Expecting C++ TCP stream at {TCP_HOST}:{TCP_PORT}") print(f"Expecting C++ H.264 stream via FIFO at {VIDEO_FIFO_PATH}")
uvicorn.run( uvicorn.run(
app, app,

View File

@ -0,0 +1,14 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:1097
#EXTINF:1.000000,
segment_1097.ts
#EXTINF:1.000000,
segment_1098.ts
#EXTINF:1.000000,
segment_1099.ts
#EXTINF:1.000000,
segment_1100.ts
#EXTINF:0.400000,
segment_1101.ts

Binary file not shown.

View File

@ -3,3 +3,4 @@ uvicorn[standard]>=0.24.0
python-multipart>=0.0.6 python-multipart>=0.0.6
posix_ipc>=1.1.0 posix_ipc>=1.1.0
websockets>=12.0 websockets>=12.0
Jinja2==3.1.6

1
web_viewer/static/js/jsmpeg.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -3,9 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Beacon Tracker - Live Stream (FastAPI)</title> <title>Beacon Tracker - Live Stream (HLS)</title>
<!-- JSMpeg for H.264/MPEG-TS decoding --> <!-- HLS.js for H.264 HLS decoding -->
<script src="https://cdn.jsdelivr.net/npm/jsmpeg@0.1.0/jsmpeg.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style> <style>
* { * {
margin: 0; margin: 0;
@ -92,12 +92,20 @@
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
} }
#videoFrame, #videoCanvas { #videoFrame {
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
} }
#videoPlayer {
width: 100%;
height: auto;
display: block;
background: #000;
min-height: 480px;
}
.hidden { .hidden {
display: none !important; display: none !important;
} }
@ -276,24 +284,14 @@
</header> </header>
<div class="main-content"> <div class="main-content">
<div class="stream-selector">
<span style="font-weight: 600;">Выбрать метод передачи:</span>
<button id="btnWebSocket" onclick="switchToWebSocket()" class="active">
WebSocket (H.264) <span class="perf-badge websocket">Быстрее</span>
</button>
<button id="btnSSE" onclick="switchToSSE()">
SSE (JPEG) <span class="perf-badge sse">Совместимость</span>
</button>
</div>
<div class="video-container"> <div class="video-container">
<!-- Canvas for WebSocket/JSMpeg streaming --> <!-- HTML5 video element for HLS playback -->
<canvas id="videoCanvas"></canvas> <video id="videoPlayer" controls autoplay muted playsinline></video>
<!-- Image for SSE/JPEG streaming --> <!-- Image for SSE/JPEG streaming (fallback) -->
<img id="videoFrame" class="hidden" alt="Video stream"> <img id="videoFrame" class="hidden" alt="Video stream">
<div class="loading-overlay" id="loadingOverlay"> <div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div> <div class="spinner"></div>
<p>Ожидание данных...</p> <p>Ожидание HLS потока...</p>
</div> </div>
</div> </div>
@ -326,21 +324,21 @@
</div> </div>
<script> <script>
// Stream mode: 'websocket' or 'sse' // Stream mode: 'hls' or 'sse'
let streamMode = 'websocket'; let streamMode = 'hls';
// SSE variables // SSE variables (fallback)
let eventSource = null; let eventSource = null;
let sseFrameCount = 0; let sseFrameCount = 0;
let sseLastFrameTime = Date.now(); let sseLastFrameTime = Date.now();
let sseTotalBytes = 0; let sseTotalBytes = 0;
let sseLastBitrateCalc = Date.now(); let sseLastBitrateCalc = Date.now();
// WebSocket variables // HLS variables
let websocket = null; let hls = null;
let player = null; let video = null;
let wsFrameCount = 0; let hlsFrameCount = 0;
let wsLastFrameTime = Date.now(); let hlsLastFrameTime = Date.now();
let reconnectAttempts = 0; let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5; const MAX_RECONNECT_ATTEMPTS = 5;
@ -357,124 +355,107 @@
} }
} }
function switchToWebSocket() { function connectHLS() {
if (streamMode === 'websocket') return; document.getElementById('streamMethod').textContent = 'HLS (H.264)';
document.getElementById('videoPlayer').classList.remove('hidden');
streamMode = 'websocket';
document.getElementById('btnWebSocket').classList.add('active');
document.getElementById('btnSSE').classList.remove('active');
disconnectSSE();
connectWebSocket();
}
function switchToSSE() {
if (streamMode === 'sse') return;
streamMode = 'sse';
document.getElementById('btnSSE').classList.add('active');
document.getElementById('btnWebSocket').classList.remove('active');
disconnectWebSocket();
connectSSE();
}
function connectWebSocket() {
document.getElementById('streamMethod').textContent = 'WebSocket (H.264)';
document.getElementById('videoCanvas').classList.remove('hidden');
document.getElementById('videoFrame').classList.add('hidden'); document.getElementById('videoFrame').classList.add('hidden');
console.log('Connecting to WebSocket video stream...'); console.log('Connecting to HLS video stream...');
// Build WebSocket URL video = document.getElementById('videoPlayer');
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const hlsUrl = `${window.location.protocol}//${window.location.host}/hls/playlist.m3u8`;
const wsUrl = `${protocol}//${window.location.host}/ws/video`;
// Connect to native WebSocket if (Hls.isSupported()) {
websocket = new WebSocket(wsUrl); hls = new Hls({
websocket.binaryType = 'arraybuffer'; // Low latency configuration with increased buffer
lowLatencyMode: true,
backBufferLength: 90,
maxBufferLength: 10, // Increased from 3 to 10 seconds
maxBufferSize: 3 * 1024 * 1024, // Increased from 1MB to 3MB
liveSyncDurationCount: 3, // Keep 3 segments in sync
liveMaxLatencyDurationCount: 5, // Max 5 segments latency before catchup
maxMaxBufferLength: 30, // Maximum buffer length
enableWorker: true,
debug: false
});
websocket.onopen = () => { hls.loadSource(hlsUrl);
console.log('WebSocket connected'); hls.attachMedia(video);
updateConnectionStatus(true);
hideError();
reconnectAttempts = 0;
// Initialize JSMpeg player hls.on(Hls.Events.MANIFEST_PARSED, function() {
const canvas = document.getElementById('videoCanvas'); console.log('HLS manifest parsed, starting playback...');
player = new JSMpeg.Player(null, { video.play().catch(e => {
canvas: canvas, console.warn('Autoplay prevented, waiting for user interaction:', e);
disableGl: false, });
disableWebAssembly: false, updateConnectionStatus(true);
preserveDrawingBuffer: false, hideError();
progressive: true, });
throttled: true,
chunkSize: 1024 * 32, hls.on(Hls.Events.ERROR, function(event, data) {
onVideoDecode: (decoder, time) => { console.error('HLS error:', data.type, data.details);
// Update FPS counter if (data.fatal) {
const now = Date.now(); switch(data.type) {
const timeDiff = (now - wsLastFrameTime) / 1000; case Hls.ErrorTypes.NETWORK_ERROR:
if (timeDiff > 0.1) { // Update every 100ms console.log('Network error, attempting to recover...');
fps = Math.round(1 / timeDiff); hls.startLoad();
document.getElementById('fpsCounter').textContent = fps; break;
wsFrameCount++; case Hls.ErrorTypes.MEDIA_ERROR:
document.getElementById('frameCounter').textContent = wsFrameCount; console.log('Media error, attempting to recover...');
hls.recoverMediaError();
break;
default:
console.error('Fatal error, cannot recover');
updateConnectionStatus(false);
showError('Ошибка HLS потока: ' + data.details);
break;
} }
wsLastFrameTime = now;
// Update resolution
if (decoder.width && decoder.height) {
document.getElementById('resolution').textContent =
`${decoder.width}×${decoder.height}`;
}
// Hide loading overlay on first frame
document.getElementById('loadingOverlay').classList.add('hidden');
} }
}); });
};
websocket.onmessage = (event) => { // Hide loading overlay when playback starts
if (player && event.data instanceof ArrayBuffer) { video.addEventListener('playing', () => {
const uint8Array = new Uint8Array(event.data); console.log('Video playback started');
document.getElementById('loadingOverlay').classList.add('hidden');
updateConnectionStatus(true);
});
if (!player.source) { // Track video stats
// Create source buffer if needed video.addEventListener('timeupdate', () => {
player.source = { const now = Date.now();
write: function(data) { const timeDiff = (now - hlsLastFrameTime) / 1000;
if (player.demuxer && player.demuxer.write) { if (timeDiff > 0.5) { // Update every 500ms
player.demuxer.write(data); // Estimate FPS from video framerate
} if (video.videoWidth && video.videoHeight) {
} document.getElementById('resolution').textContent =
}; `${video.videoWidth}×${video.videoHeight}`;
}
// Calculate approximate FPS
fps = hls.media ? Math.round(1 / timeDiff * 10) : 0;
document.getElementById('fpsCounter').textContent = fps;
hlsFrameCount++;
document.getElementById('frameCounter').textContent = hlsFrameCount;
hlsLastFrameTime = now;
} }
});
// Feed data to JSMpeg } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
if (player.demuxer && player.demuxer.write) { // Native HLS support (Safari)
player.demuxer.write(uint8Array); console.log('Using native HLS support');
} video.src = hlsUrl;
} video.addEventListener('loadedmetadata', function() {
}; console.log('HLS metadata loaded');
video.play();
});
websocket.onerror = (error) => { video.addEventListener('playing', () => {
console.error('WebSocket error:', error); document.getElementById('loadingOverlay').classList.add('hidden');
updateConnectionStatus(false); updateConnectionStatus(true);
showError('Ошибка WebSocket соединения'); });
}; } else {
showError('HLS не поддерживается в этом браузере');
websocket.onclose = () => { }
console.log('WebSocket disconnected');
updateConnectionStatus(false);
// Auto-reconnect
if (streamMode === 'websocket' && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
console.log(`Attempting to reconnect (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
setTimeout(connectWebSocket, 2000);
} else if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
showError('Потеряно соединение. Нажмите "Переподключиться"');
}
};
// Start memory monitoring // Start memory monitoring
if (!statsInterval) { if (!statsInterval) {
@ -482,25 +463,16 @@
updateMemoryUsage(); updateMemoryUsage();
}, 2000); }, 2000);
} }
// Send periodic ping to keep connection alive
const pingInterval = setInterval(() => {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send('ping');
} else {
clearInterval(pingInterval);
}
}, 30000); // Every 30 seconds
} }
function disconnectWebSocket() { function disconnectHLS() {
if (websocket) { if (hls) {
websocket.close(); hls.destroy();
websocket = null; hls = null;
} }
if (player) { if (video) {
player.destroy(); video.pause();
player = null; video.src = '';
} }
if (statsInterval) { if (statsInterval) {
clearInterval(statsInterval); clearInterval(statsInterval);
@ -510,7 +482,7 @@
function connectSSE() { function connectSSE() {
document.getElementById('streamMethod').textContent = 'SSE (JPEG)'; document.getElementById('streamMethod').textContent = 'SSE (JPEG)';
document.getElementById('videoCanvas').classList.add('hidden'); document.getElementById('videoPlayer').classList.add('hidden');
document.getElementById('videoFrame').classList.remove('hidden'); document.getElementById('videoFrame').classList.remove('hidden');
if (eventSource) { if (eventSource) {
@ -635,9 +607,9 @@
function reconnect() { function reconnect() {
document.getElementById('loadingOverlay').classList.remove('hidden'); document.getElementById('loadingOverlay').classList.remove('hidden');
reconnectAttempts = 0; reconnectAttempts = 0;
if (streamMode === 'websocket') { if (streamMode === 'hls') {
disconnectWebSocket(); disconnectHLS();
connectWebSocket(); connectHLS();
} else { } else {
disconnectSSE(); disconnectSSE();
connectSSE(); connectSSE();
@ -647,8 +619,13 @@
function takeScreenshot() { function takeScreenshot() {
let dataUrl; let dataUrl;
if (streamMode === 'websocket') { if (streamMode === 'hls') {
const canvas = document.getElementById('videoCanvas'); // Capture current frame from video element
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
dataUrl = canvas.toDataURL('image/jpeg', 0.95); dataUrl = canvas.toDataURL('image/jpeg', 0.95);
} else { } else {
const img = document.getElementById('videoFrame'); const img = document.getElementById('videoFrame');
@ -661,16 +638,16 @@
link.click(); link.click();
} }
// Connect on page load with default WebSocket mode // Connect on page load with default HLS mode
if (streamMode === 'websocket') { if (streamMode === 'hls') {
connectWebSocket(); connectHLS();
} else { } else {
connectSSE(); connectSSE();
} }
// Cleanup on page unload // Cleanup on page unload
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
disconnectWebSocket(); disconnectHLS();
disconnectSSE(); disconnectSSE();
}); });
</script> </script>