10.4. Partilhar um ciclo de captura entre vários espetadores¶
Cada cliente ligado a chamar csi0.snapshot() de forma independente é ineficiente e, quando dois streams estão abertos em simultâneo, a situação agrava-se: o sensor entrega fotogramas ao seu próprio ritmo, e cada captura duplicada abranda toda a gente. A abordagem correta é uma corrotina de captura única que publica «o fotograma mais recente» numa posição partilhada, mais iteradores por cliente que lêem dessa posição.
10.4.1. A tarefa de captura¶
Uma corrotina em segundo plano obtém fotogramas tão rapidamente quanto o sensor os entrega, comprime cada um em JPEG para um bytes partilhado e emite um evento para que qualquer cliente em espera acorde:
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()
O par set() / clear() é o padrão de pulso. set() desbloqueia de uma vez todas as corrotinas atualmente à espera do evento; clear() repõe imediatamente o evento para que o próximo wait() bloqueie novamente. Com múltiplos consumidores (um espetador, outro espetador, qualquer outra corrotina que precise de reagir a um novo fotograma), nenhum consumidor é responsável por repor o evento e ninguém rouba um despertar a outra pessoa.
Nota
O embrulho bytes(...) em redor do JPEG é aqui essencial. bytearray() devolve uma vista para o buffer de imagem da câmara; a próxima chamada a snapshot() reescreve esse buffer no local com o fotograma seguinte. latest_jpeg sobrevive ao img local, por isso sem a cópia cada leitor veria a posição mudar sob os seus pés a cada captura.
10.4.2. Os iteradores por cliente lêem da posição partilhada¶
O handler da stream MJPEG deixa de chamar csi0.snapshot() por si mesmo. Em vez disso, cada instância de FrameStream aguarda no evento partilhado e lê dos bytes partilhados:
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')
A rota de captura também muda: já não despoleta uma captura, devolve o que latest_jpeg contiver nesse momento:
@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'},
)
A tupla (body, status) é a abreviatura do microdot para definir um código de estado HTTP sem construir um microdot.Response. 503 significa estou aqui mas não estou pronto – o código correto para «tente novamente daqui a pouco».
10.4.3. Executar a captura a par do servidor¶
main tem agora duas corrotinas de nível superior: o ciclo de captura e o servidor HTTP. asyncio.gather() executa ambas e, se alguma falhar, a outra é cancelada:
async def main():
await asyncio.gather(
capture_loop(),
app.start_server(host='0.0.0.0', port=80),
)
asyncio.run(main())
Agora o sensor lê um fotograma por ciclo independentemente de quantos espetadores estejam ligados. O primeiro browser em /stream.jpg vê fotogramas; o segundo também, o terceiro, o décimo – todos partilham a mesma captura e a câmara mantém-se igualmente responsiva nas suas outras rotas.