Python Flask如何实播放视频流?深入浅出实现方案

本文致力于流式传输,这是一个有趣的功能,它使Flask应用程序能够长时间有效地将大型响应有效地分成小块。为了说明这一主题,我将向您展示如何构建实时视频流服务器!

注意:本文有后续内容,Flask视频流再回顾,其中我介绍了此处介绍的流服务器的一些改进。

什么是流媒体?

流是一种技术,其中服务器以块的形式提供对请求的响应。我可以想到一些可能有用的原因:

  • 很大的Response。对于非常大的响应,仅在内存中组装响应以将其返回给客户端可能是低效率的。一种替代方法是将响应写入磁盘,然后使用来返回文件flask.send_file(),但这会增加I / O。假设可以分块生成数据,则以小部分提供响应是一种更好的解决方案。
  • 实时数据。对于某些应用程序,请求可能需要返回来自实时源的数据。实时视频或音频提要就是一个很好的例子。许多安全摄像机使用此技术将视频流传输到Web浏览器。

用Flask实现流

Flask通过使用生成器功能为流式响应提供了本机支持。生成器是一种特殊功能,可以中断和恢复。考虑以下功能:

def gen():
    yield 1
    yield 2
    yield 3

此函数分三个步骤运行,每个步骤都返回一个值。描述如何实现生成器功能不在本文的讨论范围之内,但是如果您有点好奇,下面的shell会话将带您了解如何使用生成器:

>>> x = gen()
>>> x
<generator object gen at 0x7f06f3059c30>
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

您可以在这个简单的示例中看到一个生成器函数可以按顺序返回多个结果。Flask利用生成器功能的这一特性来实现流式传输。

下面的示例显示了如何使用流技术来生成大型数据表,而不必在内存中组装整个表:

from flask import Response, render_template
from app.models import Stock

def generate_stock_table():
    yield render_template('stock_header.html')
    for stock in Stock.query.all():
        yield render_template('stock_row.html', stock=stock)
    yield render_template('stock_footer.html')

@app.route('/stock-table')
def stock_table():
    return Response(generate_stock_table())

在此示例中,您可以看到Flask如何与生成器函数一起工作。返回流式响应的路由需要返回一个Response使用generator函数初始化的对象。然后Flask负责调用生成器,并将所有部分结果作为块发送给客户端。

对于此特定示例,如果假设Stock.query.all()以迭代方式返回数据库查询的结果,则可以一次生成一个潜在的大表,因此无论查询中有多少元素,Python进程中的内存消耗都将由于必须组装一个较大的响应字符串而不会变得越来越大。

Multipart Responses

上面的表格示例会一小部分地生成一个传统页面,所有部分都连接到最终文档中。这是如何产生大响应的一个很好的例子,但是更令人兴奋的是使用实时数据。

流的一种有趣用法是让每个块替换页面中的前一个块,因为这使流能够在浏览器窗口中“播放”或设置动画。使用这种技术,您可以使流中的每个块都成为一个图像,从而为您提供在浏览器中运行的超酷视频供稿!

实现就地更新的秘密是使用Multipart Responses。Multipart Responses包含一个标头,该头包含一个多部分内容类型之一,然后是由边界标记分隔的部分,每个部分都有其自己的特定于部分内容的类型。

有几种多部分内容类型可以满足不同的需求。为了在每个部分都替换前一个部分的流中multipart/x-mixed-replace使用,必须使用内容类型。为了帮助您了解外观,以下是多部分视频流的结构:

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame

--frame
Content-Type: image/jpeg

<jpeg data here>
--frame
Content-Type: image/jpeg

<jpeg data here>
...

正如您在上面看到的,结构非常简单。将主Content-Type标头设置为,multipart/x-mixed-replace并定义边界字符串。然后包括每个零件,在它们自己的行中以两个破折号和零件边界字符串为前缀。这些部分具有自己的Content-Type标头,并且每个部分都可以选择包含一个Content-Length标头,该标头的长度为部分有效载荷的字节数,但至少对于图像而言,浏览器能够处理没有该长度的流。

构建实时视频流服务器

本文有足够的理论,现在是时候构建一个完整的应用程序,将实时视频流传输到Web浏览器。

流视频到浏览器的方法有很多,每种方法都有其优点和缺点。与Flask的流传输功能很好配合的方法是流传输一系列独立的JPEG图片。这称为Motion JPEG,并且被许多IP安全摄像机使用。这种方法的延迟时间很短,但是质量并不是最好的,因为JPEG压缩对于运动视频不是很有效。

在下面,您可以看到一个非常简单但完整的Web应用程序,可以为Motion JPEG流提供服务:

#!/usr/bin/env python
from flask import Flask, render_template, Response
from camera import Camera

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

def gen(camera):
    while True:
        frame = camera.get_frame()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

@app.route('/video_feed')
def video_feed():
    return Response(gen(Camera()),
                    mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)

该应用程序导入一个Camera负责提供帧序列的类。在这种情况下,将摄像机控制部分放在单独的模块中是个好主意,这样,Web应用程序便保持干净,简单和通用。

该应用程序有两条路线。该/路线将服务于在index.html模板中定义的主页。您可以在下面看到此模板文件的内容:

<html>
  <head>
    <title>Video Streaming Demonstration</title>
  </head>
  <body>
    <h1>Video Streaming Demonstration</h1>
    <img src="{{ url_for('video_feed') }}">
  </body>
</html>

这是一个简单的HTML页面,仅包含标题和图像标签。请注意,图像标签的src属性指向此应用程序的第二条路线,这就是魔术发生的地方。

/video_feed路线返回流响应。因为此流返回要在网页中显示的图像,所以此路由的URLsrc在image标记的属性中。浏览器将通过在其中显示JPEG图像流来自动更新图像元素,因为大多数/所有浏览器都支持多部分响应(如果您发现不喜欢这种浏览器的话,请告诉我)。

/video_feed路由中使用的生成器函数称为gen(),并将Camera类的实例作为参数。在mimetype如上述所示,用参数设定multipart/x-mixed-replace的内容类型和边界设置为字符串"frame"

gen()函数进入一个循环,在该循环中,该函数不断从相机返回帧作为响应块。该函数通过调用camera.get_frame()方法要求相机提供一个帧,然后以该帧的形式格式化为内容类型为的响应块image/jpeg,如上所示。

从摄像机获取帧

现在剩下的就是实现Camera该类了,该类必须连接到摄像机硬件并从中下载实时视频帧。将这个应用程序的硬件相关部分封装在一个类中的好处是,该类可以为不同的人提供不同的实现,但是应用程序的其余部分保持不变。您可以将此类视为设备驱动程序,无论使用什么实际的硬件设备,它都可以提供统一的实现。

Camera类与应用程序的其余部分分开的另一个优点是,很容易使应用程序愚弄应用程序以至于认为实际上有一个摄像头,因为没有摄像头类可以实现为模拟摄像头,而无需真正的硬件。实际上,当我在开发此应用程序时,测试流的最简单方法就是这样做,而不必担心硬件,直到我运行所有其他功能。在下面,您可以看到我使用的简单的模拟摄像机实现:

from time import time

class Camera(object):
    def __init__(self):
        self.frames = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]

    def get_frame(self):
        return self.frames[int(time()) % 3]

此实现从磁盘读取三个图像叫1.jpg2.jpg3.jpg再返回它们彼此反复后,以每秒一帧的速率。该get_frame()方法使用以秒为单位的当前时间来确定在任何给定时刻要返回的三个帧中的哪一个。很简单,对不对?

要运行此仿真相机,我需要创建三个框架。使用gimp我制作了以下图像:

由于摄像机是模拟的,因此该应用程序可以在任何环境下运行,因此您可以立即运行它!我已经准备好将此应用程序放到GitHub上。如果您熟悉,git可以使用以下命令克隆它:

$ git clone https://github.com/miguelgrinberg/flask-video-streaming.git

如果您喜欢下载它,则可以在此处获得一个zip文件。

安装完应用程序后,创建一个虚拟环境并在其中安装Flask。然后,您可以按以下方式运行该应用程序:

$ python app.py

启动应用程序后,进入http://localhost:5000Web浏览器,您将看到模拟的视频流反复播放1、2和3图像。太酷了吧?

一旦一切正常工作,我就用其相机模块启动Raspberry Pi,并实现了一个新Camera类,该类使用该picamera包来控制硬件,从而将Pi转换为视频流服务器。我不会在这里讨论此相机的实现,但是您可以在file的源代码中找到它camera_pi.py

如果您有Raspberry Pi和相机模块,则可以编辑app.pyCamera从该模块导入该类,然后就可以实时播放Pi相机,就像我在以下屏幕截图中所做的那样:

如果要使此流应用程序与其他摄像机一起使用,则您所需要做的就是编写Camera该类的另一个实现。如果您最终写了一篇,那么如果您将其贡献给我的GitHub项目,我将不胜感激。

流媒体的局限性

当Flask应用程序处理常规请求时,请求周期很短。网络工作者接收请求,调用处理程序函数,最后返回响应。一旦响应被发送回客户端,工作人员就可以自由地准备接受另一个请求。

收到使用流式传输的请求时,工作者在流的持续时间内保持与客户端的连接。当使用长而永无休止的流(例如来自摄像机的视频流)时,工作人员将保持锁定状态,直到客户端断开连接。这实际上意味着,除非采取特定措施,否则该应用程序只能为服务与Web工作人员一样多的客户端提供服务。当在调试模式下使用Flask应用程序时,这意味着只有一个,因此您将无法连接第二个浏览器窗口来同时观看来自两个位置的流。

有多种方法可以克服此重要限制。我认为最好的解决方案是使用Flask完全支持的基于协程的Web服务器,例如gevent。通过使用协程,gevent可以在单个工作线程上处理多个客户端,因为gevent修改了Python I / O函数以根据需要发出上下文切换。

结论

如果您在上面错过了它,则支持本文的代码是以下GitHub存储库:https : //github.com/miguelgrinberg/flask-video-streaming/tree/v1。在这里,您可以找到不需要摄像头的视频流通用实现,以及Raspberry Pi摄像头模块的实现。这篇后续文章描述了本文最初发表后我所进行的一些改进。

我希望本文能对流媒体的话题有所启发。我专注于视频流传输,因为这是我有经验的领域,但是流传输除了视频之外还有更多用途。例如,该技术可用于使客户端和服务器之间的连接保持活跃状态很长时间,从而允许服务器在新信息可用时推送新信息。如今,Web Socket协议是实现此目的的更有效方法,但是Web Socket相当新,并且仅在现代浏览器中有效,而流式传输几乎可以在您能想到的任何浏览器上运行。

Ref:

https://blog.miguelgrinberg.com/post/video-streaming-with-flask

举报
评论 0