Skip to content

Commit 1bb84f9

Browse files
committed
Merge branch 'main' into https-implementation
2 parents ff165dd + be94572 commit 1bb84f9

File tree

4 files changed

+148
-4
lines changed

4 files changed

+148
-4
lines changed

docs/conf.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@
116116
import sphinx_rtd_theme
117117

118118
html_theme = "sphinx_rtd_theme"
119-
html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."]
120119

121120
# Add any paths that contain custom static files (such as style sheets) here,
122121
# relative to this directory. They are copied after the builtin static files,

docs/examples.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,23 @@ but it is recommended as it makes it easier to handle multiple tasks. It can be
355355
:emphasize-lines: 12,20,65-72,88,99
356356
:linenos:
357357

358+
Custom response types e.g. video streaming
359+
------------------------------------------
360+
361+
The built-in response types may not always meet your specific requirements. In such cases, you can define custom response types and implement
362+
the necessary logic.
363+
364+
The example below demonstrates a ``XMixedReplaceResponse`` class, which uses the ``multipart/x-mixed-replace`` content type to stream video frames
365+
from a camera, similar to a CCTV system.
366+
367+
To ensure the server remains responsive, a global list of open connections is maintained. By running tasks asynchronously, the server can stream
368+
video to multiple clients while simultaneously handling other requests.
369+
370+
.. literalinclude:: ../examples/httpserver_video_stream.py
371+
:caption: examples/httpserver_video_stream.py
372+
:emphasize-lines: 31-77,92
373+
:linenos:
374+
358375
SSL/TLS (HTTPS)
359376
---------------
360377

examples/httpserver_multiple_servers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ def home(request: Request):
4242
return Response(request, "Hello from home!")
4343

4444

45-
id_address = str(wifi.radio.ipv4_address)
45+
ip_address = str(wifi.radio.ipv4_address)
4646

4747
# Start the servers.
48-
bedroom_server.start(id_address, 5000)
49-
office_server.start(id_address, 8000)
48+
bedroom_server.start(ip_address, 5000)
49+
office_server.start(ip_address, 8000)
5050

5151
while True:
5252
try:

examples/httpserver_video_stream.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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

Comments
 (0)