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.
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.