-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstream_server.py
More file actions
103 lines (86 loc) Β· 2.87 KB
/
stream_server.py
File metadata and controls
103 lines (86 loc) Β· 2.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/env python3
"""
Bird Watcher β Flask stream server module.
MJPEG streaming server with token-based authentication and viewer limits.
"""
import hmac
import time
import logging
import threading
from flask import Flask, Response, render_template_string, request, abort
logger = logging.getLogger("bird-watcher")
app = Flask(__name__)
# Module-level state β set by init_server() before app.run()
_config = None
_shared_state = None
_active_viewers = 0
_viewers_lock = threading.Lock()
HTML_PAGE = """
<!DOCTYPE html>
<html>
<head>
<title>π¦ Bird Watcher Live</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
img { max-width: 100%; max-height: 100vh; object-fit: contain; }
</style>
</head>
<body>
<img src="/feed" alt="Bird Watcher Live Feed">
</body>
</html>
"""
def init_server(config, shared_state):
"""
Initialize the Flask server with configuration and shared state.
Must be called before app.run().
"""
global _config, _shared_state
_config = config
_shared_state = shared_state
def _generate_mjpeg():
"""Yield MJPEG frames from the shared state."""
while True:
with _shared_state["stream_lock"]:
frame = _shared_state["stream_jpeg"]
if frame:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
time.sleep(0.033)
def _counted_stream(gen):
"""Wrap a generator to accurately track active viewer count."""
global _active_viewers
with _viewers_lock:
_active_viewers += 1
try:
yield from gen
finally:
with _viewers_lock:
_active_viewers = max(0, _active_viewers - 1)
@app.route('/')
def index():
token = request.args.get('token', '')
if not hmac.compare_digest(token, _config.stream_token):
return (
'<html><body style="background:#000;color:#fff;font-family:sans-serif;'
'display:flex;align-items:center;justify-content:center;height:100vh">'
'<div><h1>π¦ Bird Watcher</h1>'
'<p>Access token required. Add ?token=YOUR_TOKEN to the URL.</p>'
'</div></body></html>'
), 403
page = HTML_PAGE.replace('/feed', f'/feed?token={_config.stream_token}')
return render_template_string(page)
@app.route('/feed')
def video_feed():
token = request.args.get('token', '')
if not hmac.compare_digest(token, _config.stream_token):
abort(403)
with _viewers_lock:
if _active_viewers >= _config.max_concurrent_viewers:
abort(503)
return Response(
_counted_stream(_generate_mjpeg()),
mimetype='multipart/x-mixed-replace; boundary=frame',
)