14

I'm using Python2.7, django==1.7 and uwsgi for streaming video/mp4 file to iPhone player.

My code is as below:

def stream(request):
     with open('/path/video.mp4', 'r') as video_file:
        response = HttpResponse(video_file.read(), content_type='video/mp4')
        response['Content-Disposition'] = 'inline; filename=%s' % 'video.mp4'
        return response
     video_file.close

When i use some small video (less than 1MB), it streams in browser, but in iPhone palyer i have this error:

[uwsgi-http key: 127.0.0.1:8008 client_addr: 192.168.0.172 client_port: 14563] hr_write(): Broken pipe [plugins/http/http.c line 564]

And when the video size is more that 5MB, it doesn't stream in both (means browser and iPhone player) with same error.

I tried to do that by chunk chunk returning using StreamHttpRespose as below:

def read(chunksize=8192):
    with open('/path/video.mp4', 'rb') as video_file:
        byte = video_file.read(chunksize)
        while byte:
            yield byte

return StreamingHttpResponse(read(), content_type='video/mp4')

But there is the same error: Broken pipe.

fyi I can stream pdf and image files. This problem is only with mp4 files. And also i changed the content_type to 'video-mpeg', the browser downloaded that, while i want to prevent file downloading.

What's your idea? Any solution!!?

Aida.Mirabadi
  • 996
  • 4
  • 10
  • 27
  • In order to stream you need another thread to write the data to the response. Because the way you do it will simple wait until you've read the whole file and send it together. – Bogdan Iulian Bursuc Oct 19 '15 at 08:08
  • @BogdanIulianBursuc Thanks for your comment, But in the second solution (StreamHttpResponse) i'm reading video file as byte and return it in each chunk through yield command. it means it doesn't need to wait for getting whole file. – Aida.Mirabadi Oct 19 '15 at 08:18
  • Hi Aida, I would love to know if you've found any solution on this subject. I'm having the same issue :) Thanks – Arnaud Nov 27 '15 at 16:10
  • 1
    @Charlie, I explained you what i did for that. But it's not the solution, just it works correctly. If you find any solution, please answer my question and earn your score ;) – Aida.Mirabadi Nov 27 '15 at 20:45
  • Thanks @Aida.Mirabadi ! – Arnaud Nov 30 '15 at 09:19

2 Answers2

34

I had the same problem and did a lot of digging before finding a workable solution!

Apparently the Accept Ranges header is needed for HTML5 video controls to work (https://stackoverflow.com/a/24977085/4264463). So, we need to both parse the requested range from HTTP_RANGE and return Content-Range with the response. The generator that is passed to StreamingHttpResponse also needs to return content based on this range as well (by offset and length). I've found the follow snippet that works great (from http://codegist.net/snippet/python/range_streamingpy_dcwatson_python):

import os
import re
import mimetypes
from wsgiref.util import FileWrapper

from django.http.response import StreamingHttpResponse


range_re = re.compile(r'bytes\s*=\s*(\d+)\s*-\s*(\d*)', re.I)


class RangeFileWrapper(object):
    def __init__(self, filelike, blksize=8192, offset=0, length=None):
        self.filelike = filelike
        self.filelike.seek(offset, os.SEEK_SET)
        self.remaining = length
        self.blksize = blksize

    def close(self):
        if hasattr(self.filelike, 'close'):
            self.filelike.close()

    def __iter__(self):
        return self

    def __next__(self):
        if self.remaining is None:
            # If remaining is None, we're reading the entire file.
            data = self.filelike.read(self.blksize)
            if data:
                return data
            raise StopIteration()
        else:
            if self.remaining <= 0:
                raise StopIteration()
            data = self.filelike.read(min(self.remaining, self.blksize))
            if not data:
                raise StopIteration()
            self.remaining -= len(data)
            return data


def stream_video(request, path):
    range_header = request.META.get('HTTP_RANGE', '').strip()
    range_match = range_re.match(range_header)
    size = os.path.getsize(path)
    content_type, encoding = mimetypes.guess_type(path)
    content_type = content_type or 'application/octet-stream'
    if range_match:
        first_byte, last_byte = range_match.groups()
        first_byte = int(first_byte) if first_byte else 0
        last_byte = int(last_byte) if last_byte else size - 1
        if last_byte >= size:
            last_byte = size - 1
        length = last_byte - first_byte + 1
        resp = StreamingHttpResponse(RangeFileWrapper(open(path, 'rb'), offset=first_byte, length=length), status=206, content_type=content_type)
        resp['Content-Length'] = str(length)
        resp['Content-Range'] = 'bytes %s-%s/%s' % (first_byte, last_byte, size)
    else:
        resp = StreamingHttpResponse(FileWrapper(open(path, 'rb')), content_type=content_type)
        resp['Content-Length'] = str(size)
    resp['Accept-Ranges'] = 'bytes'
    return resp
Kevin Lee
  • 1,171
  • 12
  • 30
  • Can you please show an example on how to do it in browser – Joseph Daudi Mar 08 '18 at 22:05
  • @JosephDaudi HTML5 video will automatically stream the data from the endpoint associated with `stream_video`. So, I used `stream_video` as a view in my `urls.py` file and then had html like: `` – Kevin Lee Mar 09 '18 at 00:25
  • @KevinLee in the view, there is no reference to the html file. And I think you need either a view with a stream `or` the `video` html tag, but not both. – Timo Aug 23 '18 at 15:01
  • I don't understand @Timo. This is a solution for a django endpoint that the browser would stream video from. – Kevin Lee Aug 23 '18 at 16:55
  • @KevinLee Do you use a html file? Which filename? As written on 9th march, you seem to do so using ` – Timo Aug 25 '18 at 21:29
  • @Timo Yes you do use the – Kevin Lee Aug 26 '18 at 07:57
  • But you need a reference in your view to the html file. – Timo Sep 01 '18 at 08:42
  • @KevinLee Would this work for other types of files such as JPEG or Word Documents? – spencer.pinegar Mar 31 '20 at 20:31
  • @KevinLee I tried your solution. I got a byte string back and then concated to `'data:video/mp4;base64,'` and set this to `src` but it doesn't work. I also tried your suggestion for setting `src` to the full video path, but also didn't work. Any other suggestions? – Soubriquet Jun 09 '20 at 21:27
  • @Soubriquet, the correct way to apply this solution is to set the `src` to your Django endpoint on a `` tag within your ` – Kevin Lee Jun 09 '20 at 22:20
  • @KevinLee just to clarify, by endpoint you're referring to the Django urlpattern e.g. `re_path('endpoint/', my_view)`, right? – Soubriquet Jun 09 '20 at 22:25
  • @Soubriquet, yes – Kevin Lee Jun 09 '20 at 22:26
  • @KevinLee my mp4 uses H.264 codec, but that still doesn't work – Soubriquet Jun 09 '20 at 22:37
  • @KevinLee when I console.log the resp from `stream_video()` I get something that looks like this: `ftypmp42mp41mp42isommoovlmvhd` Is this correct? – Soubriquet Jun 09 '20 at 23:28
  • @Soubriquet, yes you should be getting binary output. You don't need to write javascript at all though. HTML5 handles the source by itself. – Kevin Lee Jun 09 '20 at 23:47
  • Deleted my previous two comments. So actually when I hardcode, it's not hitting the backend. I'm on local rn and hardcoding like `src="/video/test.mp4"`. Also I'm using Vue on the frontend on port 8080 while Django runs 8000. I guess you'r example just assumed Django is rendering the views. How would I make your solution work with Vue.js? – Soubriquet Jun 10 '20 at 01:05
  • @KevinLee can we move to this question I opened? https://stackoverflow.com/questions/62291888/how-to-stream-mp4-videos-from-django-3-to-vue-js – Soubriquet Jun 10 '20 at 01:14
  • I am trying to use this, but after adding the video path in the template, I am getting "no reverse match with view and argument path" am I doing something wrong? here is my source looks in the video tag. `` – Cynthia Onyilimba Aug 08 '22 at 11:34
2

After a lot of search, i didn't find my solution.

So, i tried to create a stream-server easily using nodejs from html5-video-streamer.js reference as below:

var http       = require('http'),
    fs         = require('fs'),
    url        = require('url'),
    basePath   = '/var/www/my_project/media/',
    baseUrl    = 'Your Domain or IP',
    basePort   = 8081;

http.createServer(function (req, res) {

    // Get params from request.
    var params    = url.parse(req.url, true).query, 
        filePath  = basePath + params.type + '/' + params.name,
        stat      = fs.statSync(filePath),
        total     = stat.size;

      if (req.headers['range']) {
        var range         = req.headers.range,
            parts         = range.replace(/bytes=/, "").split("-"),
            partialstart  = parts[0],
            partialend    = parts[1],
            start         = parseInt(partialstart, 10),
            end           = partialend ? parseInt(partialend, 10) : total-1,
            chunksize     = (end-start)+1;

        var file = fs.createReadStream(filePath, {start: start, end: end});
        res.writeHead(206, { 'Content-Range'  : 'bytes ' + start + '-' + end + '/' + total,
                             'Accept-Ranges'  : 'bytes',
                             'Content-Length' : chunksize,
                             'Content-Type'   : 'video/mp4' });
        file.pipe(res);

        // Close file at end of stream.
        file.on('end', function(){
          file.close();
        });
      } 
      else {
        res.writeHead(206, { 'Content-Length'   : total,
                             'Content-Type'     : 'video/mp4' });

        var file = fs.createReadStream(filePath);
        file.pipe(res);

        // Close file at end of stream.
        file.on('end', function(){
          file.close();
        });
      }
 }).listen(basePort, baseUrl);

Now i have separate stream-server with nodejs that streams mp4 files beside python project that provides my APIs.

I'm aware It's not my solution, but it works for me ;)

Aida.Mirabadi
  • 996
  • 4
  • 10
  • 27
  • Actually, I'm already using a nodejs server to stream our video via a websocket. But the error is still here :( – Arnaud Nov 30 '15 at 17:49
  • @Charlie i used "createReadStream" method to stream mp4 files not websocket. i'll put my code in my answer. – Aida.Mirabadi Dec 01 '15 at 07:14
  • Thanks a lot. But I'm not sure this can fit my needs. We're not using HttpResponse: we display the video in a web browser using videocanvas with "ws://serverip:port" – Arnaud Dec 01 '15 at 14:55
  • @Charlie ok, got it. So, Lets see this link too => http://binaryjs.com/ . and finally If you find your solution, please answer my question. Thanks. – Aida.Mirabadi Dec 02 '15 at 05:08
  • @Aida.Mirabadi i used your code but i get this error can you please help me? ` Error: ENOENT: no such file or directory, stat '/var/www/html/test/undefined/undefined' at Object.fs.statSync (fs.js:968:11) at Server. (/home/mohammadreza/www/html/academy/client/m4user/src/app/components/dashboard/superuser/server.js:13:24) ` – moh Feb 28 '18 at 08:50