-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add support for "multipart/x-mixed-replace" (for streaming video) #95
Comments
Hi, could you share some of your code so I can see how do you want to use it? Maybe using a I may be wrong, but I believe that even if you goal is possible at all, it likely will require specific hardware with enough memory. |
Each frame is a few Kb, plus I was streaming video using Micropython before, (Note: I am migrating to CircuitPython because the Micropython platform is a total mess). So it should be possible, all we need is the functionality in the server. I don't have any decent code to share yet because I am still learning so probably I am making lots of shameful mistakes :) |
I will try to make a simple video streaming example using on disk images, but I would really appreciate some, even work in progress code. I curently do not have any camera that I can connect to a microcontroller so I want to understand the workflow of capturing the frames from camera that later could be adapted to a "x-mixed-replace" response. |
In CircuitPython I tried using the following code: BOUNDARY = "FRAME"
@server.route("/")
def base(request):
response = Response(request)
response._send_headers(content_type='multipart/x-mixed-replace; boundary=%s' % BOUNDARY)
for i in range(10):
jpeg = b"cacacacacacacacacaca" #cam.take()
response._send_bytes(request.connection, b'--%s\r\n' % BOUNDARY)
response._send_bytes(request.connection, b'Content-Type: plain/text\r\nContent-Length: %d\r\n\r\n' % len(jpeg))
response._send_bytes(request.connection, jpeg)
response._send_bytes(request.connection, b'\r\n')
return response As you can see I am only streaming here some text And with MicroPython I used https://github.com/wybiral/micropython-aioweb with this code: app.route('/vid')
sync def vid_handler(r, w):
PART_BOUNDARY = "123456789000000000000987654321"
STREAM_CONTENT_TYPE = "Content-Type: multipart/x-mixed-replace;boundary=" + PART_BOUNDARY + "\r\n"
STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
STREAM_BOUNDARY = "\r\n--" + PART_BOUNDARY + "\r\n";
w.write(b'HTTP/1.0 200 OK\r\n')
w.write(STREAM_CONTENT_TYPE)
while True:
w.write(STREAM_BOUNDARY)
f = camera.capture()
w.write(STREAM_PART % len(f))
w.write(f)
await w.drain() |
Thanks, I will try making it work and will come back to you with the results. |
I managed to make a working example: https://github.com/michalpokusa/Adafruit_CircuitPython_HTTPServer/blob/x-mixed-replace-example/x_mixed_replace_example.py Make sure to also download the "frames" folder, or change the code to work with camera from the start. I am not sure whether it should be a feature in lib itself - it is already very big. Maybe I will make a PR based on example above as your usage seems like it might be common. Please try the code above, I will be happy to help with any problems you encounter. |
Thanks!!! I'll check it out later today. |
This is awesome, and it works like a charm :) I have some minor comments: I tried to combine the below two lines into 1 self._send_bytes(
self._request.connection,
bytes(f"{self._boundary}\r\n", "utf-8")
)
self._send_bytes(
self._request.connection,
bytes(f"Content-Type: {self._frame_content_type}\r\n\r\n", "utf-8"),
) but for some reason that was caused an unrelated error:
Also, I'd be great to minimize copies by moving the b"\r\n" to the above _send_bytes (only the second time this gets called) self._send_bytes(self._request.connection, bytes(encoded_frame) + b"\r\n") (note that the last b"\r\n" is not also, this bit is not needed:
Do you think the suggested changes would help? Otherwise this is great and gets a decent frame rate (140Kb/s) |
173K/s by batching the b"\r\n" like this: if self.frame_idx > 0:
self._send_bytes(
self._request.connection,
bytes(f"\r\n{self._boundary}\r\n", "utf-8")
)
else:
self._send_bytes(
self._request.connection,
bytes(f"{self._boundary}\r\n", "utf-8")
)
self.frame_idx +=1 For some reason merging the two _send_bytes doesn't work |
self._send_bytes(self._request.connection, bytes(encoded_frame))
self._send_bytes(self._request.connection, bytes("\r\n", "utf-8")) It makes the code clear and does not really impact performance. I could improve that example, but it was mainly a quickly written proof-of-concept.
The headers parser uses splitline and then splits each line to get header name and value, I suspect it tried to split empty line between "\r\n" and "\r\n", which resulted in ValueError, the question is why did the request containe doubled newline at the end. I might add check for that to the parser in the PR, thanks for spotting that. When you finish your project, please share a link if you can, I always like to see new ways how people use the adafruit_httpserver. 👍 |
I do not have experience with MicroPython, but I agree that CircuitPython is very good and easy to use for most part. It might be worth to present your setup on Show and Tell that Adafruit is hosting on Wednesday, this way more people can see what you created. When it comes to knowing when connection is closed, I encourage you to check out an Websocket example from docs, it stores the connection in global scope, and uses async to handle requests between sending next frames. It might take some work to implement this behaviour, but it is not very hard, examples show most of the functionality. |
I looked at the websocked example but I couldn't figure out how to detect when the connection closes, what did you have in mind? |
What I meant is that when the client disconnects, futher sent frames should result in I will have some time during the weekend, so I will try to make a sync await example that detects the closed connection. It is kind of necessary as without it you would be stuck in sending the one response, and the MCU wouldn't be able to do anything until client disconnects. I think I should have a working example by Monday. |
I am nearly done, I managed to make a live video feed to multiple devices at the same time and detecting when the connection is dropped, this seems like a perfect example on how to create custom response types. I will make a PR later with the elegant code. For now a little preview: Recording.2024-06-29.182853.mp4 |
Wow that is pretty cool! And there is no lag, are you using a esp32? |
I used ESP32-S2 TFT for this. The lag probably will be more noticeable with bigger frames. I suspect that under the hood the transfer is optimized somehow, as it seems to good to be true... Recording.2024-06-29.191759.mp4 |
Hi Michal, it's been a while :) Any chance to make this part of your great lib? |
To be honest, it completely slip out of my mind. Thanks for reminding me. The code I have is pretty much ready, I will add docs for the example and make a PR in a day or two. This time, the sticky note is on my desk, so hopefully I will not forget. 😅 |
Many thanks :) |
I presume this has already been considered. If so, I'd be great to document why it has been implemented yes (technical limitations, memory,..) that way if others decide to implement it, can use this information to make better decisions.
Thanks for your awesome work.
The text was updated successfully, but these errors were encountered: