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]

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
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

JM
Thanks, this helped me a lot to stream .mp4 videos to an iPhone.

– 15:09:51 31st December 2013

Ahmed El Melegy
worked perfectly , thanks

– 21:17:33 1st January 2014

sesh
worked for me but two issues are present:
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

mark
[Admin]
@sesh:

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

Hi Mark, thanks for this! Seems to be exactly what I need for mp4 videos on Chrome. Quick (maybe very beginner) question though: do I just place this snippet in my main python app file and then place a normal html5 video tag in one of my templates? Thanks!

– 19:56:49 29th December 2014

mark
[Admin]
Hi Alex,

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

mark
[Admin]
Hi Jeremie,

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:

@app.route('/play/<song>')
def play(song):
   path_to_song = "something.mp3"
   send_file_partial(path_to_song)


Then in your front-end code, you will have your audio tag something like this:
<audio controls>
  <source src="/play/some-song" type="audio/mp3">
</audio>

– 18:00:43 9th February 2015

Hey Mark,

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

mark
[Admin]
Great :)

– 11:16:02 10th February 2015

livin_astro
Wow, this was a life saver! I was absolutely baffled by the fact that some of the mp3's I put in my audo element would allow seeking, and a bunch would not, but rather, restarted at zero whenever I attempted the seek functionality. Even stranger to me was that some songs that I previously would not seek, began to seek... When I added this entry to my views page in flask seeking began working with all tracks. Thank you very much for providing this solution!

– 15:37:40 20th August 2016

FlaskNewbie
Thanks man! Works perfectly for streaming mp4 video to chrome!

– 19:29:42 30th November 2016

Diego Abad
Thanks, works perfectly!!!

– 22:03:05 10th January 2017

hhm0
This is coming to flask/werkzeug at long last...

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

Leave a comment:

HTML is not valid. Use:
[url=http://www.google.com]Google[/url] [b]bold[/b] [i]italics[/i] [u]underline[/u] [code]code[/code]