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.

Jedan zadatak snimanja zapisuje JPEG bajtove u jedan latest_jpeg utor; tri iteratora klijenata strujanja čitaju iz utora i svaki čeka na dijeljeni new_frame događaj.

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.