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.