10.4. Berbagi satu loop pengambilan gambar di antara beberapa penonton

Setiap klien yang terhubung memanggil csi0.snapshot() secara mandiri adalah pemborosan, dan setelah dua aliran terbuka sekaligus situasinya semakin buruk: sensor mengirimkan bingkai pada kecepatannya sendiri, dan setiap pengambilan gambar yang digandakan memperlambat semua orang. Pendekatan yang tepat adalah satu coroutine pengambilan gambar yang mempublikasikan "bingkai terbaru" ke slot bersama, ditambah iterator per klien yang membaca dari slot tersebut.

One capture task writes JPEG bytes to a single latest_jpeg slot; three stream-client iterators read from the slot and each wait on the shared new_frame event.

10.4.1. Task pengambilan gambar

Coroutine latar belakang mengambil bingkai secepat sensor mengirimkannya, mengompresi setiap bingkai ke JPEG ke dalam bytes bersama, dan memicu event agar klien yang menunggu dapat bangun:

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

Pasangan set() / clear() adalah pola pulse. set() membuka blokir setiap coroutine yang saat ini menunggu event sekaligus; clear() segera mengatur ulang event sehingga wait() berikutnya memblokir kembali. Dengan beberapa konsumen (satu penonton, penonton lain, coroutine lain yang perlu bereaksi terhadap bingkai baru), tidak ada satu konsumen yang bertanggung jawab mengatur ulang event, dan tidak ada yang mencuri pembangkitan dari orang lain.

Catatan

Pembungkus bytes(...) di sekitar JPEG sangat penting di sini. bytearray() mengembalikan tampilan ke buffer citra kamera; panggilan snapshot() berikutnya menulis ulang buffer tersebut di tempat dengan bingkai berikutnya. latest_jpeg bertahan melampaui variabel lokal img, sehingga tanpa salinan setiap pembaca akan melihat slot berubah di bawah mereka pada setiap pengambilan gambar.

10.4.2. Iterator per klien membaca dari slot

Handler aliran MJPEG berhenti memanggil csi0.snapshot() sendiri. Sebaliknya, setiap instans FrameStream menunggu event bersama dan membaca dari bytes bersama:

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

Rute snapshot juga berubah: rute tersebut tidak lagi memicu pengambilan gambar, melainkan mengembalikan apa pun yang saat ini ada di latest_jpeg:

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

Tuple (body, status) adalah singkatan microdot untuk menetapkan kode status HTTP tanpa membuat microdot.Response. 503 berarti saya ada tetapi belum siap -- kode yang tepat untuk "coba lagi sebentar".

10.4.3. Menjalankan pengambilan gambar bersamaan dengan server

main sekarang memiliki dua coroutine level atas: loop pengambilan gambar dan server HTTP. asyncio.gather() menjalankan keduanya, dan jika salah satu crash yang lainnya dibatalkan:

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

asyncio.run(main())

Sekarang sensor membaca satu bingkai per siklus tidak peduli berapa banyak penonton yang terhubung. Browser pertama ke /stream.jpg melihat bingkai; begitu juga yang kedua, ketiga, kesepuluh -- semuanya berbagi pengambilan gambar yang sama, dan kamera tetap responsif pada rute lainnya.