最近参与一个项目,硬件设备从摄像头采集视频直播流程,分析结果,与画面一起展示在前端页面上。
环境: 硬件设备是一个集成了显卡的开发板,可以运行ubuntu,算法部分是用python写的,前端设备在同一个局域网。摄像头为海康威视的普通网络摄像头。
关于视频画面播放的问题, 查了一下,前端目前不能直接播放rtsp
视频流。
调研了市面上有几种方案。
ffmpeg
视频转格式为frag_keyframe
的mp4视频流,也就是视频流切片, 使用websocket
转发流,前端使用Media Source Extensions
渲染- 普通的mp4格式无法播放,需要转成
fragment mp4
即分片的mp4, 目前这种格式浏览器支持度差,这个方案也没测试成功。
- 普通的mp4格式无法播放,需要转成
ffmpeg
视频转格式为flv
视频流,websocket
转发流,前端使用flv.js
渲染- 这个方案比较靠谱, 可以参考知乎这篇HTML5 播放 RTSP 视频,没有尝试。
- 这个方案的底层也是使用
Media Source Extensions
渲染,只不过格式是使用flv
ffmpeg
视频流切片成m3u8
存成文件, 前端video
标签直接播放文件地址或hls.js
渲染- 这个方案严重的问题是延迟太高,至少会延迟一个切片文件的时间,可以播放成功。
服务端视频流截图,保持连接,接口返回
content-type: multipart/x-mixed-replace
图片,前端使用img
标签加载图片地址- 这个方案是意想不到的一个方案,很神奇,不清楚会不会造成内存泄露。
转
rtmp
视频流,浏览器使用flash插件- flash不行,没有尝试
使用
webrtc
,点对点直接播放视频- 方案没尝试,点对点应该是浏览器对浏览器之间,服务端这边需要启一个webrtc的客户端处理,
上述几个方案
m3u8
是Apple
主推的技术方案,目前录播用多,直播用的少,应该以后会发展。
flv.js
是目前最好的直播解决方案,延迟也非常好。
这两种需要系统依赖ffmpeg
。但是结合项目实际,最终采用了另外一种方式:
服务端视频流截图,使用
websocket
转发图片帧二进制内容, 前端渲染到canvas
不需要安装系统依赖
一个问题是带宽占用高,另外是没有声音。但是实现起来简单,不需要系统级的依赖安装,对于当前项目只显示实时画面的场景已经够用了。
0. 先构造一个rtsp视频流
需要依赖docker
与ffmpeg
使用docker
启动一个rtsp服务,再使用ffmpeg
将视频./test.mp4
往服务写入,就可以得到一个不停循环播放视频的rtsp视频流
1 | docker run --rm -it --network=host -e RTSP_PROTOCOLS=tcp aler9/rtsp-simple-server |
测试播放
1 | ffplay -rtsp_transport tcp rtsp://localhost:8554/mystream |
这样我们就构造了一个可以读取视频的直播流了
1. websocket + opencv截图方案
1. 关于python中使用websocket
要做到数据实时发送,最好的方式就是用websocket
,结合的最好的语言是nodejs
。
可惜当前项目是用python
写的,python-socketio
由于性能的原因,已经不推荐使用flask
之类的多线程http框架
经过非常多的试错(可坑死我了),最终选择了aiohttp
+ AsyncServer
的模式
安装相关依赖
1 | pip3 install aiohttp python-socketio opencv-python |
2. 服务端原型代码实现
1 | import cv2, asyncio |
关于多进程
中间遇到很多坑,关于事件循环的,关于异步函数的。`socketio`要发送数据到客户端必须要在同一个事件循环中。跨线程的可以用`run_coroutine_threadsafe`将数据发送到主事件循环中,但是跨进程就没办法了。
由于python本身的限制,算法部分是多进程的。 于是最终的方案是再加一层进程的消息队列进行进程间的数据传递。
为了避免消费消息队列阻塞事件循环,又起了一个线程消费消息队列。
3. 前端部分实现
index.html
:
1 |
|
前端部分较为简单。有关canvas
渲染的部分可以使用requestAnimationFrame
进行优化。
浏览器打开http://localhost:8080/
可以看到视频播放
2. 使用multipart/x-mixed-replace
传输图片方案
这个方案很有意思, 相当于是将每帧画面用同一个连接返回给前端。后一帧会将前一帧的内容替换掉。前端无需js参与处理。
前端也比较简单,直接包含在代码里了。 服务端代码:
1 | from flask import Flask, render_template, Response |
方案有个问题就是前端的GET请求会一直连接着,浏览器如果刷新或者中断,服务端会报错。另外不知道会不会有什么其他风险。
浏览器打开http://localhost:8080/
可以看到视频播放
3. 使用ffmpeg
切片m3u8
, 前端video
标签直接播放
这个方式应该是目前实际使用最多的方案,各大直播以及普通视频播放都使用了m3u8
。
直播延迟的长度和切片有关,可以做到几秒级的。
1. 创建m3u8
文件及切片
仅保留最近5个文件,单个切片时长4秒。实际上这两个参数都不准确,ffmpeg
会根据视频关键帧自动设置切片时长。
1 | ffmpeg -rtsp_transport tcp -i rtsp://localhost:8554/mystream -c:v copy -hls_time 4 -hls_list_size 5 -hls_flags delete_segments -f hls ./tmp/index.m3u8 |
2. 前端文件
实际上并不是所有浏览器都原生都支持m3u8
格式, 可以使用hls.js
做兼容。
1 | <script src="https://unpkg.com/hls.js@latest"></script> |
4. 总结
方式 | 系统依赖 | 延迟 | 视频声音 |
---|---|---|---|
websocket + flv.js | ffmpeg | 低 | 有 |
websocket + 关键帧 | - | 低 | 无 |
m3u8 | ffmpeg | 中等 | 有 |
x-mixed-replace + 关键帧 | - | 低 | 无 |
大概是这样