10.4. Sdílení jedné smyčky zachytávání mezi diváky¶
Když každý připojený klient volá csi0.snapshot() nezávisle, je to plýtvání, a jakmile jsou otevřeny dva streamy najednou, je to horší: senzor dodává snímky vlastní rychlostí a každé duplikované zachycení všechny zpomaluje. Správným přístupem je jedna korutina zachytávání, která publikuje „nejnovější snímek“ do sdíleného slotu, plus iterátory pro jednotlivé klienty, které čtou ze slotu.
10.4.1. Úloha zachytávání¶
Korutina na pozadí získává snímky tak rychle, jak je senzor dodává, každý JPEG zkomprimuje do sdíleného bytes a vyšle puls události, aby se probudil každý čekající klient:
latest_jpeg = None
new_frame = asyncio.Event()
async def capture_loop():
global latest_jpeg
while True:
img = await csi0.snapshot()
latest_jpeg = bytes(img.compress(quality=85).bytearray())
new_frame.set()
new_frame.clear()
Dvojice set() / clear() je vzor puls. set() odblokuje najednou každou korutinu, která aktuálně čeká na událost; clear() událost okamžitě resetuje, takže další wait() zase zablokuje. S více konzumenty (jeden divák, další divák, jakákoli jiná korutina, která potřebuje reagovat na nový snímek) není žádný jednotlivý konzument zodpovědný za resetování události a nikdo nikomu neukradne probuzení.
Poznámka
Obal bytes(...) kolem JPEGu je zde nosný. bytearray() vrací pohled do obrazového bufferu kamery; úplně další volání snapshot() tento buffer přepíše na místě dalším snímkem. latest_jpeg přežívá lokální img, takže bez kopie by každý čtenář viděl, jak se mu slot při každém zachycení posouvá pod rukama.
10.4.2. Iterátory jednotlivých klientů čtou ze slotu¶
Handler MJPEG streamu sám přestane volat csi0.snapshot(). Místo toho každá instance FrameStream čeká na sdílenou událost a čte ze sdílených bajtů:
class FrameStream:
# One instance per connected client. Each one independently
# waits on the shared new_frame pulse; the capture loop is
# responsible for resetting the event between frames.
def __aiter__(self):
return self
async def __anext__(self):
await new_frame.wait()
if latest_jpeg is None:
return b''
return (b'--' + BOUNDARY + b'\r\n'
b'Content-Type: image/jpeg\r\n\r\n'
+ latest_jpeg + b'\r\n')
Cesta snapshot se také mění: již nespouští zachycení, vrací cokoli, co latest_jpeg aktuálně drží:
@app.get('/snapshot.jpg')
async def snapshot(request):
if latest_jpeg is None:
return 'no frame yet', 503
return Response(
body=latest_jpeg,
headers={'Content-Type': 'image/jpeg'},
)
N-tice (body, status) je microdotová zkratka pro nastavení HTTP stavového kódu bez konstrukce microdot.Response. 503 říká jsem zde, ale nejsem připraven – správný kód pro „zeptejte se znovu za chvíli.“
10.4.3. Spuštění zachytávání spolu se serverem¶
main nyní má dvě korutiny nejvyšší úrovně: smyčku zachytávání a HTTP server. asyncio.gather() spustí obě, a pokud kterákoli spadne, druhá je zrušena:
async def main():
await asyncio.gather(
capture_loop(),
app.start_server(host='0.0.0.0', port=80),
)
asyncio.run(main())
Nyní senzor čte jeden snímek na cyklus bez ohledu na to, kolik diváků je připojeno. První prohlížeč na /stream.jpg vidí snímky; stejně tak druhý, třetí, desátý – všichni sdílejí stejné zachytávání a kamera zůstává stejně responzivní na svých ostatních cestách.