10.7. 将事件推送到仪表盘¶
仪表盘需要在运动触发的那一刻就被告知——而不是等到下一次轮询。这正是 Server-Sent Events 所做的事:浏览器到摄像头之间建立一个 HTTP 连接,每当事件发生时,摄像头就通过它把事件推送下来。
10.7.1. 运动检测器协程¶
第三个顶层任务会与采集循环和 HTTP 服务器一起运行。它会等待每一个新帧,对其与上一帧做差分,并在变化超过配置的阈值时——递增一个计数器并发出一个事件信号:
import time
motion_event = asyncio.Event()
last_motion = None
async def motion_detector():
global last_motion
prev = None
while True:
await new_frame.wait()
change = compute_change(prev, latest_jpeg)
if change > state['threshold']:
state['trigger_count'] += 1
last_motion = {
'ts': time.time(),
'count': state['trigger_count'],
'change': change,
}
motion_event.set()
prev = latest_jpeg
await asyncio.sleep_ms(50)
compute_change 的实现超出了本章的范围——图像处理那一节会正式介绍帧差分。现在就把它当作一个返回数字的占位符。
把这个新任务加入 main:
async def main():
await asyncio.gather(
capture_loop(),
motion_detector(),
app.start_server(host='0.0.0.0', port=80),
)
10.7.2. /events 路由¶
microdot.sse.with_sse() 会装饰一个异步处理函数,使 microdot 执行 SSE 握手(状态码 200、Content-Type: text/event-stream、不缓冲),并把一个 SSE 对象交给处理函数。只要浏览器保持连接打开,处理函数就会一直保持活动:
from microdot.sse import with_sse
@app.get('/events')
@with_sse
async def events(request, sse):
while True:
try:
await asyncio.wait_for(motion_event.wait(), timeout=15)
motion_event.clear()
if last_motion:
await sse.send(last_motion, event='motion')
except asyncio.TimeoutError:
await sse.send('keepalive', comment=True)
send() 会把一个事件写到线路上,然后把控制权交回事件循环。event='motion' 为事件类型命名,这样浏览器端的 EventSource 就可以仅针对这个名称注册一个监听器。event_id=(图中未显示)会设置 id: 行,这样浏览器在重连时就能通过 Last-Event-ID 请求头从一个已知的偏移处恢复。
15 秒超时加上 comment=True 的发送就是保活技巧。注释行以 : 开头,浏览器会完全忽略它们,但在线路上移动的这些字节能阻止中间的代理和 NAT 设备掐断一个空闲的连接。
10.7.3. 仪表盘消费事件¶
把这段加入 app.js:
const events = document.getElementById('events');
const source = new EventSource('/events');
source.addEventListener('motion', (e) => {
const data = JSON.parse(e.data);
const li = document.createElement('li');
const t = new Date(data.ts * 1000).toLocaleTimeString();
li.textContent = t + ' -- change ' + data.change;
events.prepend(li);
});
浏览器会向 /events 打开一个持久的 HTTP 连接,并在任何断开时自动重新打开它。摄像头推送的每一个 motion 事件都会作为一个新的 <li> 出现在列表顶部。
现在拥有者会在运动触发的瞬间就看到运动事件。