2

I'm trying to play a video that is served by a Flask web application on my iOS application. While I can play any video served with a "conventional" web server (like Apache), I can't play the video served by Flask. Here is the relevant code:

Objective-C

NSURL *videoURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@",videourltemp]];
AVPlayer *player = [AVPlayer playerWithURL:videoURL];

playerViewController.player = player;
[self.view addSubview:playerViewController.view];
[self.navigationController pushViewController:playerViewController animated:YES];

Python

from flask import Response, ...

def get_img(imgid):
    # private code hidden - file["path"] contains the path relative to /root/media_assets directory

    return Response(open("/root/media_assets/" + file["path"], "rb"), mimetype="video/mp4")

Sidenote: if I try to reach my URL from a browser, the video is correctly loaded.

How could I solve my problem?

Thank you in advance!

jinzo78
  • 195
  • 1
  • 2
  • 9

2 Answers2

2

I encountered the same problem and eventually found that the real issue is that the video player client (in Objective-C iOS at least) uses the "range" header in the response (you can print out Flask request.headers to check). In other words, the streaming is really implemented using "range" support in HTTP.

I followed examples at https://codeburst.io/the-taste-of-media-streaming-with-flask-cdce35908a50, the Flask server code needs to build response using "partial content" (HTTP status code 206) and needs to process the "range" header in the request. The related code looks like this:

  1. add "Accept-Ranges" in Flask app after_request so that the client knows "range" is supported:
@app.after_request
def after_request(response):
    response.headers.add('Accept-Ranges', 'bytes')
    return response
  1. in your function that serving the mp4 file, suppose the file path is "full_path":
    file_size = os.stat(full_path).st_size
    start = 0
    length = 10240  # can be any default length you want

    range_header = request.headers.get('Range', None)
    if range_header:
        m = re.search('([0-9]+)-([0-9]*)', range_header)  # example: 0-1000 or 1250-
        g = m.groups()
        byte1, byte2 = 0, None
        if g[0]:
            byte1 = int(g[0])
        if g[1]:
            byte2 = int(g[1])
        if byte1 < file_size:
            start = byte1
        if byte2:
            length = byte2 + 1 - byte1
        else:
            length = file_size - start

    with open(full_path, 'rb') as f:
        f.seek(start)
        chunk = f.read(length)

    rv = Response(chunk, 206, mimetype='video/mp4', content_type='video/mp4', direct_passthrough=True)
    rv.headers.add('Content-Range', 'bytes {0}-{1}/{2}'.format(start, start + length - 1, file_size))
    return rv

In my testing, the above Flask code works with iOS objective-C client as well as Chrome, Firefox browsers for .mp4 files.

user1783732
  • 1,599
  • 5
  • 22
  • 44
0

You have two options there:

  1. Open the file and read it in chunks instead of reading it as a single blob, like in your code. Follow example from: https://stackoverflow.com/a/24318158/1955346:

    from flask import stream_with_context, Response
    
    @app.route('/stream_data')
    def stream_data():
        def generate():
            with open("/root/media_assets/" + file["path"], "rb") as f:
                while True:
                    chunk = ... # read each chunk or break if EOF
                    yield chunk
    
        return Response(stream_with_context(generate()), mimetype="video/mp4")
    
  2. Use direct approach from How do I stream a file using werkzeug?: return Response(file("/root/media_assets/" + file["path"]), direct_passthrough=True)

Marcin Zablocki
  • 10,171
  • 1
  • 37
  • 47
  • 1
    Neither solution managed to solve my problem. The first one makes my video load endlessly, the second one does not change the issue. If it may help, I've seen that not only iOS, but also Safari for Mac has this issue, while Chrome for any OS (including macOS) and Firefox has no issue. – jinzo78 Apr 24 '18 at 14:19