海康威视摄像头输出的RTSP流是安防监控领域最常见的视频流格式之一。在实际项目中,我们经常需要将这类摄像头的实时画面集成到Web应用中。不同于普通的HTTP视频流,RTSP协议需要特殊的处理方式才能在浏览器中播放。
先说说我踩过的第一个坑:直接用浏览器打开RTSP地址是行不通的。现代浏览器出于安全考虑,都不支持直接播放RTSP流。这也是为什么我们需要借助FFmpeg这样的工具进行转码。记得第一次对接时,我花了整整两天时间才搞明白这个基本原理。
测试RTSP流是否可用的最简单方法是使用VLC播放器。把摄像头的RTSP地址(类似rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101)直接粘贴到VLC的"打开网络串流"中,如果能正常播放,说明流地址和摄像头配置都没问题。这一步看似简单,但能帮你排除至少50%的初期问题。
FFmpeg的参数配置直接决定了转码效果和延迟表现。经过多次测试,我总结出这套针对海康摄像头的优化参数:
bash复制ffmpeg -rtsp_transport tcp -i "rtsp://摄像头地址"
-f mpegts
-codec:v mpeg1video
-b:v 1000k
-r 25
-vf "scale=1280:720"
-preset ultrafast
-fflags nobuffer
-
几个关键参数解释:
-rtsp_transport tcp:强制使用TCP传输,避免UDP丢包导致的卡顿-preset ultrafast:牺牲压缩率换取最低编码延迟-fflags nobuffer:减少缓冲,实现近乎实时的传输-vf scale:根据前端展示需求调整分辨率在实际部署中,FFmpeg进程可能会意外退出。我的经验是添加以下错误处理逻辑:
javascript复制ffmpeg.on('exit', (code) => {
console.error(`FFmpeg异常退出,代码: ${code}`);
// 自动重启逻辑
setTimeout(restartFFmpeg, 5000);
});
ffmpeg.stderr.on('data', (data) => {
const msg = data.toString();
if (msg.includes('Connection refused')) {
console.error('摄像头连接被拒绝,检查地址和权限');
}
});
WebSocket服务的关键是高效转发FFmpeg输出。这里分享一个经过生产验证的版本:
javascript复制const WebSocket = require('ws');
const { spawn } = require('child_process');
const wss = new WebSocket.Server({ port: 9999 });
wss.on('connection', (ws) => {
console.log('客户端连接建立');
const ffmpeg = spawn('ffmpeg', [
'-rtsp_transport', 'tcp',
'-i', 'rtsp://摄像头地址',
'-f', 'mpegts',
'-codec:v', 'mpeg1video',
'-'
], { stdio: ['ignore', 'pipe', 'pipe'] });
// 数据管道
ffmpeg.stdout.on('data', (data) => {
if (ws.readyState === ws.OPEN) {
ws.send(data, { binary: true });
}
});
// 错误处理
ffmpeg.stderr.on('data', (data) => {
console.error(`FFmpeg错误: ${data}`);
});
// 连接关闭处理
ws.on('close', () => {
ffmpeg.kill();
});
});
当需要支持多路摄像头时,传统的单进程模式会遇到性能瓶颈。我的解决方案是:
javascript复制// 示例:多线程处理
const { Worker } = require('worker_threads');
function createStreamWorker(streamUrl) {
return new Worker('./stream-worker.js', {
workerData: { streamUrl }
});
}
推荐使用@cycjimmy/jsmpeg-player,它的兼容性更好。这是我优化过的播放器组件:
vue复制<template>
<div class="video-container">
<canvas ref="videoCanvas"></canvas>
<div v-if="loading" class="loading-overlay">
<div class="spinner"></div>
</div>
</div>
</template>
<script>
import JSMpeg from '@cycjimmy/jsmpeg-player';
export default {
props: {
wsUrl: {
type: String,
required: true
}
},
data() {
return {
player: null,
loading: true
};
},
mounted() {
this.initPlayer();
},
methods: {
initPlayer() {
this.player = new JSMpeg.Player(this.wsUrl, {
canvas: this.$refs.videoCanvas,
autoplay: true,
onPlay: () => {
this.loading = false;
},
onError: () => {
this.$emit('error');
}
});
}
}
};
</script>
通过实测,我发现以下措施能显著降低端到端延迟:
chunkSize: 1024*512增大网络缓冲区disableGl: true关闭WebGL加速(某些设备上更快)javascript复制// 网络自适应示例
window.addEventListener('online', this.checkNetworkQuality);
window.addEventListener('offline', this.handleDisconnect);
checkNetworkQuality() {
const bitrate = navigator.connection.downlink * 1024;
this.$emit('quality-change', bitrate > 2000 ? 'hd' : 'sd');
}
在实际部署中,必须注意以下安全事项:
javascript复制// 鉴权中间件示例
wss.on('connection', (ws, req) => {
const token = req.headers['sec-websocket-protocol'];
if (!validateToken(token)) {
ws.close(1008, '未授权访问');
return;
}
// ...正常处理逻辑
});
建议部署以下监控措施:
bash复制# PM2启动命令示例
pm2 start stream-server.js --name "video-stream" --max-restarts 5
网络波动导致的断流是常见问题。我的处理方案是:
javascript复制// 前端重连逻辑
reconnect() {
this.player.destroy();
setTimeout(() => {
this.initPlayer();
}, 3000);
}
开发环境下常遇到的跨域问题,可以通过以下方式解决:
nginx复制# nginx配置示例
location /video-ws {
proxy_pass http://localhost:9999;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
对于高并发场景,可以考虑:
bash复制# GPU加速示例
ffmpeg -hwaccel cuda -i rtsp://摄像头地址 -c:v h264_nvenc ...
根据网络状况动态调整码率:
javascript复制// 动态码率调整
adjustBitrate(level) {
const args = {
'low': ['-b:v', '500k', '-r', '15'],
'medium': ['-b:v', '1000k', '-r', '25'],
'high': ['-b:v', '2000k', '-r', '30']
};
this.ffmpeg.kill();
this.startFFmpeg(args[level]);
}
虽然WebRTC延迟更低,但存在以下问题:
HLS的优点是兼容性好,但缺点明显:
为移动端添加手势控制:
javascript复制// 手势控制示例
this.$refs.canvas.addEventListener('touchstart', this.handleTouch);
handleTouch(e) {
if (e.touches.length === 2) {
this.startPinchDistance = getDistance(e.touches);
}
}
移动设备上的省电措施:
javascript复制document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.player.pause();
}
});
最后分享几个实战经验:
javascript复制// 配置热更新示例
fs.watch('config.json', () => {
this.reloadConfig();
});