10.4. Dijeljenje jedne petlje snimanja među gledateljima¶
Da svaki povezani klijent neovisno poziva csi0.snapshot() rasipno je, a kada su dva strujanja otvorena odjednom postaje gore: senzor isporučuje sličice svojom vlastitom brzinom, a svako udvostručeno snimanje usporava svakoga. Ispravan pristup je jedna korutina snimanja koja objavljuje „najnoviju sličicu” u dijeljeni utor, plus iteratori po klijentu koji čitaju iz utora.
10.4.1. Zadatak snimanja¶
Pozadinska korutina hvata sličice brzinom kojom ih senzor isporučuje, JPEG-komprimira svaku u dijeljeni bytes i okida događaj kako bi se svaki klijent koji čeka probudio:
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()
Par set() / clear() je uzorak pulsa. set() odjednom odblokira svaku korutinu koja trenutno čeka na događaju; clear() odmah resetira događaj pa sljedeći wait() ponovno blokira. S više potrošača (gledatelj, drugi gledatelj, bilo koja druga korutina koja mora reagirati na novu sličicu), nijedan pojedinačni potrošač nije odgovoran za resetiranje događaja, i nitko nikome ne krade buđenje.
Napomena
Omot bytes(...) oko JPEG-a ovdje je nosivi element. bytearray() vraća pogled u međuspremnik slike kamere; sljedeći poziv snapshot() prepisuje taj međuspremnik na licu mjesta sljedećom sličicom. latest_jpeg nadživljava lokalni img, pa bi bez kopije svaki čitatelj vidio kako se utor pomiče ispod njega pri svakom snimanju.
10.4.2. Iteratori po klijentu čitaju iz utora¶
MJPEG rukovatelj strujanja prestaje sam pozivati csi0.snapshot(). Umjesto toga, svaka FrameStream instanca čeka na dijeljeni događaj i čita iz dijeljenih bajtova:
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')
Mijenja se i ruta snimanja: više ne pokreće snimanje, vraća što god latest_jpeg trenutno sadrži:
@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'},
)
Tuple (body, status) je microdotova skraćenica za postavljanje HTTP statusnog koda bez konstruiranja microdot.Response. 503 kaže ovdje sam, ali nisam spreman – ispravan kod za „pitaj ponovno za trenutak.”
10.4.3. Pokretanje snimanja uz poslužitelj¶
main sada ima dvije korutine na najvišoj razini: petlju snimanja i HTTP poslužitelj. asyncio.gather() pokreće obje, a ako se bilo koja sruši, druga se otkazuje:
async def main():
await asyncio.gather(
capture_loop(),
app.start_server(host='0.0.0.0', port=80),
)
asyncio.run(main())
Sada senzor čita jednu sličicu po ciklusu bez obzira na to koliko je gledatelja povezano. Prvi preglednik na /stream.jpg vidi sličice; tako i drugi, treći, deseti – svi dijele isto snimanje, a kamera ostaje jednako odzivna na svojim ostalim rutama.