├── .gitignore ├── server.py └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | videos/ -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import print_function 3 | 4 | from tornado.wsgi import WSGIContainer 5 | from tornado.httpserver import HTTPServer 6 | from tornado.ioloop import IOLoop 7 | 8 | import logging 9 | import os 10 | import re 11 | import sys 12 | import time 13 | import pprint 14 | from datetime import datetime 15 | 16 | import mimetypes 17 | from flask import Response, render_template 18 | from flask import Flask 19 | from flask import send_file 20 | from flask import request 21 | 22 | LOG = logging.getLogger(__name__) 23 | app = Flask(__name__) 24 | 25 | VIDEO_PATH = '/video' 26 | 27 | MB = 1 << 20 28 | BUFF_SIZE = 10 * MB 29 | 30 | @app.route('/') 31 | def home(): 32 | LOG.info('Rendering home page') 33 | response = render_template( 34 | 'index.html', 35 | time=str(datetime.now()), 36 | video=VIDEO_PATH, 37 | ) 38 | return response 39 | 40 | def partial_response(path, start, end=None): 41 | LOG.info('Requested: %s, %s', start, end) 42 | file_size = os.path.getsize(path) 43 | 44 | # Determine (end, length) 45 | if end is None: 46 | end = start + BUFF_SIZE - 1 47 | end = min(end, file_size - 1) 48 | end = min(end, start + BUFF_SIZE - 1) 49 | length = end - start + 1 50 | 51 | # Read file 52 | with open(path, 'rb') as fd: 53 | fd.seek(start) 54 | bytes = fd.read(length) 55 | assert len(bytes) == length 56 | 57 | response = Response( 58 | bytes, 59 | 206, 60 | mimetype=mimetypes.guess_type(path)[0], 61 | direct_passthrough=True, 62 | ) 63 | response.headers.add( 64 | 'Content-Range', 'bytes {0}-{1}/{2}'.format( 65 | start, end, file_size, 66 | ), 67 | ) 68 | response.headers.add( 69 | 'Accept-Ranges', 'bytes' 70 | ) 71 | LOG.info('Response: %s', response) 72 | LOG.info('Response: %s', response.headers) 73 | return response 74 | 75 | def get_range(request): 76 | range = request.headers.get('Range') 77 | LOG.info('Requested: %s', range) 78 | m = re.match('bytes=(?P\d+)-(?P\d+)?', range) 79 | if m: 80 | start = m.group('start') 81 | end = m.group('end') 82 | start = int(start) 83 | if end is not None: 84 | end = int(end) 85 | return start, end 86 | else: 87 | return 0, None 88 | 89 | @app.route(VIDEO_PATH) 90 | def video(): 91 | path = 'videos/movie.mp4' 92 | # path = 'demo.mp4' 93 | 94 | start, end = get_range(request) 95 | return partial_response(path, start, end) 96 | 97 | if __name__ == '__main__': 98 | logging.basicConfig(level=logging.INFO) 99 | HOST = '0.0.0.0' 100 | http_server = HTTPServer(WSGIContainer(app)) 101 | http_server.listen(8080) 102 | IOLoop.instance().start() 103 | 104 | # Standalone 105 | # app.run(host=HOST, port=8080, debug=True) 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Home page! Time: {{ time }} 4 |

5 | 12 | 13 | --------------------------------------------------------------------------------