10.3. Živé streamování – jeden divák#
Prohlížeče dokážou vykreslit vícedílné Motion JPEG (MJPEG) streamy přímo uvnitř tagu <img>. Předejte prohlížeči jednu HTTP odpověď, která nikdy neskončí, zapisujte JPEGy oddělené multipart hranicí a prohlížeč zobrazí každý snímek, jakmile dorazí.
Přenos je přímočarý: jedna hlavička odpovědi, Content-Type: multipart/x-mixed-replace; boundary=frame, poté řádek --frame, Content-Type: image/jpeg, prázdný řádek, JPEG bajty, \r\n a opakovat. Prohlížeč spojení uzavře, když je <img> odebrán nebo je zavřena karta.
10.3.1. Zachytávání bez blokování#
Doposud používané blokující csi0.snapshot() zastaví celou event loop, dokud senzor nedodá snímek. To bylo v pořádku, když jeden požadavek spustil jeden snímek a nic jiného neběželo. Jakmile je stream otevřen, server musí pokračovat v obsluze ostatních požadavků, zatímco se zachytává další snímek – volání zachycení musí předat řízení event loopu, zatímco čeká na senzor.
Vzorem je tenký wrapper AsyncCSI, který dotazuje csi.CSI.snapshot() v neblokujícím režimu a mezi dotazy korutinu uspí. Kapitola o asyncio tento vzor prošla v AsyncCSI; pro teď jej vložte přímo do skriptu:
import asyncio
class AsyncCSI:
def __init__(self, *args, **kwargs):
self._csi = csi.CSI(*args, **kwargs)
def __getattr__(self, name):
return getattr(self._csi, name)
async def snapshot(self):
while True:
img = self._csi.snapshot(blocking=False)
if img is not None:
return img
await asyncio.sleep_ms(0)
Každá další metoda CSI (reset(), pixformat(), framesize(), gain_db(), …) je přeposlána přes __getattr__; pouze snapshot() je nahrazena awaitable verzí, která umožňuje event loopu naplánovat mezi dotazy jiné korutiny.
Vyměňte holé csi.CSI() z cesty snapshot za AsyncCSI():
csi0 = AsyncCSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)
10.3.2. Těla streamů jsou iterátory založené na třídách#
Tělo streamovací odpovědi je jen objekt, který microdot iteruje pomocí async for a posílá každý vyprodukovaný kus do socketu. V CPythonu je to běžně async generátorová funkce – async def s yield. MicroPython to nepodporuje:
Poznámka
asyncio v MicroPythonu nepodporuje async-generátorové funkce (async def name(): ... yield ...). Těla streamovacích odpovědí musí být async iterátory založené na třídách s __aiter__ vracejícím self a __anext__ definovaným jako async def.
Pro MJPEG stream to znamená třídu, jejíž __anext__ počká na jeden snímek a vrátí jej zabalený v multipart obalu:
BOUNDARY = b'frame'
class FrameStream:
def __aiter__(self):
return self
async def __anext__(self):
img = await csi0.snapshot()
jpeg = bytes(img.compress(quality=85).bytearray())
return (b'--' + BOUNDARY + b'\r\n'
b'Content-Type: image/jpeg\r\n\r\n'
+ jpeg + b'\r\n')
@app.get('/stream.jpg')
async def stream(request):
return Response(
body=FrameStream(),
headers={
'Content-Type':
b'multipart/x-mixed-replace; boundary=' + BOUNDARY,
},
)
Instance je pro každý požadavek čerstvá, takže každý připojený klient dostane svůj vlastní iterátor. Když se prohlížeč odpojí, microdot přestane čekat na __anext__ a iterátor je uvolněn garbage collectorem.
Poznámka
Obal bytes(...) kolem JPEGu je obranný. bytearray() vrací pohled do obrazového bufferu kamery a další volání snapshot() tento buffer přepíše na místě. Zabalení do bytes zkopíruje JPEG ven, takže kus, který microdot právě zapisuje, zůstane stabilní, i kdyby zapisovač nedokončil vyprázdnění, než se __anext__ znovu spustí.
10.3.3. Spuštění serveru uvnitř asyncio#
Dřívější volání app.run(host=..., port=...) je blokující. MJPEG handler potřebuje sdílet smyčku s dotazy AsyncCSI snapshot, takže vyměňte app.run za start_server() uvnitř asyncio.run():
async def main():
await app.start_server(host='0.0.0.0', port=80)
asyncio.run(main())
Wrapper asyncio.run() umožňuje, aby byl server jednou úlohou z několika – korutina main je pak přirozeným místem pro spuštění zachytávání, detekce pohybu a čehokoli dalšího, co musí sdílet smyčku s HTTP serverem.
10.3.4. Jeden divák najednou#
Každý připojený klient spouští svůj vlastní iterátor FrameStream, což znamená, že každý klient spouští své vlastní volání csi0.snapshot(). Dva prohlížeče znamenají dvě čtení senzoru na interval snímku, tři znamenají tři a tak dále. Senzor ve skutečnosti nedokáže dodávat snímky rychleji než je jeho vlastní snímková frekvence, takže se požadavky řadí za sebe a stream každého se zpomalí.
Řešením je jediná sdílená smyčka zachytávání, která publikuje jeden snímek mnoha čtenářům.