10.4. Yhden kaappaussilmukan jakaminen katsojien kesken

Jokaisen yhdistetyn asiakkaan kutsuessa csi0.snapshot() itsenäisesti on tuhlaavaa, ja kun kaksi virtaa on auki yhtä aikaa, tilanne pahenee: sensori toimittaa kehyksiä omalla nopeudellaan, ja jokainen toistettu kaappaus hidastaa kaikkia. Oikea lähestymistapa on yksi kaappauskorutiini, joka julkaisee ”uusimman kehyksen” jaettuun lokeroon, sekä asiakaskohtaiset iteraattorit, jotka lukevat lokerosta.

Yksi kaappaustehtävä kirjoittaa JPEG-tavut yhteen latest_jpeg- lokeroon; kolme virta-asiakkaan iteraattoria lukee lokerosta ja kukin odottaa jaettua new_frame-tapahtumaa.

10.4.1. Kaappaustehtävä

Taustakorutiini nappaa kehyksiä niin nopeasti kuin sensori niitä toimittaa, JPEG-pakkaa jokaisen jaetuksi bytes-arvoksi ja sykäyttää tapahtuman, jotta mikä tahansa odottava asiakas herää:

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()

set() / clear() -pari on sykäys-kuvio. set() vapauttaa kerralla jokaisen korutiinin, joka tällä hetkellä odottaa tapahtumaa; clear() nollaa tapahtuman välittömästi, jotta seuraava wait() estyy taas. Useiden kuluttajien kanssa (katsoja, toinen katsoja, mikä tahansa muu korutiini, jonka on reagoitava uuteen kehykseen) yksikään kuluttaja ei ole vastuussa tapahtuman nollaamisesta, eikä kukaan varasta herätystä keneltäkään.

Muista

JPEG:in ympärille kiedottu bytes(...) on tässä kantava. bytearray() palauttaa näkymän kameran kuvapuskuriin; aivan seuraava snapshot()-kutsu kirjoittaa kyseisen puskurin uudelleen paikan päällä seuraavalla kehyksellä. latest_jpeg elää paikallista img-muuttujaa pidempään, joten ilman kopiota jokainen lukija näkisi lokeron muuttuvan altaan jokaisella kaappauksella.

10.4.2. Asiakaskohtaiset iteraattorit lukevat lokerosta

MJPEG-virran käsittelijä lakkaa kutsumasta itse csi0.snapshot()-metodia. Sen sijaan jokainen FrameStream-instanssi odottaa jaettua tapahtumaa ja lukee jaetuista tavuista:

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')

Tilannekuvareitti muuttuu myös: se ei enää laukaise kaappausta, vaan palauttaa sen mitä latest_jpeg parhaillaan sisältää:

@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'},
    )

(body, status) -monikko on microdotin pikamuoto HTTP-tilakoodin asettamiseen ilman microdot.Response-objektin rakentamista. 503 sanoo olen täällä mutta en valmis – oikea koodi viestille ”kysy uudelleen hetken kuluttua”.

10.4.3. Kaappauksen ajaminen palvelimen rinnalla

main-funktiolla on nyt kaksi ylätason korutiinia: kaappaussilmukka ja HTTP-palvelin. asyncio.gather() ajaa molempia, ja jos kumpi tahansa kaatuu, toinen perutaan:

async def main():
    await asyncio.gather(
        capture_loop(),
        app.start_server(host='0.0.0.0', port=80),
    )

asyncio.run(main())

Nyt sensori lukee yhden kehyksen sykliä kohden riippumatta siitä, kuinka monta katsojaa on yhdistettynä. Ensimmäinen selain, joka avaa /stream.jpg, näkee kehyksiä; samoin toinen, kolmas ja kymmenes – ne kaikki jakavat saman kaappauksen, ja kamera pysyy yhtä responsiivisena muilla reiteillään.