Stream responses (Fixes #44)
This commit is contained in:
BIN
examples/1.jpg
Normal file
BIN
examples/1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
examples/2.jpg
Normal file
BIN
examples/2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
examples/3.jpg
Normal file
BIN
examples/3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
45
examples/video_stream.py
Normal file
45
examples/video_stream.py
Normal file
@@ -0,0 +1,45 @@
|
||||
try:
|
||||
import utime as time
|
||||
except ImportError:
|
||||
import time
|
||||
|
||||
from microdot import Microdot
|
||||
|
||||
app = Microdot()
|
||||
|
||||
frames = []
|
||||
for file in ['1.jpg', '2.jpg', '3.jpg']:
|
||||
with open(file, 'rb') as f:
|
||||
frames.append(f.read())
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index(request):
|
||||
return '''<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Microdot Video Streaming</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Microdot Video Streaming</h1>
|
||||
<img src="/video_feed">
|
||||
</body>
|
||||
</html>''', 200, {'Content-Type': 'text/html'}
|
||||
|
||||
|
||||
@app.route('/video_feed')
|
||||
def video_feed(request):
|
||||
def stream():
|
||||
yield b'--frame\r\n'
|
||||
while True:
|
||||
for frame in frames:
|
||||
yield b'Content-Type: image/jpeg\r\n\r\n' + frame + \
|
||||
b'\r\n--frame\r\n'
|
||||
time.sleep(1)
|
||||
|
||||
return stream(), 200, {'Content-Type':
|
||||
'multipart/x-mixed-replace; boundary=frame'}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
64
examples/video_stream_async.py
Normal file
64
examples/video_stream_async.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import sys
|
||||
|
||||
try:
|
||||
import uasyncio as asyncio
|
||||
except ImportError:
|
||||
import asyncio
|
||||
|
||||
from microdot_asyncio import Microdot
|
||||
|
||||
app = Microdot()
|
||||
|
||||
frames = []
|
||||
for file in ['1.jpg', '2.jpg', '3.jpg']:
|
||||
with open(file, 'rb') as f:
|
||||
frames.append(f.read())
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index(request):
|
||||
return '''<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Microdot Video Streaming</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Microdot Video Streaming</h1>
|
||||
<img src="/video_feed">
|
||||
</body>
|
||||
</html>''', 200, {'Content-Type': 'text/html'}
|
||||
|
||||
|
||||
@app.route('/video_feed')
|
||||
async def video_feed(request):
|
||||
if sys.implementation.name != 'micropython':
|
||||
# CPython supports yielding async generators
|
||||
async def stream():
|
||||
yield b'--frame\r\n'
|
||||
while True:
|
||||
for frame in frames:
|
||||
yield b'Content-Type: image/jpeg\r\n\r\n' + frame + \
|
||||
b'\r\n--frame\r\n'
|
||||
await asyncio.sleep(1)
|
||||
|
||||
else:
|
||||
# MicroPython can only use class-based async generators
|
||||
class stream():
|
||||
def __init__(self):
|
||||
self.i = 0
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
await asyncio.sleep(1)
|
||||
self.i = (self.i + 1) % len(frames)
|
||||
return b'Content-Type: image/jpeg\r\n\r\n' + \
|
||||
frames[self.i] + b'\r\n--frame\r\n'
|
||||
|
||||
return stream(), 200, {'Content-Type':
|
||||
'multipart/x-mixed-replace; boundary=frame'}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
@@ -385,7 +385,7 @@ class Response():
|
||||
elif isinstance(body, str):
|
||||
self.body = body.encode()
|
||||
else:
|
||||
# this applies to bytes or file-like objects
|
||||
# this applies to bytes, file-like objects or generators
|
||||
self.body = body
|
||||
|
||||
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
|
||||
@@ -445,8 +445,19 @@ class Response():
|
||||
stream.write(b'\r\n')
|
||||
|
||||
# body
|
||||
for body in self.body_iter():
|
||||
stream.write(body)
|
||||
can_flush = hasattr(stream, 'flush')
|
||||
try:
|
||||
for body in self.body_iter():
|
||||
if isinstance(body, str):
|
||||
body = body.encode()
|
||||
stream.write(body)
|
||||
if can_flush: # pragma: no cover
|
||||
stream.flush()
|
||||
except OSError as exc: # pragma: no cover
|
||||
if exc.errno == 32: # errno.EPIPE
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def body_iter(self):
|
||||
if self.body:
|
||||
@@ -459,6 +470,8 @@ class Response():
|
||||
break
|
||||
if hasattr(self.body, 'close'): # pragma: no cover
|
||||
self.body.close()
|
||||
elif hasattr(self.body, '__next__'):
|
||||
yield from self.body
|
||||
else:
|
||||
yield self.body
|
||||
|
||||
@@ -803,7 +816,7 @@ class Microdot():
|
||||
try:
|
||||
sock, addr = self.server.accept()
|
||||
except OSError as exc: # pragma: no cover
|
||||
if exc.args[0] == errno.ECONNABORTED:
|
||||
if exc.errno == errno.ECONNABORTED:
|
||||
break
|
||||
else:
|
||||
raise
|
||||
@@ -847,7 +860,13 @@ class Microdot():
|
||||
print_exception(exc)
|
||||
res = self.dispatch_request(req)
|
||||
res.write(stream)
|
||||
stream.close()
|
||||
try:
|
||||
stream.close()
|
||||
except OSError as exc: # pragma: no cover
|
||||
if exc.errno == 32: # errno.EPIPE
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
if stream != sock: # pragma: no cover
|
||||
sock.close()
|
||||
if self.shutdown_requested: # pragma: no cover
|
||||
|
||||
@@ -133,10 +133,21 @@ class Response(BaseResponse):
|
||||
await stream.awrite(b'\r\n')
|
||||
|
||||
# body
|
||||
async for body in self.body_iter():
|
||||
await stream.awrite(body)
|
||||
try:
|
||||
async for body in self.body_iter():
|
||||
if isinstance(body, str):
|
||||
body = body.encode()
|
||||
await stream.awrite(body)
|
||||
except OSError as exc: # pragma: no cover
|
||||
if exc.errno == 32 or exc.args[0] == 'Connection lost':
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def body_iter(self):
|
||||
if hasattr(self.body, '__anext__'):
|
||||
return self.body
|
||||
|
||||
response = self
|
||||
|
||||
class iter:
|
||||
@@ -284,7 +295,13 @@ class Microdot(BaseMicrodot):
|
||||
|
||||
res = await self.dispatch_request(req)
|
||||
await res.write(writer)
|
||||
await writer.aclose()
|
||||
try:
|
||||
await writer.aclose()
|
||||
except OSError as exc: # pragma: no cover
|
||||
if exc.errno == 32: # errno.EPIPE
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
if self.debug and req: # pragma: no cover
|
||||
print('{method} {path} {status_code}'.format(
|
||||
method=req.method, path=req.path,
|
||||
|
||||
@@ -286,3 +286,21 @@ class TestMicrodot(unittest.TestCase):
|
||||
self.assertIn(b'Content-Length: 3\r\n', fd.response)
|
||||
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||
self.assertTrue(fd.response.endswith(b'\r\n\r\n501'))
|
||||
|
||||
def test_streaming(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
def stream():
|
||||
yield 'foo'
|
||||
yield b'bar'
|
||||
return stream()
|
||||
|
||||
mock_socket.clear_requests()
|
||||
fd = mock_socket.add_request('GET', '/')
|
||||
self._add_shutdown(app)
|
||||
app.run()
|
||||
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
|
||||
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoobar'))
|
||||
|
||||
@@ -264,3 +264,33 @@ class TestMicrodotAsync(unittest.TestCase):
|
||||
self.assertIn(b'Content-Length: 3\r\n', fd.response)
|
||||
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||
self.assertTrue(fd.response.endswith(b'\r\n\r\n501'))
|
||||
|
||||
def test_streaming(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
class stream():
|
||||
def __init__(self):
|
||||
self.i = 0
|
||||
self.data = ['foo', b'bar']
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
if self.i >= len(self.data):
|
||||
raise StopAsyncIteration
|
||||
data = self.data[self.i]
|
||||
self.i += 1
|
||||
return data
|
||||
|
||||
return stream()
|
||||
|
||||
mock_socket.clear_requests()
|
||||
fd = mock_socket.add_request('GET', '/')
|
||||
self._add_shutdown(app)
|
||||
app.run()
|
||||
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
|
||||
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoobar'))
|
||||
|
||||
Reference in New Issue
Block a user