学校小学期要整带入侵检测的视频监控,其中难点之一就是从摄像头拿流在前端播放。摄像头是海康的,拿的是rstp流,有点小麻烦。网上冲浪了一会,现成的方案大概有那么几种:
- 转成rtmp协议流,前端需要有flash。哈哈,flash爪巴;
- 依靠浏览器插件或者本地插件。比如海康自己就整了个本地插件,播放直播的时候用插件播放界面覆盖在一片黑屏上。浏览器插件的话也就vlc啥的,不仅过时,而且不太优雅;
- 转成hls流,直接塞进video标签,就是延迟堪忧;
- 转成flv流,然后用flvjs或者基于此的mpegts播放,也就是通过mse接口做一些必要的操作,然后把数据塞进video标签;
- 自己实现websocket流喂给video标签,或者简单粗暴放一个img标签(之前折腾opencv+flask时用的方法,太暴力了);
- 用WebRTC技术弄类似视频聊天室这样的东西,当然也有现成的实现;
当然,一开始让我整转流我是拒绝的,这意味着要加服务器,或者增加原本做图形计算的后端的压力,所以最开始想的是直接前端收rtsp流然后转码。
ffmpeg.wasm纯前端
见https://github.com/ffmpegwasm/ffmpeg.wasm
ffmpeg.js目前只有webm和mp4两类build,只能放弃。但是ffmpeg.wasm不一样,ffmpeg.wasm顾名思义是一个用了WebAssembly技术的好东西,性能非常好,功能非常多,于是我赶紧试了一下:
const VideoBlock: React.FC<{
source: string;
}> = ({source}) =>{
return (
<div>
<video id="player" controls src={source}></video>
</div>
)
}
const LiveStream = (): React.ReactNode => {
const transcodeRtspToPlay = async () => {
try{
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
log: true,
corePath: 'https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js',
});
await ffmpeg.load();
await ffmpeg.run(
'-re',
'-i',
'rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov',
'-vcodec',
'copy',
'-acodec',
'copy',
'-f',
'flv',
'test.flv',
);
const data = ffmpeg.FS('readFile', 'test.flv');
const videoUrl = URL.createObjectURL(new Blob([data.buffer], { type: 'video/flv' }));
ReactDOM.render(
<VideoBlock source={videoUrl}></VideoBlock>,
document.getElementById('video-block-container')
);
}catch (error){
message.error(error.message)
}
};
笑死,根本不能用。
翻了一下issue,说是不能接受rstp/rtmp流输入:
https://github.com/ffmpegwasm/ffmpeg.wasm/issues/67#issuecomment-721452717
Right now rtsp/rtmp is not supported due to the external libraries is unable to integrate with ffmpeg.wasm, I am still looking for possibilities.
不过这个功能呼声还挺高的(https://github.com/ffmpegwasm/ffmpeg.wasm/issues/61#issuecomment-620948390),希望未来可以加入吧。
(同时还发现了一个挺有意思的东西,JSMpeg – MPEG1 Video & MP2 Audio Decoder in JavaScript,在这里马克一下,有空再看吧)
ffmpeg转推HLS流+video标签
hls本质上就是一堆切片加一个不断更新的播放列表文件。前端没啥好说的,收一个m3u8就行,video标签直接就可以放,后端用python直接启动ffmpeg然后进行一波流的推,搭一个nginx反代就完事了。延迟很高,20s左右,优化了一波ffmpeg的参数(i帧间隔啊、切片间隔啊、缓存数啊啥的)可以到6s左右。这里需要看一下ffmpeg的文档,稍微了解一下h264有关视频i帧p帧b帧和gop之类的知识。
因为延迟太高,放弃。
ffmpeg转推FLV流+mpegts.js
转推FLV流的过程和HLS类似,nginx有个模块叫http-flv-module,非常好用,但是需要自己编译进去。linux下倒是好说,问题是小组内全是win,我的机器虽然有kubuntu但是是双系统,不想折腾wsl。阿里云的服务器也早就白嫖干净了。本着能省就省的精神,我毅然决然的走上了用win编译win版nginx的不归路。
前略,中略,后略,总之就是非常蛋疼,浪费了好几个钟头的人生…
这里放一下nginx的配置:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 8080;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location /live {
flv_live on; #打开 HTTP 播放 FLV 直播流功能
chunked_transfer_encoding on; #支持 'Transfer-Encoding: chunked' 方式回复
add_header 'Access-Control-Allow-Origin' '*'; #添加额外的 HTTP 头
add_header 'Access-Control-Allow-Credentials' 'true'; #添加额外的 HTTP 头
}
}
}
rtmp_auto_push on;
rtmp_auto_push_reconnect 1s;
rtmp_socket_dir /tmp;
rtmp {
out_queue 4096;
out_cork 8;
max_streams 128;
timeout 10s;
drop_idle_publisher 10s;
log_interval 5s; #log 模块在 access.log 中记录日志的间隔时间,对调试非常有用
log_size 1m; #log 模块用来记录日志的缓冲区大小
server {
listen 1935; #默认的 1935 端口
application flv {
live on;
gop_cache on; #打开 GOP 缓存,减少首屏等待时间
}
# application hls {
# live on;
# hls on;
# hls_path /tmp/hls;
# }
# application dash {
# live on;
# dash on;
# dash_path /tmp/dash;
# }
}
}
其实也就是按照http-flv-module给的文档xjb配(注意跨域头),然后开ffmpeg往nginx给的地址端口推由rtsp流转换的flv流即可。
前端部分一开始用的flvjs,延迟还是不太理想,大概2s左右。后来翻到了mpegts.js,延迟就给干到了500ms,舒服。
这里直接贴一手播放器组件的完整代码,没用函数式用了类式组件:
import Mpegts from 'mpegts.js';
import React from 'react';
interface MpegtsVideoProps {
mediaDataSource: Mpegts.MediaDataSource;
config?: Mpegts.Config;
autoPlay?: boolean;
controls?: boolean;
onProgress?: (player?: Mpegts.Player, video?: React.RefObject<HTMLVideoElement>) => void;
onPlay?: (player?: Mpegts.Player, video?: React.RefObject<HTMLVideoElement>) => void;
onPause?: (player?: Mpegts.Player, video?: React.RefObject<HTMLVideoElement>) => void;
onPlayerLoadingComplete?: (player?: Mpegts.Player) => void;
className?: string;
}
class MpegtsVideo extends React.Component<MpegtsVideoProps> {
videoRef = React.createRef<HTMLVideoElement>();
player = Mpegts.createPlayer(this.props.mediaDataSource, this.props.config);
isPlayerInit = false;
componentDidMount() {
if (this.videoRef.current && Mpegts.isSupported() && !this.isPlayerInit) {
this.player.attachMediaElement(this.videoRef.current);
this.player.load();
this.player.play();
this.player.on(Mpegts.Events.ERROR, () => {
this.isPlayerInit = false;
});
this.player.on(Mpegts.Events.LOADING_COMPLETE, () => {
this.isPlayerInit = true;
if (this.props.onPlayerLoadingComplete) this.props.onPlayerLoadingComplete(this.player);
});
}
}
componentWillUnmount() {
if (this.isPlayerInit) {
this.player.detachMediaElement();
this.player.pause();
this.player.unload();
this.player.destroy();
this.isPlayerInit = false;
}
}
render() {
return (
<div>
<video
className={this.props.className}
ref={this.videoRef}
onProgress={() => {
if (this.props.onProgress) this.props.onProgress(this.player, this.videoRef);
}}
autoPlay={this.props.autoPlay}
controls={this.props.controls}
onPlay={() => {
if (this.props.onPlay) this.props.onPlay(this.player, this.videoRef);
}}
onPause={() => {
if (this.props.onPause) this.props.onPause(this.player, this.videoRef);
}}
></video>
</div>
);
}
}
export default MpegtsVideo;
什么?你问我为什么不直接按照mpegts给的播放器api来?因为懒
到这里其实已经可以实现想要的效果了,因为小学期一共就两周,而且这之前完全没碰过react,自己慢慢摸索React的函数式组件和各种生命周期hook就花了不少时间,所以也没时间探索其他方案了…个人比较在意webrtc的实现,也翻了不少方案,不过还是等有空再说吧