diff options
Diffstat (limited to 'host/gifstream.py')
-rwxr-xr-x | host/gifstream.py | 111 |
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() |