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.

One capture task writes JPEG bytes to a single latest_jpeg slot; three stream-client iterators read from the slot and each wait on the shared new_frame event.

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.