10.4. Tek bir yakalama döngüsünü izleyiciler arasında paylaşma¶
Bağlanan her istemcinin bağımsız olarak csi0.snapshot() çağırması savurgandır ve aynı anda iki yayın açıldığında durum daha da kötüleşir: sensör çerçeveleri kendi hızında teslim eder ve her kopyalanan yakalama herkesi yavaşlatır. Doğru yaklaşım, “en son çerçeveyi” paylaşılan bir yuvaya yayınlayan tek bir yakalama eşyordamı, artı yuvadan okuyan istemci başına yineleyicilerdir.
10.4.1. Yakalama görevi¶
Bir arka plan eşyordamı, sensör teslim ettiği kadar hızlı çerçeve alır, her birini paylaşılan bir bytes içine JPEG olarak sıkıştırır ve bekleyen herhangi bir istemcinin uyanması için bir olay atımı gönderir:
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() çifti atım desenidir. set(), o anda olayı bekleyen her eşyordamı tek seferde serbest bırakır; clear() olayı hemen sıfırlar, böylece sonraki wait() tekrar engellenir. Birden çok tüketiciyle (bir izleyici, başka bir izleyici, yeni bir çerçeveye tepki vermesi gereken başka herhangi bir eşyordam) hiçbir tek tüketici olayı sıfırlamaktan sorumlu değildir ve hiç kimse başkasının uyandırılmasını çalmaz.
Not
JPEG’in etrafındaki bytes(...) sarmalaması burada yük taşıyıcıdır. bytearray(), kameranın görüntü arabelleğine bir görünüm döndürür; hemen sonraki snapshot() çağrısı o arabelleği bir sonraki çerçeveyle yerinde yeniden yazar. latest_jpeg, yerel img değişkeninden daha uzun yaşar, bu yüzden kopya olmadan her okuyucu, her yakalamada yuvanın altlarından kaymasını görürdü.
10.4.2. İstemci başına yineleyiciler yuvadan okur¶
MJPEG yayın işleyicisi artık kendisi csi0.snapshot() çağırmayı bırakır. Bunun yerine, her FrameStream örneği paylaşılan olayı bekler ve paylaşılan baytlardan okur:
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')
Anlık görüntü rotası da değişir: artık bir yakalama tetiklemez, latest_jpeg‘in o anda tuttuğu her şeyi döndürür:
@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) demeti, bir microdot.Response oluşturmadan bir HTTP durum kodu ayarlamak için microdot’un kısaltmasıdır. 503, buradayım ama hazır değilim der – “birazdan tekrar sor” için doğru kod.
10.4.3. Yakalamayı sunucuyla birlikte çalıştırma¶
main artık iki üst düzey eşyordama sahiptir: yakalama döngüsü ve HTTP sunucusu. asyncio.gather() her ikisini de çalıştırır ve biri çökerse diğeri iptal edilir:
async def main():
await asyncio.gather(
capture_loop(),
app.start_server(host='0.0.0.0', port=80),
)
asyncio.run(main())
Artık kaç izleyici bağlı olursa olsun sensör döngü başına bir çerçeve okur. /stream.jpg adresine ilk gelen tarayıcı çerçeveleri görür; ikincisi de, üçüncüsü de, onuncusu da – hepsi aynı yakalamayı paylaşır ve kamera diğer rotalarında yanıt vermeye devam eder.