""" 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)