aboutsummaryrefslogtreecommitdiff
path: root/host/gifstream.py
diff options
context:
space:
mode:
Diffstat (limited to 'host/gifstream.py')
-rwxr-xr-xhost/gifstream.py111
1 files changed, 111 insertions, 0 deletions
diff --git a/host/gifstream.py b/host/gifstream.py
new file mode 100755
index 0000000..a580ae1
--- /dev/null
+++ b/host/gifstream.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+# This script eats CRAP and outputs a live GIF-over-HTTP stream. The required bandwidth is about 20kB/s
+
+import io
+import math
+import threading
+import struct
+import argparse
+from itertools import chain
+from contextlib import suppress
+
+from PIL import Image, ImageFile
+from flask import Flask, Response
+
+import config
+
+import crap
+
+
+frame = None
+frame_cond = threading.Condition()
+
+def framestream():
+ global frame
+ while True:
+ with frame_cond:
+ frame_cond.wait()
+ img = Image.frombuffer('RGB', (config.display_width, config.display_height), frame, 'raw', 'RGB', 0, 1)
+ yield encode_image(img)
+
+def putframe(f):
+ global frame
+ with frame_cond:
+ frame = f
+ frame_cond.notify_all()
+
+def file_headers(w, h):
+ yield (b'GIF89a'
+ + struct.pack('<H', w) # image width
+ + struct.pack('<H', h) # image height
+ + b'\x70' # flags bits 2-4: color resolution = 7
+ + b'\x00' # background color index
+ + b'\x00' # pixel aspect ratio (1:1)
+ # Netscape headers
+ + b'\x21' # extension block
+ + b'\xff' # app extension
+ + b'\x0b' # block size
+ + b'NETSCAPE2.0' # app name
+ + b'\x03' # sub-block size
+ + b'\x01' # loop sub-block id
+ + b'\x01\x00' # repeat count (0 == Infinity)
+ + b'\x00' # block terminator
+ )
+
+def encode_image(img):
+ img = img.convert('P', palette=Image.ADAPTIVE, colors=256)
+ palbytes = img.palette.palette
+# assert len(img.palette) in (2, 4, 8, 0x10, 0x20, 0x40, 0x80, 0x100)
+ imio = io.BytesIO()
+ ImageFile._save(img, imio, [("gif", (0, 0)+img.size, 0, 'P')])
+ return (
+ # GCE
+ b'\x21' # extension block
+ + b'\xf9' # graphic control extension
+ + b'\x04' # block size
+ + b'\x00' # flags
+ + b'\x00\x00' # frame delay
+ + b'\x00' # transparent color index
+ + b'\x00' # block terminator
+ # Image headers
+ + b'\x2c' # image block
+ + b'\x00\x00' # image x
+ + b'\x00\x00' # image y
+ + struct.pack('<H', img.width) # image width
+ + struct.pack('<H', img.height) # image height
+ + bytes((0x80 | int(math.log(len(palbytes)//3, 2.0))-1,)) # flags (local palette and palette size)
+ # Palette
+ + palbytes
+ # LZW code size
+ + b'\x08'
+ # Image data. We're using pillow here because I was too lazy finding a suitable LZW encoder.
+ + imio.getbuffer()
+ # End of image data
+ + b'\x00'
+ )
+
+
+app = Flask(__name__)
+#app.debug = True
+
+@app.route('/')
+def hello_world():
+ return Response(chain(file_headers(config.display_width, config.display_height), framestream()), mimetype='image/gif')
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('addr', default='127.0.0.1', nargs='?')
+ parser.add_argument('port', type=int, default=1337, nargs='?')
+ args = parser.parse_args()
+
+ udp_server = crap.CRAPServer(args.addr, args.port, blocking=True, log=lambda *_a: None)
+
+ def receiver():
+ for _title, frame in udp_server:
+ putframe(frame)
+ frame_runner = threading.Thread(target=receiver, daemon=True)
+ frame_runner.start()
+
+ app.run()
+ udp_server.close()