目录
一、Video Streaming with Flask(Flask视频流)
Flask视频流
streaming ——流式传输
它使Flask应用程序能够长时间有效地将大型响应有效地分成小块。
为了说明这一主题,我将向您展示如何构建实时视频流服务器!
二、 What is Streaming?(什么是流媒体?)
- 流是一种技术,其中服务器以块的形式提供对请求的响应。
有用的原因:
-
Very large responses _很大的反应。
对于非常大的响应,仅在内存中组装响应以将其返回给客户端可能是低效率的。一种替代方法是将响应写入磁盘,然后使用来返回文件
flask.send_file()
但这会增加I / O。假设可以分块生成数据,则以小部分提供响应是一种更好的解决方案。
-
Real time data._实时数据。
对于某些应用程序,请求可能需要返回来自实时源的数据。
实时视频或音频提要就是一个很好的例子。
许多安全摄像机使用此技术 :将视频流传输到Web浏览器。
三、用Flask实现流
- Flask通过使用生成器功能( generator functions.)为流式响应提供了本机支持。
- 生成器是一种特殊功能,可以中断和恢复。
考虑以下功能:
def gen():
yield 1
yield 2
yield 3
此函数分三个步骤运行,每个步骤都返回一个值。
- 一个生成器函数可以按顺序返回多个结果。
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负责调用生成器,并将所有部分结果作为块发送给客户端。
四、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
Motion JPEG, 即运动JPEG(M-JPEG或MJPEG)是一种视频压缩格式,其中数字视频序列的每个视频帧或隔行扫描场都分别压缩为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>
注意,图像标签的src
属性指向此应用程序的第二条路线
- 这就是魔术发生的地方。
- 该
/video_feed
路线返回流响应。
因为此流返回要在网页中显示的图像,所以此路由的URLsrc
在image
标记的属性中。
浏览器将通过在其中显示JPEG图像流来自动更新图像元素
- 因为大多数/所有浏览器都支持多部分响应*。
/video_feed
路由中使用的生成器函数称为gen()
- 并将
Camera
类的实例作为参数。
在mimetype
中,用参数设定multipart/x-mixed-replace
内容类型、、、边界boundary
设置为为字符串"frame
"。
@app.route('/video_feed')
def video_feed():
return Response(gen(Camera()),
mimetype='multipart/x-mixed-replace; boundary=frame')
六、从摄像机获取帧_Obtaining Frames from a Video Camera
剩下的就是实现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.jpg
、2.jpg
、3.jpg
- 再返回它们彼此反复后,以每秒一帧的速率。
该get_frame()
方法使用以秒为单位的当前时间
- 来确定在任何给定时刻,要返回的三个帧中的哪一个。
要运行此仿真相机,我需要创建三个框架。
启动应用程序后,进入【http://localhost:5000Web】浏览器,您将看到模拟的视频流反复播放1、2和3图像。
七、流媒体的局限性
- 当Flask应用程序处理常规请求时,请求周期很短。
- 网络工作者接收请求,调用处理程序函数,最后返回响应。
一旦响应被发送回客户端,工作人员就可以自由地准备接受另一个请求。
收到使用流式传输的请求时: - 工作者在流的持续时间内保持与客户端的连接。
当使用长而永无休止的流(例如来自摄像机的视频流)时,工作人员将保持锁定状态,直到客户端断开连接。 - 这实际上意味着,除非采取特定措施,否则该应用程序只能为服务与Web工作人员一样多的客户端提供服务。
- 当在调试模式下使用Flask应用程序时,这意味着只有一个,因此您将无法连接第二个浏览器窗口来同时观看来自两个位置的流。
克服此重要限制。我认为最好的解决方案是:
使用Flask完全支持的基于协程的Web服务器,例如gevent。
- 通过使用协程,gevent可以在单个工作线程上处理多个客户端,
- 因为gevent修改了Python I / O函数,以根据需要发出上下文切换。
八、后续
该文章非常受欢迎,这不是因为它教了如何实现流式响应
-
而是因为很多人都想实现流式视频服务器。
-
不幸的是,在撰写本文时,我的重点不是创建强大的视频服务器
对该服务器进行的一些改进。
8.1 回顾:使用Flask的视频流
- 简而言之,这是一台Flask服务器
它使用流响应来提供从运动JPEG格式的摄像机捕获的视频帧流。
这种格式非常简单,不是最有效的,但是它的优点是所有浏览器都可以本地支持它
而无需任何客户端脚本。
- 因此,这是安全摄像机使用的一种相当常见的格式。
- 为了演示服务器,我使用其相机模块为
Raspberry Pi
实现了相机驱动程序。
对于那些没有配备相机的Pi的人,我还编写了一个仿真的相机驱动程序,该驱动程序可以流式传输存储在磁盘上的jpeg图像序列。
8.2 捕获视频帧的后台线程启动,但是从不停止
当第一个客户端连接到流时,从Raspberry Pi摄像头捕获视频帧的后台线程启动,但是从不停止。
- 处理此后台线程的一种更有效的方法是:
仅在有查看器的情况下运行它,以便在没有人连接时可以关闭相机。
改进想法是:
- 每当客户端访问一个帧时,都会记录该访问的当前时间。
- 摄像头线程检查此时间戳,如果发现该时间戳早于十秒,则退出。
- 进行此更改后,如果服务器在没有任何客户端的情况下运行十秒钟,它将关闭其相机并停止所有后台活动。
- 客户端再次连接后,线程将重新启动。
class Camera(object):
# ...
last_access = 0 # time of last client access to the camera
# ...
def get_frame(self):
Camera.last_access = time.time()
# ...
@classmethod
def _thread(cls):
with picamera.PiCamera() as camera:
# ...
for foo in camera.capture_continuous(stream, 'jpeg', use_video_port=True):
# ...
# if there hasn't been any clients asking for frames in
# the last 10 seconds stop the thread
if time.time() - cls.last_access > 10:
break
cls.thread = None
8.3 简化相机类别 _ Simplifying the Camera Class
常见问题是,很难添加对其他相机的支持。
Camera
为Raspberry Pi实现的类相当复杂,因为它使用后台捕获线程与相机硬件通信。
简化操作:
对框架进行所有后台处理的通用功能移至基类
- 而仅剩下从摄像机获取框架以在子类中实现的任务。
BaseCamera
模块中的新类base_camera.py
实现了此基类。
这是通用线程的样子:
class BaseCamera(object):
thread = None # background thread that reads frames from camera
frame = None # current frame is stored here by background thread
last_access = 0 # time of last client access to the camera
# ...
@staticmethod
def frames():
"""Generator that returns frames from the camera."""
raise RuntimeError('Must be implemented by subclasses.')
@classmethod
def _thread(cls):
"""Camera background thread."""
print('Starting camera thread.')
frames_iterator = cls.frames()
for frame in frames_iterator:
BaseCamera.frame = frame
# if there hasn't been any clients asking for frames in
# the last 10 seconds then stop the thread
if time.time() - BaseCamera.last_access > 10:
frames_iterator.close()
print('Stopping camera thread due to inactivity.')
break
BaseCamera.thread = None
Raspberry Pi的摄像头线程的这个新版本,已通过使用另一个生成器而变得通用。
- 线程希望该
frames()
方法(是静态方法)是在特定于不同相机的子类中实现的生成器。 - 迭代器返回的每个项目都必须是
jpeg格式
的视频帧。
如何使返回静态图像的仿真相机适用于此基类:
class Camera(BaseCamera):
"""An emulated camera implementation that streams a repeated sequence of
files 1.jpg, 2.jpg and 3.jpg at a rate of one frame per second."""
imgs = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]
@staticmethod
def frames():
while True:
time.sleep(1)
yield Camera.imgs[int(time.time()) % 3]
注意,在此版本中:
frames()
生成器如何通过简单地在帧之间休眠,该数量来强制每秒一帧的帧速率。
通过这种重新设计,Raspberry Pi相机的相机子类也变得更加简单:
import io
import picamera
from base_camera import BaseCamera
class Camera(BaseCamera):
@staticmethod
def frames():
with picamera.PiCamera() as camera:
# let camera warm up
time.sleep(2)
stream = io.BytesIO()
for foo in camera.capture_continuous(stream, 'jpeg', use_video_port=True):
# return current frame
stream.seek(0)
yield stream.read()
# reset stream for next frame
stream.seek(0)
stream.truncate()
九、OpenCV相机驱动程序
相当多的用户抱怨说,他们无法访问配备了摄像头模块的Raspberry Pi,因此他们无法使用模拟摄像头以外的其他任何设备来尝试使用该服务器。
- 现在添加摄像头驱动程序要容易得多,我还希望有一个基于OpenCV的摄像头
- 该摄像头支持大多数USB网络摄像头和笔记本电脑摄像头。
这是一个简单的相机驱动程序:
import cv2
from base_camera import BaseCamera
class Camera(BaseCamera):
@staticmethod
def frames():
camera = cv2.VideoCapture(0)
if not camera.isOpened():
raise RuntimeError('Could not start camera.')
while True:
# read current frame
_, img = camera.read()
# encode as a jpeg image and return it
yield cv2.imencode('.jpg', img)[1].tobytes()
此类中,将使用系统报告的第一台摄像机。
- 如果您使用的是笔记本电脑,则可能是您的内置相机。
- 如果要使用此驱动程序,则需要安装适用于Python的OpenCV绑定:
$ pip install opencv-python
9.1 相机选择
项目现在支持三种不同的相机驱动程序:仿真,Raspberry Pi和OpenCV。
- 为了使无需编辑代码即可更轻松地选择要使用的驱动程序,
Flask服务器会寻找一个CAMERA环境变量来知道要导入的类。
- 该变量可以设置为pi或opencv,如果未设置,则默认使用仿真相机。
from importlib import import_module
import os
# import camera driver
if os.environ.get('CAMERA'):
Camera = import_module('camera_' + os.environ['CAMERA']).Camera
else:
from camera import Camera
这种实现的方式是相当通用的
- 无论
CAMERA
环境变量的值是多少 - 服务器都希望驱动程序位于名为的模块中
camera_$CAMERA.py
。 - 服务器将导入此模块,然后
Camera
在其中寻找一个类。
十、性能改进
几次观察到的另一个结果是服务器消耗大量CPU。
- 这样做的原因是,后台线程捕获帧与将这些帧馈送到客户端的生成器之间没有同步。
- 两者都尽可能快地运行,而不考虑对方的速度。
为了避免在生成器中添加事件处理逻辑,我决定实现一个自定义事件类:
- 该类使用调用者的线程ID为每个客户端线程自动创建和管理一个单独的事件。
- 老实说,这有点复杂,但是这个想法来自于Flask的上下文局部变量是如何实现的。
- 新的事件类叫做
CameraEvent
,并拥有wait()
,set()
和clear()
方法。
在此类的支持下,可以将速率控制机制添加到BaseCamera
该类中:
class CameraEvent(object):
# ...
class BaseCamera(object):
# ...
event = CameraEvent()
# ...
def get_frame(self):
"""Return the current camera frame."""
BaseCamera.last_access = time.time()
# wait for a signal from the camera thread
BaseCamera.event.wait()
BaseCamera.event.clear()
return BaseCamera.frame
@classmethod
def _thread(cls):
# ...
for frame in frames_iterator:
BaseCamera.frame = frame
BaseCamera.event.set() # send signal to clients
# ...
在CameraEvent
该类中完成的魔术使多个客户端能够分别等待新的帧。
- 该
wait()
方法使用当前线程ID为每个客户端分配一个单独的事件对象,然后等待它。 - 该
clear()
方法将重置与调用方的线程ID相关联的事件,以便每个生成器线程可以其自己的速度运行。 set()
摄像头线程调用的方法将信号发送给为所有客户端分配的事件对象,并且还将删除其所有者不提供的任何事件,因为这意味着与这些事件关联的客户端已关闭连接,并且消失了。
进行这些更改之后,同一流将消耗大约3%的CPU
在这两种情况下,只有一个客户端查看该流。
对于单个客户端,OpenCV驱动程序从大约45%的CPU降低到了12%,每个新客户端增加了大约3%。
[参考——reference]
article video streaming with Flask :http://blog.miguelgrinberg.com/post/video-streaming-with-flask
follow-up Flask Video Streaming Revisited.:https://blog.miguelgrinberg.com/post/flask-video-streaming-revisited
GitHub:https://github.com/miguelgrinberg/flask-video-streaming