Skip to content

Commit ec89785

Browse files
committed
Added example code for video streaming using multipart/x-mixed-replace content type
1 parent 0e6180d commit ec89785

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed

examples/httpserver_video_stream.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
content_type: str = None,
41+
) -> None:
42+
super().__init__(
43+
request=request,
44+
headers=headers,
45+
cookies=cookies,
46+
status=status,
47+
content_type=content_type,
48+
)
49+
self._boundary = self._get_random_boundary()
50+
self._headers.setdefault(
51+
"Content-Type", f"multipart/x-mixed-replace; boundary={self._boundary}"
52+
)
53+
self._frame_content_type = frame_content_type
54+
55+
@staticmethod
56+
def _get_random_boundary() -> str:
57+
symbols = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
58+
return "--" + "".join([choice(symbols) for _ in range(16)])
59+
60+
def send_frame(self, frame: Union[str, bytes] = "") -> None:
61+
encoded_frame = bytes(
62+
frame.encode("utf-8") if isinstance(frame, str) else frame
63+
)
64+
65+
self._send_bytes(
66+
self._request.connection, bytes(f"{self._boundary}\r\n", "utf-8")
67+
)
68+
self._send_bytes(
69+
self._request.connection,
70+
bytes(f"Content-Type: {self._frame_content_type}\r\n\r\n", "utf-8"),
71+
)
72+
self._send_bytes(self._request.connection, encoded_frame)
73+
self._send_bytes(self._request.connection, bytes("\r\n", "utf-8"))
74+
75+
def _send(self) -> None:
76+
self._send_headers()
77+
78+
def close(self) -> None:
79+
self._close_connection()
80+
81+
82+
stream_connections: List[XMixedReplaceResponse] = []
83+
84+
85+
@server.route("/frame")
86+
def frame_handler(request: Request):
87+
frame = camera.capture_into_jpeg()
88+
89+
return Response(request, body=frame, content_type="image/jpeg")
90+
91+
92+
@server.route("/stream")
93+
def stream_handler(request: Request):
94+
response = XMixedReplaceResponse(request, frame_content_type="image/jpeg")
95+
stream_connections.append(response)
96+
97+
return response
98+
99+
100+
async def send_stream_frames():
101+
while True:
102+
await sleep(0.1)
103+
104+
frame = camera.capture_into_jpeg()
105+
106+
for connection in iter(stream_connections):
107+
try:
108+
connection.send_frame(frame)
109+
except BrokenPipeError:
110+
connection.close()
111+
stream_connections.remove(connection)
112+
113+
114+
async def handle_http_requests():
115+
server.start(str(wifi.radio.ipv4_address))
116+
117+
while True:
118+
await sleep(0)
119+
120+
server.poll()
121+
122+
123+
async def main():
124+
await gather(
125+
create_task(send_stream_frames()),
126+
create_task(handle_http_requests()),
127+
)
128+
129+
130+
run(main())

0 commit comments

Comments
 (0)