Python - Django:使用HttpResponse流式传输video/mp4文件

14

我正在使用Python2.7,django==1.7uwsgi来将视频/mp4文件流式传输到播放器。

我的代码如下:

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

当我使用一些小于1MB的小视频时,它可以在浏览器中流式传输,但是在iPhone播放器中出现了以下错误:

[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]

当视频大小超过5MB时,在浏览器和iPhone播放器中都无法流式传输,都会出现相同的错误。

我尝试通过使用StreamHttpRespose分块返回来实现这个目标,代码如下:

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')

但是仍然出现相同的错误:Broken pipe

我可以流式传输PDF和图像文件。这个问题只出现在MP4文件中。我已将content_type更改为“video-mpeg”,浏览器下载了该文件,而我想要防止文件下载。你有什么想法吗?有解决方法吗?


为了进行流式传输,您需要另一个线程将数据写入响应中。因为如果您不这样做,程序会一直等待,直到整个文件都被读取并一起发送。 - Bogdan Iulian Bursuc
@BogdanIulianBursuc 感谢您的评论,但在第二种解决方案(StreamHttpResponse)中,我将视频文件作为字节读取,并通过yield命令在每个块中返回它。这意味着它不需要等待获取整个文件。 - Aida.Mirabadi
嗨,艾达,我想知道你是否找到了任何关于这个主题的解决方案。我也遇到了同样的问题 :) 谢谢 - Arnaud
1
@Charlie,我已经向你解释了我为此所做的事情。但这不是解决方案,只是它能正常工作。如果你找到任何解决方案,请回答我的问题并获得分数;) - Aida.Mirabadi
感谢 @Aida.Mirabadi! - Arnaud
2个回答

35

我曾经遇到同样的问题,在找到可行解决方案之前做了很多调查研究!

显然,HTML5视频控件需要 Accept Ranges 标题才能正常工作(参见 https://dev59.com/718f5IYBdhLWcg3wJf0H#24977085)。因此,我们需要从HTTP_RANGE解析请求范围,并在响应中返回Content-Range。传递给StreamingHttpResponse的生成器还需要根据这个范围返回内容(通过 offsetlength)。我找到了以下代码段,它运行良好(来自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

@JosephDaudi HTML5视频将自动从与stream_video相关的终端点流式传输数据。因此,我在我的urls.py文件中使用了stream_video作为视图,然后有类似于以下的HTML代码:<video width="320" height="240" controls><source src="the/full/url/to/video.mp4" type="video/mp4"></video> - Kevin Lee
我不明白@Timo的意思。这是一个Django端点的解决方案,浏览器可以从中流式传输视频。 - Kevin Lee
但是您需要在视图中引用HTML文件的引用。 - Timo
@KevinLee 这对其他类型的文件如JPEG或Word文档适用吗? - spencer.pinegar
我正在尝试使用这个,但在将视频路径添加到模板后,我得到了“视图和参数路径不匹配”的错误信息。我做错了什么吗?以下是我的源代码中视频标签的样子。<source src="{% url 'video:stream_video' path="video.data.path" %}" type="video/mp4" /> - Cynthia Onyilimba
显示剩余14条评论

2

经过大量搜索,我没有找到解决方案。

因此,我尝试使用以下来自html5-video-streamer.js的参考代码,轻松创建一个流服务器:nodejs

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);

现在我有一个独立的流媒体服务器,使用nodejs来流式传输mp4文件,除此之外还有一个提供API的python项目。
我知道这不是最好的解决方案,但对我来说它很有效 ;)

实际上,我已经在使用Node.js服务器通过WebSocket流式传输我们的视频。但是错误仍然存在 :( - Arnaud
@Charlie 我使用了“createReadStream”方法来流式传输mp4文件,而不是websocket。我会在我的回答中放置我的代码。 - Aida.Mirabadi
非常感谢。但我不确定这是否符合我的需求。我们没有使用HttpResponse:我们使用videocanvas在Web浏览器中显示视频,使用“ws://serverip:port”。 - Arnaud
@Charlie 好的,明白了。那么,让我们也看一下这个链接 => http://binaryjs.com/ 。最后,如果你找到了解决方案,请回答我的问题。谢谢。 - Aida.Mirabadi
@Aida.Mirabadi 我使用了你的代码,但是出现了以下错误,你能帮忙解决一下吗?错误:ENOENT:没有这样的文件或目录,状态'/var/www/html/test/undefined/undefined' 在Object.fs.statSync(fs.js:968:11) 在Server.<anonymous> (/home/mohammadreza/www/html/academy/client/m4user/src/app/components/dashboard/superuser/server.js:13:24) - moh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接