参考资料
在 Flask 里产生流式响应
使用 multipart/x-mixed-replace 实现 http 实时视频流
使用 Flask 进行视频流传输
重新审视 Flask 视频流

目录

  • 流媒体
  • Flask实现流传输
    • multipart Response
    • 构建实时视频流服务器
    • 从相机中获取帧
      • 有线程时才运行照相机
      • 相机基类BaseCamera
    • 上一个版本存在的问题

流媒体

流式传输是一种技术,其中服务器以块的形式提供对请求的响应。流媒体有两大特点:
1、large response(即数据量大) :对于非常大的响应,只需要在内存中组装响应才能将其返回给客户端可能效率低下。另一种方法是将响应写入磁盘,然后使用 返回文件flask.send_file(),但这会增加 I/O。假设数据可以分块生成,提供小部分响应是一个更好的解决方案。
2、实时数据 :对于某些应用程序,请求可能需要返回来自实时源的数据。一个很好的例子是实时视频或音频馈送。许多安全摄像头使用这种技术将视频流式传输到网络浏览器。

Flask实现流传输

流式传输的实现是采用 生成器函数(yeild关键字) ,那么 HTTP 客户端将会得到这个迭代器每次迭代的结果一部分,迭代器产生多少客户端收到多少,就像流一样 。将一个生成器包装在Response类中返回。下面就展示了一个简单的流式输出,当输入http://localhost:5000/foo地址时,页面会分别输出两个字符串。

返回流式响应的路由需要返回一个Response用生成器函数初始化的对象。然后 Flask 负责调用生成器并将所有部分结果作为块发送给客户端。

@app.route('/foo')
def foo():def generate():yield 'first part'sleep(3)yield 'second part'return Response(generate(), direct_passthrough=True)

这样,当从数据库中查询大量的数据并返回时,我们不需要消耗大量的内存来存储数据并返回,而是生成一个迭代器来流式传输,这样Python 进程中的内存消耗将不会因为必须组装一个大的响应字符串而变得越来越大。

multipart Response

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

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

有了视频帧之后,接下来的问题就是如何传输到客户端,这里有很多成熟的传输技术,包括: HLS、RTSP、RTMP等。这些技术有一定的复杂性,各自有其适用场景,如果业务场景对实时性、性能没有太高要求,那显得有点牛刀杀鸡了。有一个更简单,对前端更友好的方案: http 的 multipart 类型。

multipart 通过 content-type 头定义。这里稍微解释一下,content-type 用于声明资源的媒体类型,浏览器会根据媒体类型的值做出不同动作。 比如,通常来说,chrome 遇到application/zip会下载资源;遇到application/pdf会启动预览,正是通过判断这个头部做出的分支选择。

而 multipart 类型值声明服务器会将 多份数据 合并成当个请求。比较常见的例子是 form 表单提交,浏览器默认的 form 表单提交行为就是通过指定 content-type: multipart/form-data; boundary=xxx 头,服务器接收到后会根据 boundary 分割内容,提取多个字段。 规范文档 rfc1341 指定了四种子类型:multipart/mixed、multipart/alternative、multipart/digest、multipart/parallel,主流浏览器则扩展了一种新的类型: multipart/x-mixed-replace (不过由于很少用到这个特性,而且实现上容易出安全问题,MDN 已经标志为过期特性),该类型声明 body 内容由多份 XML 文档按序组合组合而成,每次到来的文档都会被用于创建新的 dom 文档对象,并触发文档对象 onload 事件。

下面是一个multipart的响应实例:

HTTP/1.0 200 OK
Content-Type: multipart/x-mixed-replace; boundary=gc0p4Jq0M2Yt08jU534c0p
X-Request-ID: bcd9f083-af7a-4419-94bd-0e47851a542d
Date: Tue, 12 Mar 2019 05:04:39 GMT--gc0p4Jq0M2Yt08jU534c0p
Content-Type: text/html<html><body>0</body></html>--gc0p4Jq0M2Yt08jU534c0p
Content-Type: text/html<html><body>1</body></html>...

与常见的 http 响应相比,上例有两个特点。第一,在 header 中并没有指明 content-length 头,客户端无法预知资源大小 ,按规范,在这条 TCP 连接中断之前所传输过来的数据都是本次响应的内容,这个特性可以用于构建一个持久、可扩展的响应流,非常契合实时视频传输场景 。第二点是,response 的 body 部分由多份资源按序排列 而成,并使用 boundary 字符串标志资源的分割点 ,客户端可以使用 boundary 字符串抽取、解析出每一份资源的内容。

简单的HTTP响应传输视频帧,存在一些问题

  • OpenCV 编解码效率并不高,替代方案是 FFMPEG
  • multipart/x-mixed-replace 是单次 http 请求-响应模型,如果网络中断,会导致视频流异常终止,必须重新连接
  • 无法同时输出音频
  • 针对专业、高性能要求的场景,建议还是使用专用协议,如 HLS、RTSP 等

浏览器处理multipart/x-mixed-replace请求时,会使用当前的块数据替换之前的块数据。这刚好就是我们想要的流媒体的效果。我们可以把媒体的一帧数据打包为一个数据块,每块数据有自己的Content-Type和可选的Content-Length。浏览器逐帧替换,就实现了视频的播放功能。

构建实时视频流服务器

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

下面就是一个非常简单完整的web推流服务器,它可以提供Motion JPEG流:

from flask import Flask, render_template, Response
from camera import Cameraapp = 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位于图像标记的属性中。浏览器将通过在其中显示 JPEG 图像流来自动更新图像元素,因为大多数/所有浏览器都支持multipart response。

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

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

从相机中获取帧

有线程时才运行照相机

当第一个客户端连接到流时,从摄像头捕获视频帧的后台线程就开始了,但它永远不会停止。处理此后台线程的更有效方法是仅在有线程时运行它,以便在没有人连接时关闭相机。

具体做法是:这个想法是每次客户端访问帧时,都会记录该访问的当前时间。相机线程检查这个时间戳,如果它发现它超过十秒就退出。通过此更改,当服务器在没有任何客户端的情况下运行十秒钟时,它将关闭其摄像头并停止所有后台活动。一旦客户端再次连接,线程就会重新启动。

下面是程序部分实现:

class Camera(object):# ...last_access = 0  # time of last client access to the camera# ...def get_frame(self):Camera.last_access = time.time()# ...@classmethoddef _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 threadif time.time() - cls.last_access > 10:breakcls.thread = None

相机基类BaseCamera

我们需要使这个流服务器能够适用于不同的流(图片,视频,相机,树莓派等),可以将执行所有帧后台处理的通用功能移至基类,只留下从相机获取帧的任务以在子类中实现 ,下面是该基类的实现:

class BaseCamera(object):thread = None  # background thread that reads frames from cameraframe = None  # current frame is stored here by background threadlast_access = 0  # time of last client access to the camera# ...@staticmethoddef frames():"""Generator that returns frames from the camera."""raise RuntimeError('Must be implemented by subclasses.')def get_frame(self):"""Return the current camera frame."""BaseCamera.last_access = time.time()# wait for a signal from the camera threadBaseCamera.event.wait()BaseCamera.event.clear()return BaseCamera.frame@classmethoddef _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 threadif time.time() - BaseCamera.last_access > 10:frames_iterator.close()print('Stopping camera thread due to inactivity.')breakBaseCamera.thread = None

这样 我们可以在不同子类中实现具体的获取帧的方法frames(),该方法就是一个生成器,里面含有yield关键字,将获取的帧数据转化为byte进行返回,如果需要进行图片的检测,也是在这一方法中进行

例如从笔记本自带摄像头获取帧

import cv2
from base_camera import BaseCameraclass Camera(BaseCamera):@staticmethoddef 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 ityield cv2.imencode('.jpg', img)[1].tobytes()

其中cv2.VideoCapture(0)中的0就表示从本地摄像头获取cap。

上一个版本存在的问题

多次观察到的另一个观察结果是服务器消耗大量 CPU。原因是后台线程捕获帧和生成器将这些帧提供给客户端之间没有同步。两者都尽可能快地跑,而不考虑对方的速度。

通常,后台线程尽可能快地运行是有意义的,因为您希望每个客户端的帧速率尽可能高。但是您绝对不希望向客户端传送帧的生成器以比相机生成帧更快的速度运行,因为这意味着将向客户端发送重复的帧。(因为从上面BaseCamera的get_frame()函数可以看出,每次get_frame都是从一个类变量frame中获取的,而该frame的生成是在生成器中,如果客户端那边调用get_frame的速度大于生成器的生成速度,就会返回重复的图片) 虽然这些重复不会造成任何问题,但它们会增加 CPU 和网络使用率而没有任何好处。

所以需要有一种机制,生成器只将原始帧传递给客户端,如果生成器内部的传递循环比相机线程的帧速率快,那么生成器应该等待新帧可用,以便它自己调整速度以匹配相机速率。另一方面,如果传递循环的运行速度比相机线程慢,那么它在处理帧时永远不会落后,而是应该跳过帧以始终传递最新的帧。

我想要的解决方案是让相机线程在新帧可用时向正在运行的生成器发出信号。然后,生成器可以在等待信号时阻塞,然后再传送下一帧。在查看同步原语时,我发现threading.Event是与这种行为相匹配的。所以基本上,每个生成器都应该有一个事件对象,然后相机线程应该向所有活动的事件对象发出信号,以在新帧可用时通知所有正在运行的生成器。生成器传递帧并重置它们的事件对象,然后返回以再次等待下一帧。

为了避免在生成器中添加事件处理逻辑,我决定实现一个定制的事件类,它使用调用者的线程 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 threadBaseCamera.event.wait()BaseCamera.event.clear()return BaseCamera.frame@classmethoddef _thread(cls):# ...for frame in frames_iterator:BaseCamera.frame = frameBaseCamera.event.set()  # send signal to clients# ...

在CameraEvent类中完成的函数使多个客户端能够单独等待新帧。该wait()方法使用当前线程 id 为每个客户端分配一个单独的事件对象并等待它。该clear()方法将重置与调用者线程 id 关联的event,以便每个生成器线程可以以自己的速度运行。set()相机线程调用的方法向为所有客户端分配的事件对象发送一个信号,并且还将删除其所有者未提供服务的任何event,因为这意味着与这些event关联的客户端已关闭连接并且消失了。

在BaseCamera的_thread()函数中,当执行一次BaseCamera.frame = frame后,说明出现了一张新的帧,这时调用set()方法发送信号,此时在get_frame()函数中的wait()阻塞的线程继续执行,将该新的帧return回去,这样就能将保证在get_frame()快的时候,先让他等待知道有新的帧出现。如果是_thread中相机线程更快,那么每次get_frame()都是最新的帧,而不必担心重复问题。而在get_frame()方法中的clear()表示将信号清除,这样每次在event.set()时判断是否event有信号isset()来判断客户端连接是否断开,如果没有信号,说明get_frame()中执行了wait()和clear(),表示客户端正在等待新的帧出现;如果是有信号isset()的状态,说明要么是客户端那边处理上一张帧的速度慢还没有执行get_frame(),要么是该客户端已经断开连接,如果这个时间超过了阈值5秒,那么就remove掉这个event。

为了让您了解性能改进的幅度,请考虑模拟相机驱动程序在此更改之前消耗了大约 96% 的 CPU,因为它不断以远高于每秒生成一帧的速率发送重复帧。经过这些改动后,同样的流消耗了大约 3% 的 CPU。在这两种情况下,都有一个客户端查看流。对于单个客户端,OpenCV 驱动程序的 CPU 使用率从大约 45% 下降到 12%,每个新客户端增加了大约 3%。

Flask视频流传输相关推荐

  1. python实现流媒体传输_基于OpenCV的网络实时视频流传输的实现

    很多小伙伴都不会在家里或者办公室安装网络摄像头或监视摄像头.但是有时,大家又希望能够随时随地观看视频直播. 大多数人会选择使用IP摄像机(Internet协议摄像机)而不是CCTV(闭路电视),因为它 ...

  2. flask 视频流直播

    flask 视频流直播 本文将介绍如何本地通过浏览器查看远端服务器的摄像头采集到的视频. 服务端 实现实时视频流式传输主要采用服务器推送技术. 服务器在响应请求时,HTTP使用MIME报文格式来封装数 ...

  3. 【Python】基于OpenCV与UDP实现的视频流传输

    文章目录 前言 原理 代码 服务端 客户端 运行效果 参考资料 前言 2021年电赛的测量题(如下)需要实现局域网视频传输,我们的方案是使用gst-rtsp-server 搭建 RTSP 服务器 进行 ...

  4. 【GStreamer】基于NTP+SEI的视频流传输时延测量

    [GStreamer]基于NTP+SEI的视频流传输时延测量 本文以H.264视频流为例,用GStreamer实现插入和提取SEI(Supplemental Enhancement Informati ...

  5. 基于视频流传输 — 在线教育白板技术

    在线教育不同于线下教育, 内容需要经过电子白板展现给用户,如何做出优秀的在线教育白板成为研究的重点.本文来自学而思网校客户端架构负责人赵文杰在LiveVideoStackCon 2018大会上的分享, ...

  6. 第6季2:基于RTSP协议的实时视频流传输的源码分析

    以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除. 前言 博文第一季2:HI3518EV200的初体验中,所提供的测试文件sample_venc实现了基于RTSP协议的实时视频流传输功能. ...

  7. (超)低延迟视频流传输的未来

    作者:Anthony Dantard 翻译:Alex 技术审校:袁荣喜 ▲扫描图中二维码了解音视频技术大会更多信息▲ 影音探索 #013# 用户对服务的期望在不断攀升,并逐渐出现了不满情绪.由于有了Y ...

  8. ZED相机使用记录(一):利用ZED SDK使用python完成局域网内的远程视频(视频流)传输

    ** 本文主要介绍ZED2相机以及具有的功能,ZED2相机(这里使用ZED2相机,主要是因为视频流传输功能目前只有ZED2.ZED mini等新版本相机才有的功能)** 本文所使用的环境: pytho ...

  9. 基于 OpenCV 的网络实时视频流传输

    作者 | 努比 来源 | 小白学视觉 大多数人会选择使用IP摄像机(Internet协议摄像机)而不是CCTV(闭路电视),因为它们具有更高的分辨率并降低了布线成本.在本文中,我们将重点介绍IP摄像机 ...

最新文章

  1. 庆祝自己过了ACP!!
  2. 云端服务器如何调整分机显示,云电话总机分机设置_Enjoytalk云通信
  3. 使用poi进行excel导入并解析插入数据库
  4. 微处理器 微型计算机系统,作业答案11微处理器微型计算机和微型计算机系统三者之间.DOC...
  5. dlink虚拟服务器端口转发,D-Link路由器端口转发怎么设置【图文教程】
  6. 2022快手春节集卡活动 集好运中国福活动攻略
  7. Algorithms Lecture 1 -- Introduction to asymptotic notations【渐进表示法】​​
  8. 重装系统 winserver2008 R2 激活以及优化
  9. swot分析法案例_项目型销售案例剖析的五大步骤
  10. 世预赛:12强赛首战国足0-3不敌澳大利亚,下一场面对日本队国足会如何调整?
  11. 阿里巴巴校招一道笔试题
  12. 户外直播 4G/5G户外高清直播 5G视频回传
  13. MATLAB 对多个数据自动寻峰/能谱图自动寻峰
  14. 音视频基本概念和FFmpeg的简单入门(新手友好+FFmpeg资料分享)
  15. 转自博客园:http://www.cnblogs.com/txw1958/p/wechat-tutorial.html
  16. 视频剪裁尺寸和裁剪时间
  17. 关于城市旅游的HTML网页设计——(旅游风景云南 5页)HTML+CSS+JavaScript
  18. 服务器宕机原因有哪些?服务器宕机解决方案
  19. Yukon中椭圆弧对象的使用方法
  20. 2023年申请发明专利的重要性和注意问题。

热门文章

  1. 超全!9种PCB表面处理工艺大对比
  2. MFC(九)编辑框的控件
  3. mean teacher
  4. 高数 06.03 积分习题课01不定积分
  5. NFS高可用方案:NFS+keepalived+Sersync
  6. 【人脸表情识别】基于图片的人脸表情识别,基本概念和数据集
  7. 自定义View实现通讯录和索引联动,如丝般顺滑
  8. 香港市场人民币IPO为何吸引眼球?
  9. 各浏览器对常用或者错误的 Content-Type 类型处理方式不一致
  10. android gc,Android 内存 - 垃圾回收(GC)机制