参考链接
基本使用
这部分可以直接看Video Streaming with Flask的内容,我只是进行了一定魔改。
安装flask与opencv-python
1
| pip install opencv-python flask
|
建立如下目录结构:

app.py
内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import cv2 from flask import Flask, render_template, Response
app = Flask(__name__, static_url_path='/') camera = cv2.VideoCapture(0)
@app.route('/') def index(): return app.send_static_file('index.html')
def gen(): while True: ret, frame = camera.read() frame = cv2.imencode('.jpg', frame)[ 1].tobytes() 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(), mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__': app.run(host='0.0.0.0', debug=True)
|
index.html内容:
1 2 3 4 5 6 7 8 9
| <html> <head> <title>Video Streaming Demonstration</title> </head> <body> <h1>Video Streaming Demonstration</h1> <img src="/video_feed"> </body> </html>
|
启动应用程序,打开网页就能看到视频传输成功:

我的更改主要是使用了静态HTML文件,以摄像头读取的内容作为视频源,其它没什么。
能看到视频就说明成功了。
简要分析
目前来说,我们已经实现了一个简简单单的视频传输流程。但如果只是这样,只要看参考链接就好。
接下来我们要实现不使用浏览器来解析请求。
不过在此之前,让我们分析下这个东西的原理。
让我们看看Flask的日志:

再看看MIME类型中的multipart
。我们可以知道,整个视频传输流程都只是在一次HTTP请求中完成的。再看看MIME类型中的replace
,我们大概可以推断出整个视频传输过程的原理。
与普通的HTTP请求类似,浏览器向/video_feed
发出请求,浏览器返回内容。
不过特殊的地方是浏览器的响应报文,响应报文应该仍然是一个响应头(其中指明了multipart/x-mixed-replace
的返回链接),但由multipart
我们可以知道,响应报文的body部分是由多部分组成的,只要连接不断开,服务器就会往body里面不断增添新的内容。
而浏览器接收到这么多内容是怎么做的呢?注意看x-mixed-replace
中的replace
,其实它是把最新拿到的一部分数据替换掉以前的数据,在我们的例子里就是用新的一帧图像替换掉前一帧图像。
也就是说,multipart/x-mixed-replace
只是告诉浏览器响应body中会不断追加数据,且请浏览器用新的一部分数据替换原来的。而浏览器又通过另一部分Content-Type: image/jpeg
标记来知道每一部分的内容是图像的。
总结:也就是说服务端得到的视频会被转换成一张张jpeg图片,传给浏览器。浏览器不断用新的图片替换以前的图片,就实现了视频的效果。这也就是为什么没法传输音频的原因了。
HTTP内容解析
理解了以上内容,那就可以进行下一步的操作了。
目前来说,浏览器自动帮我们解析了HTTP请求,我们做的只是将<img>
标签的链接指向它。
但我现在有一个需求,如果我想在其它应用程序中使用这种视频流方式该怎么办呢?
很明显,我们也必须实现对这种MIME类型的解析。
不过以我目前搜到的资料来看,好像没有什么现成的库实现了这个功能。
所以我就自己写了一个,不过需要先安装requests
库。
客户端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import cv2 import numpy as np import requests
url = 'http://127.0.0.1:5000/video_feed' res = requests.get(url, stream=True) bytes = b'\r\n' cst = b'\r\n--frame\r\nContent-Type: image/jpeg\r\n\r\n' now = 0 next = -1 for chunk in res.iter_content(chunk_size=1024): bytes += chunk next = bytes.find(cst, now + 1) if -1 != next: bin_data = bytes[now + len(cst):next] image = cv2.imdecode(np.frombuffer(bin_data, np.uint8), cv2.IMREAD_UNCHANGED) cv2.imshow('frame', image) cv2.waitKey(1) bytes = bytes[next:] now = 0 next = -1 res.close()
|
写的很随意,我们知道请求体里面的内容大概会是--frame\r\nContent-Type: image/jpeg\r\n\r\n二进制图片内容\r\n--frame\r\nContent-Type: image/jpeg\r\n\r\n二进制图片内容\r\n--frame\r\nContent-Type: image/jpeg\r\n\r\n二进制图片内容\r\n--frame\r\nContent-Type: image/jpeg\r\n\r\n二进制图片内容
······
可以看到,每一部分的分隔符可以是\r\n--frame\r\nContent-Type: image/jpeg\r\n\r\n
,所以我们可以通过检测这个来分析哪里是下一帧。(最开始的--frame\r\nContent-Type: image/jpeg\r\n\r\n
前没有\r\n
,但我们为了方便起见,就在bytes
里初始化了一个\r\n
)
bin_data
的内容就是图片的二进制内容了,numpy
和cv2
只是为了将它显示出来,如果你不需要,可以删掉。
优点:这种解析方法大概在其它语言里也能用。
缺陷:这个程序是看到下一个\r\n--frame\r\nContent-Type: image/jpeg\r\n\r\n
才知道前一帧的结束位置。所以最后一帧恐怕就没有处理。而且虽然这样写我测试成功了,但我仍然觉得这样写不太好(因为我对HTTP不是很了解,所以可能会有不知道的坑点存在)。
更进一步
目前来说,已经能实现视频的传输了,不过还有两个问题:
- 代码难以编写,其它代码很难和这部分结合
- 存在一个目前还没有提到的累计问题
先说第二个问题,如果你把客户端代码中的cv2.waitKey(1)
改成cv2.waitKey(1000)
,就会看到这个问题,视频基本上是以一种极慢速的方式播放的。
这是因为服务端的代码根本就没有考虑客户端的接收速率,它只是单纯地从摄像头采集一帧,然后塞到响应body里。它的发送速率主要是取决于读取一帧所需要的时间和网络传输所需要的时间。
1 2 3 4 5 6 7
| def gen(): while True: ret, frame = camera.read() frame = cv2.imencode('.jpg', frame)[ 1].tobytes() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
所以,最重要的就是你的应用程序必须能够及时处理,否则你得到的画面可能就不是实时的了。针对这一点,我目前采用了多线程的解决方案:
http_camera.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| import threading import time
import cv2 import numpy as np import requests
from device.camera import Camera
class HttpCamera(Camera): def __init__(self, url): self.__url = url self.__frame = None self.__last_frame = None self.__running_flag = False
def isOpened(self): return self.__running_flag
def open(self): if self.isOpened(): return response = requests.get(self.__url, stream=True)
def tmp(): bytes = b'\r\n' cst = b'\r\n--frame\r\nContent-Type: image/jpeg\r\n\r\n' now_position = 0 try: for chunk in response.iter_content(chunk_size=1024): if not self.__running_flag: break bytes += chunk next_position = bytes.find(cst, now_position + 1) if -1 != next_position: bin_data = bytes[now_position + len( cst):next_position] self.__frame = cv2.imdecode( np.frombuffer(bin_data, np.uint8), cv2.IMREAD_UNCHANGED) bytes = bytes[next_position:] now_position = 0 response.close() except Exception: self.__running_flag = False raise Exception('服务端结束了视频')
self.__running_flag = True threading.Thread(target=tmp).start() return
def read(self): if not self.isOpened(): self.open() while True: if self.__frame is not None and id(self.__frame) != id( self.__last_frame): self.__last_frame = self.__frame return True, self.__frame else: time.sleep(0.001)
def release(self): self.__running_flag = False return
if '__main__' == __name__: camera = HttpCamera('http://127.0.0.1:5000/video_feed') while True: ret, image = camera.read() cv2.imshow('image', image) k = cv2.waitKey(1) & 0xFF if 27 == k: break cv2.destroyAllWindows() camera.release()
|
模仿了cv2.VideoCapture(X)
的接口,提供了open
,isOpened
,read
和release
的接口,感觉上就像一个普通的摄像头一样。
每次调用read
方法得到的都是最新的视频帧。
总结
在本机上进行了测试,使用这种视频流方法的延迟平均在154ms左右,而rtmp的延迟则在700ms以上,总体来说还是较好地满足了我的需求。
缺陷:
- 我自己对自己的代码不太满意,不知道有没有更好的写法。
- 不是很喜欢用python的多线程(个人看法)。
- 没有很好地处理http连接发生错误的情况。
- 解决方案本身缺陷,不是专业的视频推流协议,而且使用了
multipart/x-
的实验性特性,可能存在某些坑点。