Skip to content
This repository was archived by the owner on Jun 7, 2021. It is now read-only.

Commit 8930ea9

Browse files
committed
Added mjpeg stream example
1 parent 3b3d29e commit 8930ea9

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

example/mjpeg-stream.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from __future__ import division
2+
from webthing import (
3+
Action,
4+
Event,
5+
Property,
6+
Value,
7+
Thing,
8+
WebThingServer,
9+
)
10+
import logging
11+
import time
12+
import uuid
13+
import asyncio
14+
15+
import io
16+
from datetime import datetime
17+
from PIL import Image, ImageFont, ImageDraw
18+
19+
import os
20+
import pathlib
21+
22+
"""
23+
PIL spams the logger with debug-level information. This is a pain when debugging api.app.
24+
We override the logging settings in api.app by setting a level for PIL here.
25+
"""
26+
pil_logger = logging.getLogger("PIL")
27+
pil_logger.setLevel(logging.INFO)
28+
29+
30+
class StreamGenerator:
31+
def __init__(self):
32+
self.stream = io.BytesIO()
33+
self.running = False
34+
self.event = asyncio.Event()
35+
36+
def _start_runner(self):
37+
print("Starting frame runner")
38+
task = asyncio.create_task(self.frame_loop())
39+
self.running = True
40+
return task
41+
42+
def generate_new_dummy_image(self):
43+
# Create a dummy image to serve in the stream
44+
image = Image.new(
45+
"RGB",
46+
(640, 480),
47+
color=(0, 0, 0),
48+
)
49+
50+
draw = ImageDraw.Draw(image)
51+
draw.text(
52+
(20, 70),
53+
"Current time: {}".format(
54+
datetime.now().strftime("%d/%m/%Y, %H:%M:%S")
55+
),
56+
)
57+
58+
image.save(self.stream, format="JPEG")
59+
60+
async def frame_loop(self):
61+
while True:
62+
await asyncio.sleep(1) # Only serve frames at 1fps
63+
self.event.clear()
64+
# Reset stream
65+
self.stream.seek(0)
66+
self.stream.truncate()
67+
68+
# Generate new dumm image
69+
self.generate_new_dummy_image()
70+
self.event.set()
71+
72+
async def stream_generator(self):
73+
if not self.running:
74+
self._start_runner()
75+
76+
served_image_timestamp = time.time()
77+
my_boundary = "--boundarydonotcross\n"
78+
while True:
79+
interval = 1.0
80+
if served_image_timestamp + interval < time.time():
81+
# Wait for current frame to finish being generated
82+
await self.event.wait()
83+
# Get the current frame
84+
img = self.stream.getvalue()
85+
# Write the frame data to the Tornado client
86+
served_image_timestamp = time.time()
87+
prefix = (
88+
my_boundary
89+
+ "Content-type: image/jpeg\r\n"
90+
+ "Content-length: %s\r\n\r\n" % len(img)
91+
)
92+
yield prefix.encode() + img
93+
else:
94+
# Delay by interval before checking for next frame
95+
await asyncio.sleep(interval)
96+
97+
98+
def make_thing():
99+
stream_generator = StreamGenerator()
100+
101+
thing = Thing(
102+
"urn:dev:ops:my-lamp-1234",
103+
"My Lamp",
104+
["OnOffSwitch", "Light"],
105+
"A web connected lamp",
106+
)
107+
108+
thing.add_property(
109+
Property(
110+
thing,
111+
"stream",
112+
Value(stream_generator.stream_generator()),
113+
metadata={
114+
"title": "Stream",
115+
"readOnly": True
116+
},
117+
content_type="multipart/x-mixed-replace;boundary=--boundarydonotcross",
118+
)
119+
)
120+
return thing
121+
122+
123+
def run_server():
124+
thing = make_thing()
125+
126+
server = WebThingServer(thing, port=8888, debug=True)
127+
try:
128+
logging.info("starting the server")
129+
server.start()
130+
except KeyboardInterrupt:
131+
logging.info("stopping the server")
132+
server.stop()
133+
logging.info("done")
134+
135+
136+
if __name__ == "__main__":
137+
logging.basicConfig(
138+
level=10, format="%(asctime)s %(filename)s:%(lineno)s %(levelname)s %(message)s"
139+
)
140+
run_server()

0 commit comments

Comments
 (0)