Compare commits

..

2 Commits

Author SHA1 Message Date
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
8 changed files with 389 additions and 239 deletions

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
*.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).
"""
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, StreamingResponse, Response
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@ -15,7 +15,6 @@ import time
import base64
import json
import os
from typing import Set
from contextlib import asynccontextmanager
# Global state
@ -26,11 +25,10 @@ last_frame_header = None
frame_lock = asyncio.Lock()
# Video streaming state
active_websocket_clients: Set[WebSocket] = set()
streaming_task = None
streaming_active = False
TCP_HOST = '127.0.0.1'
TCP_PORT = 8888
VIDEO_FIFO_PATH = '/tmp/beacon_video_stream' # Named pipe for H.264 stream
video_fifo_file = None # Keep FIFO open to prevent C++ blocking
async def init_reader():
@ -46,23 +44,77 @@ async def init_reader():
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 (will block until C++ opens for writing)...")
loop = asyncio.get_event_loop()
# Open file in BLOCKING mode - this will unblock C++ when it opens for writing
def open_fifo():
return open(VIDEO_FIFO_PATH, 'rb', buffering=0)
video_fifo_file = await loop.run_in_executor(None, open_fifo)
print(f"FIFO opened successfully - C++ should now be writing")
except Exception as e:
print(f"Failed to open video FIFO: {e}")
async def cleanup_reader():
"""Clean up shared memory reader"""
global reader
global reader, video_fifo_file
async with reader_lock:
if reader:
reader.close()
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
async def lifespan(app: FastAPI):
"""Lifespan context manager for startup and shutdown events"""
global streaming_task, streaming_active
# Startup
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")
yield
# 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()
print("FastAPI application shutdown")
@ -207,123 +259,228 @@ async def latest_frame():
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.
This runs as a background task.
Read raw H.264 from FIFO and convert to HLS format via ffmpeg.
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
await asyncio.sleep(2)
ffmpeg_process = None
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:
# Connect to TCP server
print(f"Connecting to TCP server {TCP_HOST}:{TCP_PORT}...")
# Use the globally opened FIFO file
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 5: keep 5 segments in playlist
# -hls_flags delete_segments+append_list+omit_endlist: live streaming flags
# -hls_segment_type mpegts: use MPEG-TS segments
# -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', '5',
'-hls_flags', 'delete_segments+append_list+omit_endlist',
'-hls_segment_type', 'mpegts',
'-hls_segment_filename', f'{hls_dir}/segment_%03d.ts',
'-start_number', '0',
f'{hls_dir}/playlist.m3u8'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
streaming_active = True
chunk_size = 32768 # 32KB chunks for MPEG-TS
chunk_size = 65536 # 64KB chunks
loop = asyncio.get_event_loop()
while streaming_active and active_websocket_clients:
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
try:
# Read chunk from TCP
data = await asyncio.wait_for(reader.read(chunk_size), timeout=5.0)
while streaming_active:
def read_chunk():
return fifo.read(chunk_size)
if not data:
print("No data from TCP, stream may have ended")
break
data = await loop.run_in_executor(None, read_chunk)
if not data:
print("FIFO EOF reached")
break
# Broadcast binary data to all connected WebSocket clients
disconnected_clients = set()
for client in active_websocket_clients.copy():
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:
await client.send_bytes(data)
except Exception as e:
print(f"Error sending to client: {e}")
disconnected_clients.add(client)
ffmpeg_process.stdin.close()
print("Closed ffmpeg stdin")
except:
pass
# Remove disconnected clients
active_websocket_clients.difference_update(disconnected_clients)
async def read_ffmpeg_stderr():
"""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:
# No data in 5 seconds, continue waiting
continue
line = await loop.run_in_executor(None, read_err)
if not line:
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:
print(f"Error reading from TCP: {e}")
break
print(f"Error reading ffmpeg stderr: {e}")
writer.close()
await writer.wait_closed()
# Run both tasks concurrently
await asyncio.gather(
read_from_fifo(),
read_ffmpeg_stderr(),
return_exceptions=True
)
except ConnectionRefusedError:
print(f"ERROR: Could not connect to TCP server {TCP_HOST}:{TCP_PORT}")
except FileNotFoundError:
print(f"ERROR: Could not open FIFO {VIDEO_FIFO_PATH}")
print("Make sure C++ application is running first")
except Exception as e:
print(f"ERROR in video streaming: {e}")
import traceback
traceback.print_exc()
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
print("Video streaming stopped")
@app.websocket("/ws/video")
async def websocket_video_endpoint(websocket: WebSocket):
"""WebSocket endpoint for video streaming"""
global streaming_task, streaming_active
@app.get("/hls/playlist.m3u8")
async def get_hls_playlist():
"""Serve HLS playlist manifest"""
playlist_path = "hls_output/playlist.m3u8"
await websocket.accept()
print(f"Client connected to video stream: {id(websocket)}")
# 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")
if not os.path.exists(playlist_path):
return Response(content="HLS playlist not ready yet", status_code=404)
try:
# Keep connection alive and handle client messages
while True:
# 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
with open(playlist_path, 'r') as f:
content = f.read()
except WebSocketDisconnect:
print(f"Client disconnected from video stream: {id(websocket)}")
return Response(
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:
print(f"WebSocket error: {e}")
finally:
# Remove client from active set
active_websocket_clients.discard(websocket)
print(f"Error serving HLS playlist: {e}")
return Response(content=f"Error: {e}", status_code=500)
# Stop streaming if no more clients
if not active_websocket_clients:
streaming_active = False
print("No more clients, stopping stream")
@app.get("/hls/{segment_name}")
async def get_hls_segment(segment_name: str):
"""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")
async def health():
"""Health check endpoint"""
hls_playlist_exists = os.path.exists("hls_output/playlist.m3u8")
return {
"status": "healthy",
"active_websocket_clients": len(active_websocket_clients),
"streaming_active": streaming_active,
"tcp_endpoint": f"{TCP_HOST}:{TCP_PORT}"
"hls_playlist_ready": hls_playlist_exists,
"video_fifo": VIDEO_FIFO_PATH
}
@ -332,7 +489,7 @@ if __name__ == '__main__':
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"Expecting C++ TCP stream at {TCP_HOST}:{TCP_PORT}")
print(f"Expecting C++ H.264 stream via FIFO at {VIDEO_FIFO_PATH}")
uvicorn.run(
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,4 +3,4 @@ uvicorn[standard]>=0.24.0
python-multipart>=0.0.6
posix_ipc>=1.1.0
websockets>=12.0
jinja2
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>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Beacon Tracker - Live Stream (FastAPI)</title>
<!-- JSMpeg for H.264/MPEG-TS decoding -->
<script src="https://cdn.jsdelivr.net/npm/jsmpeg@0.1.0/jsmpeg.min.js"></script>
<title>Beacon Tracker - Live Stream (HLS)</title>
<!-- HLS.js for H.264 HLS decoding -->
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style>
* {
margin: 0;
@ -92,12 +92,20 @@
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
#videoFrame, #videoCanvas {
#videoFrame {
width: 100%;
height: auto;
display: block;
}
#videoPlayer {
width: 100%;
height: auto;
display: block;
background: #000;
min-height: 480px;
}
.hidden {
display: none !important;
}
@ -276,24 +284,14 @@
</header>
<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">
<!-- Canvas for WebSocket/JSMpeg streaming -->
<canvas id="videoCanvas"></canvas>
<!-- Image for SSE/JPEG streaming -->
<!-- HTML5 video element for HLS playback -->
<video id="videoPlayer" controls autoplay muted playsinline></video>
<!-- Image for SSE/JPEG streaming (fallback) -->
<img id="videoFrame" class="hidden" alt="Video stream">
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
<p>Ожидание данных...</p>
<p>Ожидание HLS потока...</p>
</div>
</div>
@ -326,21 +324,21 @@
</div>
<script>
// Stream mode: 'websocket' or 'sse'
let streamMode = 'websocket';
// Stream mode: 'hls' or 'sse'
let streamMode = 'hls';
// SSE variables
// SSE variables (fallback)
let eventSource = null;
let sseFrameCount = 0;
let sseLastFrameTime = Date.now();
let sseTotalBytes = 0;
let sseLastBitrateCalc = Date.now();
// WebSocket variables
let websocket = null;
let player = null;
let wsFrameCount = 0;
let wsLastFrameTime = Date.now();
// HLS variables
let hls = null;
let video = null;
let hlsFrameCount = 0;
let hlsLastFrameTime = Date.now();
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
@ -357,124 +355,106 @@
}
}
function switchToWebSocket() {
if (streamMode === 'websocket') return;
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');
function connectHLS() {
document.getElementById('streamMethod').textContent = 'HLS (H.264)';
document.getElementById('videoPlayer').classList.remove('hidden');
document.getElementById('videoFrame').classList.add('hidden');
console.log('Connecting to WebSocket video stream...');
console.log('Connecting to HLS video stream...');
// Build WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/video`;
video = document.getElementById('videoPlayer');
const hlsUrl = `${window.location.protocol}//${window.location.host}/hls/playlist.m3u8`;
// Connect to native WebSocket
websocket = new WebSocket(wsUrl);
websocket.binaryType = 'arraybuffer';
if (Hls.isSupported()) {
hls = new Hls({
// Low latency configuration
lowLatencyMode: true,
backBufferLength: 90,
maxBufferLength: 3,
maxBufferSize: 1 * 1024 * 1024, // 1MB
liveSyncDurationCount: 1,
liveMaxLatencyDurationCount: 2,
enableWorker: true,
debug: false
});
websocket.onopen = () => {
console.log('WebSocket connected');
updateConnectionStatus(true);
hideError();
reconnectAttempts = 0;
hls.loadSource(hlsUrl);
hls.attachMedia(video);
// Initialize JSMpeg player
const canvas = document.getElementById('videoCanvas');
player = new JSMpeg.Player(null, {
canvas: canvas,
disableGl: false,
disableWebAssembly: false,
preserveDrawingBuffer: false,
progressive: true,
throttled: true,
chunkSize: 1024 * 32,
onVideoDecode: (decoder, time) => {
// Update FPS counter
const now = Date.now();
const timeDiff = (now - wsLastFrameTime) / 1000;
if (timeDiff > 0.1) { // Update every 100ms
fps = Math.round(1 / timeDiff);
document.getElementById('fpsCounter').textContent = fps;
wsFrameCount++;
document.getElementById('frameCounter').textContent = wsFrameCount;
hls.on(Hls.Events.MANIFEST_PARSED, function() {
console.log('HLS manifest parsed, starting playback...');
video.play().catch(e => {
console.warn('Autoplay prevented, waiting for user interaction:', e);
});
updateConnectionStatus(true);
hideError();
});
hls.on(Hls.Events.ERROR, function(event, data) {
console.error('HLS error:', data.type, data.details);
if (data.fatal) {
switch(data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.log('Network error, attempting to recover...');
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
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) => {
if (player && event.data instanceof ArrayBuffer) {
const uint8Array = new Uint8Array(event.data);
// Hide loading overlay when playback starts
video.addEventListener('playing', () => {
console.log('Video playback started');
document.getElementById('loadingOverlay').classList.add('hidden');
updateConnectionStatus(true);
});
if (!player.source) {
// Create source buffer if needed
player.source = {
write: function(data) {
if (player.demuxer && player.demuxer.write) {
player.demuxer.write(data);
}
}
};
// Track video stats
video.addEventListener('timeupdate', () => {
const now = Date.now();
const timeDiff = (now - hlsLastFrameTime) / 1000;
if (timeDiff > 0.5) { // Update every 500ms
// 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
if (player.demuxer && player.demuxer.write) {
player.demuxer.write(uint8Array);
}
}
};
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Native HLS support (Safari)
console.log('Using native HLS support');
video.src = hlsUrl;
video.addEventListener('loadedmetadata', function() {
console.log('HLS metadata loaded');
video.play();
});
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
updateConnectionStatus(false);
showError('Ошибка WebSocket соединения');
};
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('Потеряно соединение. Нажмите "Переподключиться"');
}
};
video.addEventListener('playing', () => {
document.getElementById('loadingOverlay').classList.add('hidden');
updateConnectionStatus(true);
});
} else {
showError('HLS не поддерживается в этом браузере');
}
// Start memory monitoring
if (!statsInterval) {
@ -482,25 +462,16 @@
updateMemoryUsage();
}, 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() {
if (websocket) {
websocket.close();
websocket = null;
function disconnectHLS() {
if (hls) {
hls.destroy();
hls = null;
}
if (player) {
player.destroy();
player = null;
if (video) {
video.pause();
video.src = '';
}
if (statsInterval) {
clearInterval(statsInterval);
@ -510,7 +481,7 @@
function connectSSE() {
document.getElementById('streamMethod').textContent = 'SSE (JPEG)';
document.getElementById('videoCanvas').classList.add('hidden');
document.getElementById('videoPlayer').classList.add('hidden');
document.getElementById('videoFrame').classList.remove('hidden');
if (eventSource) {
@ -635,9 +606,9 @@
function reconnect() {
document.getElementById('loadingOverlay').classList.remove('hidden');
reconnectAttempts = 0;
if (streamMode === 'websocket') {
disconnectWebSocket();
connectWebSocket();
if (streamMode === 'hls') {
disconnectHLS();
connectHLS();
} else {
disconnectSSE();
connectSSE();
@ -647,8 +618,13 @@
function takeScreenshot() {
let dataUrl;
if (streamMode === 'websocket') {
const canvas = document.getElementById('videoCanvas');
if (streamMode === 'hls') {
// 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);
} else {
const img = document.getElementById('videoFrame');
@ -661,16 +637,16 @@
link.click();
}
// Connect on page load with default WebSocket mode
if (streamMode === 'websocket') {
connectWebSocket();
// Connect on page load with default HLS mode
if (streamMode === 'hls') {
connectHLS();
} else {
connectSSE();
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
disconnectWebSocket();
disconnectHLS();
disconnectSSE();
});
</script>