Files
radar_frontend/web_viewer/app.py
2025-11-18 12:26:48 +03:00

170 lines
4.9 KiB
Python

"""
Flask Web Application for Beacon Tracker Video Streaming
This application reads JPEG frames from shared memory and streams them
to web browsers via Server-Sent Events (SSE).
"""
from flask import Flask, render_template, Response, jsonify
from shared_memory_reader import SharedMemoryFrameReader
import time
import base64
import json
from threading import Lock
app = Flask(__name__)
# Global state
reader = None
reader_lock = Lock()
last_frame_data = None
last_frame_header = None
frame_lock = Lock()
def init_reader():
"""Initialize the shared memory reader"""
global reader
with reader_lock:
if reader is None:
try:
reader = SharedMemoryFrameReader()
print("Connected to shared memory successfully")
except Exception as e:
print(f"Failed to connect to shared memory: {e}")
reader = None
@app.route('/')
def index():
"""Render the main page"""
return render_template('index.html')
@app.route('/stream')
def stream():
"""
Server-Sent Events stream endpoint
Continuously sends JPEG frames to the client as they become available.
"""
def generate():
global reader
# Initialize reader for this stream
init_reader()
# Check again after init
with reader_lock:
local_reader = reader
if local_reader is None:
yield f"data: {json.dumps({'error': 'Failed to connect to shared memory'})}\n\n"
return
consecutive_failures = 0
max_failures = 100
while True:
try:
with reader_lock:
if reader is None:
yield f"data: {json.dumps({'error': 'Reader is None'})}\n\n"
return
result = reader.read_frame()
if result:
header, jpeg_data = result
consecutive_failures = 0
# Encode JPEG as base64 for transmission
jpeg_base64 = base64.b64encode(jpeg_data).decode('utf-8')
# Create event data
event_data = {
'frame_number': header.frame_number,
'timestamp_us': header.timestamp_us,
'width': header.width,
'height': header.height,
'data_size': header.data_size,
'jpeg': jpeg_base64
}
# Send as SSE event
yield f"data: {json.dumps(event_data)}\n\n"
# Update global state
with frame_lock:
global last_frame_data, last_frame_header
last_frame_data = jpeg_data
last_frame_header = header
else:
consecutive_failures += 1
if consecutive_failures >= max_failures:
yield f"data: {json.dumps({'error': 'No frames available'})}\n\n"
consecutive_failures = 0
# Small delay to prevent busy waiting
time.sleep(0.001) # 1ms - fast updates
except GeneratorExit:
# Client disconnected
break
except Exception as e:
print(f"Error in stream: {e}")
yield f"data: {json.dumps({'error': str(e)})}\n\n"
time.sleep(1)
return Response(generate(), mimetype='text/event-stream')
@app.route('/status')
def status():
"""Get the current status of the stream"""
init_reader()
with frame_lock:
if last_frame_header:
return jsonify({
'connected': True,
'last_frame': last_frame_header.frame_number,
'timestamp_us': last_frame_header.timestamp_us,
'resolution': f"{last_frame_header.width}x{last_frame_header.height}"
})
else:
return jsonify({
'connected': reader is not None,
'last_frame': None,
'message': 'Waiting for frames...'
})
@app.route('/latest_frame')
def latest_frame():
"""Get the latest frame as a JPEG image"""
with frame_lock:
if last_frame_data:
return Response(last_frame_data, mimetype='image/jpeg')
else:
return "No frame available", 404
@app.teardown_appcontext
def cleanup(exception=None):
"""Clean up resources on shutdown"""
global reader
with reader_lock:
if reader:
reader.close()
reader = None
if __name__ == '__main__':
# Initialize reader on startup
init_reader()
# Run the Flask app
# Use 0.0.0.0 to make it accessible from other machines on the network
app.run(host='0.0.0.0', port=5000, debug=True, threaded=True)