HTTP 206 (Partial Content) For Flask/Python
Serving media files with HTTP 200 seems to make browsers act a bit strangely. Chrome refuses to fire onended events, and neither Chrome nor Firefox seems happy seeking through them. I am Not entirely sure for the reasons for this; I guess that they try to play the media before receiving all of it and can't guarantee they'll have downloaded the right chunk for seeking, while having no way of requesting it specifically. Who knows.
Here's a simple replacement to Flask's send_file that supports 206 partial content via byte ranges (and a function that adds the appropriate headers to all responses). It doesn't fully wrap send_file, if you care about all the other arguments and mirroring the exact behaviour, it needs a bit more work, but should suffice for 99% of uses.
[update 31/8: sorry there was a bug in this which caused strange things with certain Chrome versions, it's now been fixed]
import mimetypes import os import re from flask import request, send_file, Response @app.after_request def after_request(response): response.headers.add('Accept-Ranges', 'bytes') return response def send_file_partial(path): """ Simple wrapper around send_file which handles HTTP 206 Partial Content (byte ranges) TODO: handle all send_file args, mirror send_file's error handling (if it has any) """ range_header = request.headers.get('Range', None) if not range_header: return send_file(path) size = os.path.getsize(path) byte1, byte2 = 0, None m = re.search('(\d+)-(\d*)', range_header) g = m.groups() if g[0]: byte1 = int(g[0]) if g[1]: byte2 = int(g[1]) length = size - byte1 if byte2 is not None: length = byte2 - byte1 data = None with open(path, 'rb') as f: f.seek(byte1) data = f.read(length) rv = Response(data, 206, mimetype=mimetypes.guess_type(path)[0], direct_passthrough=True) rv.headers.add('Content-Range', 'bytes {0}-{1}/{2}'.format(byte1, byte1 + length - 1, size)) return rv
Talk is cheap
– 15:09:51 31st December 2013
– 21:17:33 1st January 2014
1) you probably shouldn't be calling open(filename) since, depending on how it gets integrated, this may be an absolute path into the filesystem. flask does this instead in send_file:
if filename is not None:
if not os.path.isabs(filename):
filename = os.path.join(flask.globals.current_app.root_path, filename)
2) the requested range is interpreted off by 1, which works in most cases but breaks html5 video delivery on safari mobile (which requires range requests to work). Range: bytes=0-1 is actually a request for 2 bytes, but your code will interpret it as a request for just 1. so line 34 should read:
length = byte2 - byte1 + 1
thanks
– 19:19:55 22nd January 2014
It's been a while since I needed this and I don't currently have an environment set up to test any modifications, but thanks for the corrections. Would you post your version in full?
– 11:32:41 24th January 2014
– 19:56:49 29th December 2014
No, not quite. Assuming that you are using Flask to serve up the video file, include this snippet, then replace your call to send_file with send_file_partial. If you are currently serving the file statically (i.e the video is stored on the filesystem and the video's src attribute points directly to it, you will need to alter things around such that the video file is served via a route you define in Flask (and use send_file_partial).
– 22:29:19 29th December 2014
mark
:
Hi Mark,
Thanks for this snippet!
I'm in the same case as Alex (comment above): I just have an html5 audio player in the html with src pointing to my .mp3 file in my static folder.
I get that to use this snippet I should first use send_file (or send_from_directory) to send the .mp3 to the client, then add you snippet to my python code to allow partial requests, but I can't figure out how to use send_from_directory (or send_file).
There are some stuff online, but they all seem to be for applications where the user uploads a file..
Is there a simple way to do this?
thanks.
– 17:25:52 9th February 2015
I have previously written code that does something similar to what you want: https://github.com/markwatkinson/music/blob/703b3752b083662e52370aaf10048ae2b0bd99af/server.py#L107
Look at play() starting on line 107. It's a little confusing because there are some more abstractions going on, but the gist of it is that you have a play() function:
Then in your front-end code, you will have your audio tag something like this:
– 18:00:43 9th February 2015
Thanks it works!! I'm new to flask (and coding in general) and have been stuck on this for ages :)
thanks for your help
– 21:31:38 9th February 2015
– 11:16:02 10th February 2015
– 15:37:40 20th August 2016
– 19:29:42 30th November 2016
– 22:03:05 10th January 2017
See this pull request in flask (already in flask since v0.12) and this pull request in werkzeug (set to be included in werkzeug v0.12).
– 21:54:40 1st March 2017