|
| 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