Coverage for orchestrant/streaming/app.py: 31%
49 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-18 00:48 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-18 00:48 +0000
1"""Flask app for streaming camera frames over HTTP."""
3from __future__ import annotations
5import os
6import secrets
7import warnings
8from functools import lru_cache
10from flask import Flask, Response, render_template, stream_with_context
11from loguru import logger
13from orchestrant.logging_config import setup_logging
14from orchestrant.streaming.capture import FrameCapture
15from orchestrant.streaming.generator import gen_frames
18def create_app(frame_capture: FrameCapture | None = None) -> Flask:
19 """Create and configure the streaming Flask app."""
20 setup_logging()
22 app = Flask(__name__, template_folder="template-files")
23 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
25 secret_key = os.getenv("KATAGLYPHIS_SECRET_KEY")
26 if secret_key:
27 app.config["SECRET_KEY"] = secret_key
28 else:
29 generated_key = secrets.token_hex(32)
30 app.config["SECRET_KEY"] = generated_key
31 warnings.warn(
32 "KATAGLYPHIS_SECRET_KEY environment variable not set. "
33 "Using a randomly generated secret key. Sessions will be invalidated "
34 "on restart. Set KATAGLYPHIS_SECRET_KEY for production deployments.",
35 UserWarning,
36 stacklevel=2,
37 )
38 logger.warning(
39 "KATAGLYPHIS_SECRET_KEY not set - using ephemeral secret key. "
40 "Sessions will not persist across restarts."
41 )
43 capture = frame_capture or FrameCapture()
45 @app.route("/video_feed")
46 def video_feed() -> Response:
47 """Return multipart MJPEG stream of camera frames."""
48 response = Response(
49 stream_with_context(gen_frames(capture)),
50 mimetype="multipart/x-mixed-replace; boundary=frame",
51 )
52 # Disable caching so the browser always loads the newest frame
53 response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
54 response.headers["Pragma"] = "no-cache"
55 response.headers["Expires"] = "0"
56 return response
58 @app.route("/")
59 def index() -> str:
60 """Render the streaming index page."""
61 return render_template("index.html")
63 app.extensions["frame_capture"] = capture
64 return app
67@lru_cache(maxsize=1)
68def _get_app() -> Flask:
69 """Lazily create the module-level app on first access."""
70 # Cached, not a function attribute: tests reset it with _get_app.cache_clear().
71 return create_app()
74def run() -> None:
75 """Run the streaming Flask app."""
76 app = _get_app()
77 capture = app.extensions.get("frame_capture")
78 try:
79 host = os.getenv("KATAGLYPHIS_STREAM_HOST", "127.0.0.1")
80 app.run(
81 host=host,
82 port=5000,
83 debug=False,
84 threaded=True,
85 use_reloader=False,
86 )
87 except KeyboardInterrupt:
88 logger.info("Shutting down video stream")
89 finally:
90 if capture is not None:
91 capture.stop()
94if __name__ == "__main__":
95 run()