| 
 | 1 | +# SPDX-FileCopyrightText: 2024 Michał Pokusa  | 
 | 2 | +#  | 
 | 3 | +# SPDX-License-Identifier: Unlicense  | 
 | 4 | + | 
 | 5 | +try:  | 
 | 6 | +    from typing import Dict, List, Tuple, Union  | 
 | 7 | +except ImportError:  | 
 | 8 | +    pass  | 
 | 9 | + | 
 | 10 | +from asyncio import create_task, gather, run, sleep  | 
 | 11 | +from random import choice  | 
 | 12 | + | 
 | 13 | +import socketpool  | 
 | 14 | +import wifi  | 
 | 15 | + | 
 | 16 | +from adafruit_pycamera import PyCamera  | 
 | 17 | +from adafruit_httpserver import Server, Request, Response, Headers, Status, OK_200  | 
 | 18 | + | 
 | 19 | + | 
 | 20 | +pool = socketpool.SocketPool(wifi.radio)  | 
 | 21 | +server = Server(pool, debug=True)  | 
 | 22 | + | 
 | 23 | + | 
 | 24 | +camera = PyCamera()  | 
 | 25 | +camera.display.brightness = 0  | 
 | 26 | +camera.mode = 0  # JPEG, required for `capture_into_jpeg()`  | 
 | 27 | +camera.resolution = "1280x720"  | 
 | 28 | +camera.effect = 0  # No effect  | 
 | 29 | + | 
 | 30 | + | 
 | 31 | +class XMixedReplaceResponse(Response):  | 
 | 32 | +    def __init__(  | 
 | 33 | +        self,  | 
 | 34 | +        request: Request,  | 
 | 35 | +        frame_content_type: str,  | 
 | 36 | +        *,  | 
 | 37 | +        status: Union[Status, Tuple[int, str]] = OK_200,  | 
 | 38 | +        headers: Union[Headers, Dict[str, str]] = None,  | 
 | 39 | +        cookies: Dict[str, str] = None,  | 
 | 40 | +    ) -> None:  | 
 | 41 | +        super().__init__(  | 
 | 42 | +            request=request,  | 
 | 43 | +            headers=headers,  | 
 | 44 | +            cookies=cookies,  | 
 | 45 | +            status=status,  | 
 | 46 | +        )  | 
 | 47 | +        self._boundary = self._get_random_boundary()  | 
 | 48 | +        self._headers.setdefault(  | 
 | 49 | +            "Content-Type", f"multipart/x-mixed-replace; boundary={self._boundary}"  | 
 | 50 | +        )  | 
 | 51 | +        self._frame_content_type = frame_content_type  | 
 | 52 | + | 
 | 53 | +    @staticmethod  | 
 | 54 | +    def _get_random_boundary() -> str:  | 
 | 55 | +        symbols = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"  | 
 | 56 | +        return "--" + "".join([choice(symbols) for _ in range(16)])  | 
 | 57 | + | 
 | 58 | +    def send_frame(self, frame: Union[str, bytes] = "") -> None:  | 
 | 59 | +        encoded_frame = bytes(  | 
 | 60 | +            frame.encode("utf-8") if isinstance(frame, str) else frame  | 
 | 61 | +        )  | 
 | 62 | + | 
 | 63 | +        self._send_bytes(  | 
 | 64 | +            self._request.connection, bytes(f"{self._boundary}\r\n", "utf-8")  | 
 | 65 | +        )  | 
 | 66 | +        self._send_bytes(  | 
 | 67 | +            self._request.connection,  | 
 | 68 | +            bytes(f"Content-Type: {self._frame_content_type}\r\n\r\n", "utf-8"),  | 
 | 69 | +        )  | 
 | 70 | +        self._send_bytes(self._request.connection, encoded_frame)  | 
 | 71 | +        self._send_bytes(self._request.connection, bytes("\r\n", "utf-8"))  | 
 | 72 | + | 
 | 73 | +    def _send(self) -> None:  | 
 | 74 | +        self._send_headers()  | 
 | 75 | + | 
 | 76 | +    def close(self) -> None:  | 
 | 77 | +        self._close_connection()  | 
 | 78 | + | 
 | 79 | + | 
 | 80 | +stream_connections: List[XMixedReplaceResponse] = []  | 
 | 81 | + | 
 | 82 | + | 
 | 83 | +@server.route("/frame")  | 
 | 84 | +def frame_handler(request: Request):  | 
 | 85 | +    frame = camera.capture_into_jpeg()  | 
 | 86 | + | 
 | 87 | +    return Response(request, body=frame, content_type="image/jpeg")  | 
 | 88 | + | 
 | 89 | + | 
 | 90 | +@server.route("/stream")  | 
 | 91 | +def stream_handler(request: Request):  | 
 | 92 | +    response = XMixedReplaceResponse(request, frame_content_type="image/jpeg")  | 
 | 93 | +    stream_connections.append(response)  | 
 | 94 | + | 
 | 95 | +    return response  | 
 | 96 | + | 
 | 97 | + | 
 | 98 | +async def send_stream_frames():  | 
 | 99 | +    while True:  | 
 | 100 | +        await sleep(0.1)  | 
 | 101 | + | 
 | 102 | +        frame = camera.capture_into_jpeg()  | 
 | 103 | + | 
 | 104 | +        for connection in iter(stream_connections):  | 
 | 105 | +            try:  | 
 | 106 | +                connection.send_frame(frame)  | 
 | 107 | +            except BrokenPipeError:  | 
 | 108 | +                connection.close()  | 
 | 109 | +                stream_connections.remove(connection)  | 
 | 110 | + | 
 | 111 | + | 
 | 112 | +async def handle_http_requests():  | 
 | 113 | +    server.start(str(wifi.radio.ipv4_address))  | 
 | 114 | + | 
 | 115 | +    while True:  | 
 | 116 | +        await sleep(0)  | 
 | 117 | + | 
 | 118 | +        server.poll()  | 
 | 119 | + | 
 | 120 | + | 
 | 121 | +async def main():  | 
 | 122 | +    await gather(  | 
 | 123 | +        create_task(send_stream_frames()),  | 
 | 124 | +        create_task(handle_http_requests()),  | 
 | 125 | +    )  | 
 | 126 | + | 
 | 127 | + | 
 | 128 | +run(main())  | 
0 commit comments