aboutsummaryrefslogtreecommitdiff
path: root/host/gifstream.py
blob: a580ae18256abda070395f3d7e3638928332f5a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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()