wplayer

Check-in [d98f35b757]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Use sendfile for efficient sending of large media. Implement range: support for player seeking of media. Implement a favicon so we get a pretty picture for our player.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:d98f35b757054e1e1945127cd7bc4b9671ed3bc48cb3ea2a1c57474314bc9e84
User & Date: ajv-899-334-8894@vsta.org 2016-01-29 22:09:38
Context
2016-01-29
22:10
Oh yeah, the actual favicon (cribbed from some other Github player project) check-in: 3d030a8995 user: ajv-899-334-8894@vsta.org tags: master, trunk
22:09
Use sendfile for efficient sending of large media. Implement range: support for player seeking of media. Implement a favicon so we get a pretty picture for our player. check-in: d98f35b757 user: ajv-899-334-8894@vsta.org tags: master, trunk
04:36
Tidy UI a bit check-in: 18c6501d0d user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to README.md.

12
13
14
15
16
17
18



    etc/player.cfg

The program is started simply as:

    python main.py etc/player.cfg










>
>
12
13
14
15
16
17
18
19
20

    etc/player.cfg

The program is started simply as:

    python main.py etc/player.cfg

GET of binary files uses the OS routine sendfile(), which is
in python-sendfile on Debian.

Changes to chore/handlers.py.

1
2
3
4
5
6

7
8
9
10
11
12
13
..
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
..
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
126
127
128
129
130
...
153
154
155
156
157
158
159


160
161
162
163
164
165
166
...
174
175
176
177
178
179
180
181

















































182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201






























202



























203
204
205
206
207
208

209
210
211
212
213
214
215
...
281
282
283
284
285
286
287


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
...
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
...
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
#
# www.py
#	Handling for actual web server bits
#
import base64, socket, sys, json, os
from BaseHTTPServer import BaseHTTPRequestHandler


# Return value for a digit (hex)
def digval(c):
    if c.isdigit():
	return ord(c) - ord('0')
    c = c.lower()
    if (c < 'a') or (c > 'f'):
................................................................................
	self.vals = self.rvals = None

	# This both init's, and runs the web service
	# (BTW, this sucks.  Break out instance creation and
	#  service start--always.)
	BaseHTTPRequestHandler.__init__(self, conn, addr, approot)

    # Log HTTP-level errors
    # (or pass, to quiet them)
    def send_error(self, code, msg):
	print "HTTP error", code, msg

    # Hook to set up SSL
    def setup(self):
	self.connection = self.request
	self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
        self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)

    # Find & dispatch handler
................................................................................
	for tup in self.dispatchers:
	    if tup[0] == op:
		dispatched,res = tup[1](*args)
		if dispatched:
		    return res

	# Nobody could make heads or tails of it
	self.send_error(404, "File not found")
	return None

    # HTTP GET operation
    def do_GET(self):
	sys.stderr.write("GET: %s\n" % (self.path,))

	buf = self.dispatch("GET")
	if buf:
	    self.wfile.write(buf)

    # HTTP HEAD; header of a GET result, no body
    def do_HEAD(self):
	sys.stderr.write("HEAD: %s\n" % (self.path,))

	buf = self.dispatch("GET")

    # A POST; dispatch our various form inputs
    def do_POST(self):
	sys.stderr.write("POST: %s\n" % (self.path,))

	content_len = int(self.headers.getheader('content-length', 0))
	dbuf = self.rfile.read(content_len)
	buf = self.dispatch("POST", dbuf)
	if buf:
	    self.wfile.write(buf)

    # Standard header for web pages
................................................................................
	    return True,"image/jpeg"
	if fn.endswith(".png"):
	    return True,"image/png"
	if fn.endswith(".svg"):
	    return True,"image/svg+xml"
	if fn.endswith(".gif"):
	    return True,"image/gif"


	if fn.endswith(".wav"):
	    return True,"audio/x-wav"
	if fn.endswith(".mp3"):
	    return True,"audio/mpeg"
	if fn.endswith(".flac"):
	    return True,"audio/flac"
	if fn.endswith(".ogg"):
................................................................................
	if fn.endswith(".json"):
	    return False,"text/plain"
	if fn.endswith(".css"):
	    return False,"text/css"

	# Unknown file type
	return None


















































    # Try to serve the named file
    def send_files(self, fn):
	# Hanky-panky?
	if ".." in fn:
	    self.send_error(404, "File not found")
	    return None

	# Make sure we know its MIME type
	tup = self.get_mtype(fn)
	if tup is None:
	    # Unknown file type
	    self.send_error(404, "File not found")
	    return None
	isbin,mtyp = tup

	try:
	    f = open(fn, "r")
	except:
	    self.send_error(404, "File not found")
	    return None






























	buf = f.read()



























	f.close()

	if self.vals and ("range" in str(self.vals.keys()).lower()):
	    import pdb
	    pdb.set_trace()
	buf = self.send_result(buf, mtyp, binary=isbin)

	return buf

    # Common code to strip and decode options
    # Also bursts the path to self.paths[]
    def options(self):
	p = self.path

................................................................................
	self.user = user

	# Ok
	return True

    # Send header
    # Also canonicalize to DOS-style line endings (ew)


    def send_result(self, buf,
	    mtype="text/html", cacheable=False, binary=False,
	    code=None):

	# Rewrite to DOS line endings unless it's binary
	#  (mp3 audio, etc.)
	if not binary:
	    buf = buf.replace("\n", "\r\n")

	# Override for partial response
	if code is None:
	    self.send_response(200)
	else:
	    self.send_response(code)
        self.send_header("Content-type", mtype)
        self.send_header("Content-Length", len(buf))
	if not cacheable:
	    self.send_header("Cache-Control", "NoCache")
	else:
	    self.send_header("Last-Modified",
		time.asctime(time.localtime(self.changed)))
        self.end_headers()
................................................................................
</head>
<body>
%s
</body>
</html>
""" % (self.title, timeout, url, msg)

	buf = self.send_result(buf)
	return buf

    # In <url>[?key[=val][&key[=val...]]], parse key[/val] and
    #  put into self.vals{} and self.rvals{}
    # Returns the input @buf, or None if there was a problem
    def parseKV(self, buf):
	# Walk each key/val, in the format "k[=v]"
................................................................................
	    if len(tup2) == 1:
		k = tup2[0]
		v = True
	    elif len(tup2) == 2:
		k,v = tup2
	    else:
		# x=y=z, something like that?
		self.send_error(404, "File not found")
		return None

	    # Field name should be folded to lower case, as case
	    #  sensitivity varies by browser.
	    k = k.lower()
	    if isinstance(v, bool):
		vals[k] = v




|

>







 







<
<
<
<
<







 







|





>







>





>







 







>
>







 








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




|






|






|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


<
<
<
<
>







 







>
>
|
<
<

|
<
<
|

<
<
|
<
|
|







 







|







 







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
75
76
77
78
79
80
81





82
83
84
85
86
87
88
..
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
126
127
128
129
...
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
...
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311




312
313
314
315
316
317
318
319
...
385
386
387
388
389
390
391
392
393
394


395
396


397
398


399

400
401
402
403
404
405
406
407
408
...
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
...
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
#
# www.py
#	Handling for actual web server bits
#
import base64, socket, sys, json, os, time
from BaseHTTPServer import BaseHTTPRequestHandler
from sendfile import sendfile

# Return value for a digit (hex)
def digval(c):
    if c.isdigit():
	return ord(c) - ord('0')
    c = c.lower()
    if (c < 'a') or (c > 'f'):
................................................................................
	self.vals = self.rvals = None

	# This both init's, and runs the web service
	# (BTW, this sucks.  Break out instance creation and
	#  service start--always.)
	BaseHTTPRequestHandler.__init__(self, conn, addr, approot)






    # Hook to set up SSL
    def setup(self):
	self.connection = self.request
	self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
        self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)

    # Find & dispatch handler
................................................................................
	for tup in self.dispatchers:
	    if tup[0] == op:
		dispatched,res = tup[1](*args)
		if dispatched:
		    return res

	# Nobody could make heads or tails of it
	self.send_error(404)
	return None

    # HTTP GET operation
    def do_GET(self):
	sys.stderr.write("GET: %s\n" % (self.path,))
	self.base_op = "GET"
	buf = self.dispatch("GET")
	if buf:
	    self.wfile.write(buf)

    # HTTP HEAD; header of a GET result, no body
    def do_HEAD(self):
	sys.stderr.write("HEAD: %s\n" % (self.path,))
	self.base_op = "HEAD"
	buf = self.dispatch("GET")

    # A POST; dispatch our various form inputs
    def do_POST(self):
	sys.stderr.write("POST: %s\n" % (self.path,))
	self.base_op = "POST"
	content_len = int(self.headers.getheader('content-length', 0))
	dbuf = self.rfile.read(content_len)
	buf = self.dispatch("POST", dbuf)
	if buf:
	    self.wfile.write(buf)

    # Standard header for web pages
................................................................................
	    return True,"image/jpeg"
	if fn.endswith(".png"):
	    return True,"image/png"
	if fn.endswith(".svg"):
	    return True,"image/svg+xml"
	if fn.endswith(".gif"):
	    return True,"image/gif"
	if fn.endswith(".ico"):
	    return True,"image/vnd.microsoft.icon"
	if fn.endswith(".wav"):
	    return True,"audio/x-wav"
	if fn.endswith(".mp3"):
	    return True,"audio/mpeg"
	if fn.endswith(".flac"):
	    return True,"audio/flac"
	if fn.endswith(".ogg"):
................................................................................
	if fn.endswith(".json"):
	    return False,"text/plain"
	if fn.endswith(".css"):
	    return False,"text/css"

	# Unknown file type
	return None

    # Decode a "range:" header option, return
    #  (offset,length) or None if we don't like
    #  the region (TBD, multiple ranges and
    #  multipart)
    # We're passed the file's os.stat as well as
    #  the range: field value.
    def decode_range(self, st, range):
	# Byte units, please
	if not range.startswith("bytes="):
	    return None
	range = range[6:]

	# Single range
	if ',' in range:
	    return None

	# Start to offset
	if range[0] == '-':
	    range = range[1:]
	    if not range.isdigit():
		return None
	    val1 = int(range)
	    if val1 >= st.st_size:
		return None
	    return (0, val1)

	# Offset to end...
	elif range[-1] == '-':
	    range = range[:-1]
	    if not range.isdigit():
		return None
	    val2 = int(range)
	    if val2 >= st.st_size:
		return None
	    return (val2, st.st_size - val2)

	# Offset1 to offset2
	else:
	    parts = range.split('-')
	    if len(parts) != 2:
		return None
	    if not all(p.isdigit() for p in parts):
		return None
	    val1 = int(parts[0])
	    val2 = int(parts[1])
	    if val1 >= val2:
		return None
	    return (val1, val2-val1)

    # Try to serve the named file
    def send_files(self, fn):
	# Hanky-panky?
	if ".." in fn:
	    self.send_error(404)
	    return None

	# Make sure we know its MIME type
	tup = self.get_mtype(fn)
	if tup is None:
	    # Unknown file type
	    self.send_error(404)
	    return None
	isbin,mtyp = tup

	try:
	    f = open(fn, "r")
	except:
	    self.send_error(404)
	    return None

	# Get dope on file overall
	st = os.fstat(f.fileno())
	startoff = 0
	nbyte = st.st_size
	ranged = False

	# Sub-ranged output
	if 'range' in self.headers:
	    tup = self.decode_range(st, self.headers['range'])
	    if tup is None:
		# Bad range
		self.send_error(416)
		return None
	    ranged = True
	    startoff,nbyte = tup
	    print "range:", startoff, "for", nbyte
	else:
	    startoff = 0
	    nbyte = st.st_size

	# For media files, use sendfile() rather than passing
	#  it all through this process.
	# We also support ranges here.
	if isbin:

	    # Ranged or normal response
	    if ranged:
		self.send_response(206)
	    else:
		buf = f.read()
		self.send_response(200)
	    self.send_header("Content-type", mtyp)
	    self.send_header("Content-Length", nbyte)
	    if ranged:
		self.send_header("Content-Range",
		"bytes %d-%d/%d" % (startoff, startoff+nbyte-1, nbyte))
	    self.send_header("Last-Modified",
		time.asctime(time.localtime(st.st_mtime)))
	    self.end_headers()

	    # Don't push out body if they're just asking us about
	    #  the file's size via HEAD
	    if self.base_op == "GET":
		sendfile(self.wfile.fileno(), f.fileno(), startoff, nbyte)

	    # We've pushed it, tell the upper layers
	    buf = None

	# Text, just shuffle bytes around as a whole
	# TBD are gigabyte text files... an encyclopedia, anybody?
	# But don't forget the DOS-style line endings; can't just
	#  use sendfile() if you honor that.
	else:
	    buf = f.read()
	    buf = self.send_result(buf, mtyp)

	# Done with the file
	f.close()





	# Return contents (or None if we already pushed it out)
	return buf

    # Common code to strip and decode options
    # Also bursts the path to self.paths[]
    def options(self):
	p = self.path

................................................................................
	self.user = user

	# Ok
	return True

    # Send header
    # Also canonicalize to DOS-style line endings (ew)
    # This code only handles textual responses; binary/large
    #  media is handled inline.
    def send_result(self, buf, mtyp, cacheable=False):



	# Rewrite to DOS line endings


	buf = buf.replace("\n", "\r\n")



	# Send response

	self.send_response(200)
        self.send_header("Content-type", mtyp)
        self.send_header("Content-Length", len(buf))
	if not cacheable:
	    self.send_header("Cache-Control", "NoCache")
	else:
	    self.send_header("Last-Modified",
		time.asctime(time.localtime(self.changed)))
        self.end_headers()
................................................................................
</head>
<body>
%s
</body>
</html>
""" % (self.title, timeout, url, msg)

	buf = self.send_result(buf, "text/html")
	return buf

    # In <url>[?key[=val][&key[=val...]]], parse key[/val] and
    #  put into self.vals{} and self.rvals{}
    # Returns the input @buf, or None if there was a problem
    def parseKV(self, buf):
	# Walk each key/val, in the format "k[=v]"
................................................................................
	    if len(tup2) == 1:
		k = tup2[0]
		v = True
	    elif len(tup2) == 2:
		k,v = tup2
	    else:
		# x=y=z, something like that?
		self.send_error(404)
		return None

	    # Field name should be folded to lower case, as case
	    #  sensitivity varies by browser.
	    k = k.lower()
	    if isinstance(v, bool):
		vals[k] = v

Changes to get.py.

50
51
52
53
54
55
56






57
58
59
60
61
62
63

    # GET dispatcher
    # See if it's a configured path, and serve file or dir
    def send_path(self):
	app = self.server
	cpaths = app.config["files"]
	pp = [urllib.unquote(p) for p in self.paths]







	# We serve /media/prefix/blah/blah...
	if pp[0] != "media":
	    return False,None
	del pp[0]

	# Root; list configured prefixes themselves







>
>
>
>
>
>







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

    # GET dispatcher
    # See if it's a configured path, and serve file or dir
    def send_path(self):
	app = self.server
	cpaths = app.config["files"]
	pp = [urllib.unquote(p) for p in self.paths]

	# Special case, bleh
	if (len(pp) == 1) and (pp[0] == "favicon.ico"):
	    print "GET favicon.ico"
	    buf = self.send_files("imgs/favicon.ico")
	    return True,buf

	# We serve /media/prefix/blah/blah...
	if pp[0] != "media":
	    return False,None
	del pp[0]

	# Root; list configured prefixes themselves

Changes to post.py.

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
	# Burst from JSON
	msg = json.loads(buf)
	towhom = toascii(msg.get("recipient", ""))
	body = toascii(msg.get("body", ""))

	# Malformed?
	if (not towhom) or (not body):
	    self.send_error(404, "Malformed request")
	    return None

	# Recipient known?
	user = self.user
	acct = user.roster.get(towhom)
	if acct is None:
	    self.send_error(404, "Unknown recipient")
	    return None

	# Send message
	with user.exclusion:
	    print "Send msg", towhom, body
	    acct.send(towhom, body)

................................................................................
	pp = p.strip("/").split("/")

	# Operation on root; Form submit, so probably an old
	#  non-JS browser sending a message.
	if (len(pp) == 1) and (not pp[0]):
	    print "TBD: POST to root"
	    # return self.post_root(buf)
	    self.send_error(404, "File not found")
	    return None

	# JS submission of text to post
	if (len(pp) == 1) and (pp[0] == "msg.json"):
	    return self.post_msg(buf)

	# Unknown submission type
	self.send_error(404, "File not found")
	return None







|






|







 







|







|

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
	# Burst from JSON
	msg = json.loads(buf)
	towhom = toascii(msg.get("recipient", ""))
	body = toascii(msg.get("body", ""))

	# Malformed?
	if (not towhom) or (not body):
	    self.send_error(404)
	    return None

	# Recipient known?
	user = self.user
	acct = user.roster.get(towhom)
	if acct is None:
	    self.send_error(404)
	    return None

	# Send message
	with user.exclusion:
	    print "Send msg", towhom, body
	    acct.send(towhom, body)

................................................................................
	pp = p.strip("/").split("/")

	# Operation on root; Form submit, so probably an old
	#  non-JS browser sending a message.
	if (len(pp) == 1) and (not pp[0]):
	    print "TBD: POST to root"
	    # return self.post_root(buf)
	    self.send_error(404)
	    return None

	# JS submission of text to post
	if (len(pp) == 1) and (pp[0] == "msg.json"):
	    return self.post_msg(buf)

	# Unknown submission type
	self.send_error(404)
	return None