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.
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 generaattorifunktio – async 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.