[译] 用 Flask 输出视频流

相信你已经知道,我和 O'Reilly Media 合做出版了 讲解 Flask 的书籍和一些视频。尽管这些书籍和视频对 Flask 的讲解已经足够详细了,但因为某些缘由一小部分特性讲的不够多,所以我以为把他们写在这篇文章中是个好主意。html

本文专一于,一个有意思的特性,它让 Flask 应用可以以分割成小块的形式提供超大的响应,这可能要花一段较长的时间。为了阐明这个主题,你将会看到如何构建一个实时视频流服务器。前端

注意:如今有一篇关于本文的后续文章,Flask Video Streaming Revisited,我在后续文章中讲了关于本文介绍的流服务器的一些改进。python

什么是流?

流是一种让服务器在响应请求时将响应数据分块的技术。我能想到好多可能颇有用的理由:android

  • 超级巨大的响应数据。对于超大的响应数据来讲,先把响应数据装载到内存中,再返回给客户端是很是低效的。另外一种方法是将响应数据写入到磁盘中,而后用 flask.send_file() 将文件返回给客户端,但这样将会增长 I/O 操做。若是响应数据较小,这就是个好得多的方法,由于数据可以按块进行存储。
  • 实时数据。对于某些应用来讲,也许须要向某个请求返回来自实时数据源的数据。一个很贴切的例子是实时视频或音频传送。不少安全摄像头用该技术将视频以流的形式发送到服务器。

用 Flask 实现流

Flask 经过使用 生成器(generator functions) 原生支持流式响应。生成器是一个特殊的函数,能够被停止或继续运行。看看下面的函数:ios

def gen():
    yield 1
    yield 2
    yield 3
复制代码

这是一个分三步运行的函数,每一步都返回一个值。生成器的实现超出了本文的范围,若是你对此很感兴趣的话,下面的 shell session 会让你知道怎么使用生成器:git

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

能够看到,在这个简单的例子中,一个生成器能够依次返回多个值。Flask 使用生成器的该特性实现了流。github

下面的例子展现了如何在不把整个表都装配到内存的状况下,使用流生成巨型数据表:web

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 对象。Flask 将会负责调用生成器,并把全部部分的结果以块的形式发送给客户端。shell

译者注:python3 中,访问 /stock-table 路由时,若是在 Debug 模式下看到 AttributeError: 'NoneType' object has no attribute 'app',则须要将 Response 的入参用 stream_with_context() 预处理。导入该函数:from flask import stream_with_context,路由的返回值:return Response( stream_with_context( generate_stock_table() ) )数据库

对于这个特殊的例子,假设 Stock.query.all() 返回的是可迭代的数据库查询结果,那么你能够按每次一行的速度生成一个巨大的表,所以不管查询结果中的元素数量有多少,该 Python 进程的内存占用不会由于装配巨大的响应字符串而变得愈来愈大。

分部响应

上述的表格示例生成小部分传统页面,再把全部部分衔接成最终的文档。这是如何生成巨大响应的很好的示例,但更让人兴奋的事情是操做实时数据。

一种有趣的流的用法是让每个数据块取代页面中的前一块,这样流就可以在浏览器窗口中进行“播放”或者动画。使用该技术你可以用图片做为流的每一部分,这将带来一个很酷的在浏览器中运行的视频播放器。

实现原地更新的秘诀在于使用 multipart(分部) 响应。分部响应的内容是一个包含分部内容类型的头部,后面的是用 boundary(分界线) 标记分割的部分,每一部分有各自的特定内容类型。

有若干个分部内容类型用于不一样的用途。为了达到让流中的每部分可以替代前一部分的目的,内容类型必须用 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 头部,代表该部分数据的字节数长度,但至少对于图片来讲,浏览器不须要长度也可以处理流数据。

构建一个实时视频流服务器

在本文中已经有了足够的理论,如今是时候构建一个完整的可以将直播视频流式传输到浏览器的应用了。

有不少种流式传输视频到浏览器的方式,每一种方法各有优劣。与 Flask 的流式特性结合得很是好的一种方法是流式输出一系列单独的 JPEG 图片。这被称为 移动的 JPEG(Motion JPEG),这种方法正被一些 IP 安全摄像头使用。这种方法的延迟低,可是质量并非最好,由于对于移动视频来讲,JPEG 的压缩并不高效。

下面你将看到一个特别简单但又十分完善的 web 应用,能够提供移动的 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 页面,只有一个 heading 和一个图片标签。注意图片标签的 src 属性指向的是该应用的第二个路由,而这正是奇妙的地方。

路由 /video_feed 返回的是流式响应。由于流返回的是能够显示在网页中的图片,到该路由的 URL 就放在图片标签的 src 属性中。浏览器会自动显示流中的 JPEG 图片,从而保持更新图片元素,因为分部响应受大多数(甚至全部)浏览器的支持(若是你找到一款浏览器没有这种功能,请务必告诉我)。

/video_feed 路由中用到的生成器函数叫作 gen(),它接收 Camera 类的实例做为参数。mimetype 参数的设置和上面同样,是 multipart/x-mixed-replace 类型,边界字符串设置为 frame

gen() 函数进入循环,从而持续地将摄像头中获取的帧数据做为响应块返回。该函数经过调用 camera.get_frame() 方法从摄像头中获取一帧数据,而后它将这一帧之内容类型为 image/jpeg 的响应块形式产出(yield),如上所述。

从视频摄像头中获取帧

如今剩下要作的只有实现 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 作出了下面的图片:

Frame 1
Frame 2
Frame 3

由于相机模拟出来了,这个应用能够运行在任何环境中,所以你能够当即运行它!我把这个应用的全部东西都准备好了,放在 GitHub 上。若是你熟悉 git,你能够用下面的命令克隆这个仓库:

$ git clone https://github.com/miguelgrinberg/flask-video-streaming.git
复制代码

若是你要下载该应用,你能够从 这儿 获取一个 zip 压缩文件。

装好应用后,建立一个虚拟环境并安装好 Flask。而后你能够运行命令:

$ python app.py
复制代码

当你开启应用后,在浏览器中输入 http://localhost:5000,你就能看到模拟的视频流,不断播放着图片 一、二、3。是否是很酷?

当我作好了这些事情后,我用相机模块启动了树莓派,并实现了一个新的 Camera 类,这个类将树莓派转换成一个视频流服务器,使用 picamera 包来控制硬件。这里不会涉及到相应的相机实现,但你能够在文件 camera_pi.py 中找到相应的源代码。

若是你有一个树莓派和相机模块,你能够编辑 app.py 文件,从这个模块中引入 Camera 类,而后你就能够流式直播树莓派的相机,就像在下面的截图中我所作的那样:

Frame 1

若是你想让这个流式应用和不一样的相机一块儿使用,那么你要作的就是改写 Camera 类的实现。若是你实现了这样的一个相机类,并将它贡献到个人 GitHub 项目中,我将不胜感激。

流的局限

Flask 应用在服务常规请求时,请求的周期短。web worker 接收到请求,调用处理函数,最终返回响应。一旦响应返回给了客户端,worker 就处于空闲状态,等待着接收下一次请求。

当接收到使用流的请求时,在流的持续时间内 worker 一直留存在客户端中。在处理永不结束的、长的流时,好比从摄像机发来的一个视频流,worker 将会对客户端保持锁定状态,直到客户端断开链接。这也就意味着除非采用特殊的方法,不然有多少客户端,应用就要为多少 web workers 提供服务。在 debug 模式下运行 Flask 应用意味着只有一个线程,所以你没法打开另外一个浏览器窗口,在两个地方同时观看流。

有不少方法能够解决这个关键的限制。我认为最好的方案是使用基于协程的 web 服务器,好比 Flask 支持很好的 gevent。gevent 经过使用协程可以在一个工做线程中处理多个客户端,由于 gevent 修改了 Python I/O 函数,在必要时处理上下文的切换。

结论

若是你跳过了上面的内容,能够在 GitHub 仓库上看到本文相应的代码:github.com/miguelgrinb…。你能从中找到不须要相机的视频流通用实现,也能够看到树莓派相机模块的实现。这篇 后续文章 讲述了本文最开始发布后我所作的一些改进。

我但愿本文可以为流这一话题带来一些启发。我专一于视频流,由于我在这一领域中有些经验,但流的应用不只限于视频。好比,这个技术能够用来保持服务器与客户端的链接长时间有效,容许服务器在有信息时发送新信息。最近 Web Socket 协议能够更高效的实现这个目的,可是 Web Socket 至关新颖,只能在现代浏览器中使用,而流却能在很是多的浏览器中使用。

若是你有任何问题,请将它们写在下方。我打算为不为大众所知的 Flask 专题继续撰写文章,因此但愿你能以某种方式联系我,以便知道更多文章发布的时间,下篇文章中再见。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索