10.3. Live-suoratoisto – yksi katsoja

Selaimet voivat renderöidä moniosaisia Motion JPEG (MJPEG) -virtoja suoraan <img>-tagin sisällä. Anna selaimelle yksi HTTP-vastaus, joka ei koskaan pääty, kirjoita JPEG-kuvia, jotka on erotettu moniosaisella rajaimella, ja selain näyttää jokaisen kehyksen sitä mukaa kun se saapuu.

Selain lähettää GET /stream.jpg; kamera vastaa Content-Type multipart/x-mixed-replace -tyypillä ja kirjoittaa yhden JPEG-runkoisen osan kehystä kohden, kunnes selain katkaisee yhteyden.

Siirtotie on suoraviivainen: yksi vastausotsake, Content-Type: multipart/x-mixed-replace; boundary=frame, sitten --frame-rivi, Content-Type: image/jpeg, tyhjä rivi, JPEG-tavut, \r\n, ja toistetaan. Selain sulkee yhteyden, kun <img> poistetaan tai välilehti suljetaan.

10.3.1. Kaappaaminen ilman estämistä

Tähän asti käytetty estävä csi0.snapshot() pysäyttää koko tapahtumasilmukan, kunnes sensori toimittaa kehyksen. Se oli hyvä, kun yksi pyyntö laukaisi yhden tilannekuvan eikä mitään muuta ollut käynnissä. Kun virta on auki, palvelimen on jatkettava muiden pyyntöjen käsittelyä samalla kun seuraavaa kehystä kaapataan – kaappauskutsun on luovutettava vuoro tapahtumasilmukalle odottaessaan sensoria.

Kuvio on ohut AsyncCSI-kääre, joka pollaa metodia csi.CSI.snapshot() ei-estävässä tilassa ja nukuttaa korutiinin pollausten välissä. Asyncio-luku kävi tämän kuvion läpi kohdassa AsyncCSI; liitä se toistaiseksi suoraan skriptiin:

import asyncio

class AsyncCSI:
    def __init__(self, *args, **kwargs):
        self._csi = csi.CSI(*args, **kwargs)

    def __getattr__(self, name):
        return getattr(self._csi, name)

    async def snapshot(self):
        while True:
            img = self._csi.snapshot(blocking=False)
            if img is not None:
                return img
            await asyncio.sleep_ms(0)

Jokainen muu CSI-metodi (reset(), pixformat(), framesize(), gain_db(), …) välitetään eteenpäin __getattr__-metodin kautta; vain snapshot() korvataan odotettavissa olevalla versiolla, joka antaa tapahtumasilmukan ajoittaa muita korutiineja pollausten välissä.

Vaihda tilannekuvareitin paljas csi.CSI() AsyncCSI()-instanssiin:

csi0 = AsyncCSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)

10.3.2. Suoratoistorungot ovat luokkapohjaisia iteraattoreita

Suoratoistovastauksen runko on vain objekti, jota microdot iteroi async for -rakenteella ja lähettää jokaisen tuotetun palan socketiin. CPythonissa tämä on yleensä asynkroninen generaattorifunktioasync def ja yield. MicroPython ei tue tätä:

Muista

MicroPythonin asyncio ei tue asynkronisia generaattorifunktioita (async def name(): ... yield ...). Suoratoistovastausten runkojen on oltava luokkapohjaisia asynkronisia iteraattoreita, joiden __aiter__ palauttaa self-arvon ja joiden __anext__ on määritelty async def-muodossa.

MJPEG-virralle se tarkoittaa luokkaa, jonka __anext__ odottaa yhtä kehystä ja palauttaa sen kehystettynä moniosaiseen kääreeseen:

BOUNDARY = b'frame'

class FrameStream:
    def __aiter__(self):
        return self

    async def __anext__(self):
        img = await csi0.snapshot()
        jpeg = bytes(img.compress(quality=85).bytearray())
        return (b'--' + BOUNDARY + b'\r\n'
                b'Content-Type: image/jpeg\r\n\r\n'
                + jpeg + b'\r\n')

@app.get('/stream.jpg')
async def stream(request):
    return Response(
        body=FrameStream(),
        headers={
            'Content-Type':
                b'multipart/x-mixed-replace; boundary=' + BOUNDARY,
        },
    )

Instanssi on tuore pyyntöä kohden, joten jokainen yhdistetty asiakas saa oman iteraattorinsa. Kun selain katkaisee yhteyden, microdot lopettaa __anext__-metodin odottamisen ja iteraattori kerätään roskien mukana.

Muista

JPEG:in ympärille kiedottu bytes(...) on puolustautuva. bytearray() palauttaa näkymän kameran kuvapuskuriin, ja seuraava snapshot()-kutsu kirjoittaa kyseisen puskurin uudelleen paikan päällä. Kietominen bytes-tyyppiin kopioi JPEG:in ulos, jotta pala, jota microdot on keskellä kirjoitusta, pysyy vakaana, vaikka kirjoittajan huuhtelu ei olisi valmis siihen mennessä, kun __anext__ ajetaan taas.

10.3.3. Palvelimen ajaminen asyncion sisällä

Aiempi app.run(host=..., port=...) -kutsu on estävä. MJPEG-käsittelijän on jaettava silmukka AsyncCSI:n tilannekuvapollausten kanssa, joten vaihda app.run metodiin start_server() asyncio.run()-kutsun sisällä:

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

asyncio.run(main())

asyncio.run()-kääre antaa palvelimen olla yksi tehtävä useiden joukossa – main-korutiini on silloin luonnollinen paikka käynnistää kaappaus, liiketunnistus ja kaikki muu, mikä joutuu jakamaan silmukan HTTP-palvelimen kanssa.

10.3.4. Yksi katsoja kerrallaan

Jokainen yhdistetty asiakas ajaa omaa FrameStream-iteraattoriaan, mikä tarkoittaa, että jokainen asiakas laukaisee oman csi0.snapshot()-kutsunsa. Kaksi selainta tarkoittaa kahta sensorilukua kehysväliä kohden, kolme tarkoittaa kolmea ja niin edelleen. Sensori ei voi tosiasiassa toimittaa kehyksiä omaa kehysnopeuttaan nopeammin, joten pyynnöt jonottavat toistensa taakse ja kaikkien virta hidastuu.

Korjaus on yksi jaettu kaappaussilmukka, joka julkaisee yhden kehyksen monelle lukijalle.