aboutsummaryrefslogtreecommitdiff
path: root/host/gifstream.py
blob: 9905d34ddd3cbc47a523868be49441230dc06255 (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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/usr/bin/env python3
# This script eats CRAP and outputs a live GIF-over-HTTP stream. The required bandwidth is about 20kB/s
# Copyright (C) 2016 Sebastian Götte <code@jaseg.net>
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

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