Node.js + FFmpeg 拉取海康行车记录仪 RTSP 流并 WebSocket 转发至网页播放
在 Web 页面中实时显示海康威视行车记录仪摄像头视频流,常见方案是利用海康云平台做转发和存储,再通过 SDK 截取流。本文介绍一种经实际验证的 Node.js 方案:通过海康汽车电子云 API 获取设备预览 URL(RTSP 地址),再使用 FFmpeg 将 RTSP 流转码为 FLV 格式,最后通过 WebSocket 推送到前端 flv.js 播放。该方案支持多通道并发,具有可调节的缓冲延时。// 海康汽车设备 SDK 封装类
// 用于获取设备预览 URL、录像列表和回放地址
const crypto = require('crypto');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const querystring = require('querystring');
class CarSDKApp {
constructor() {
this.ACCESS_KEY = "****************";
this.ACCESS_SECRET = "**************";
this.SIGNATURE_METHOD = "HMAC-SHA1";
this.DEFAULT_CHARSET = "UTF-8";
this.REGION_ID = "cn-hangzhou";
this.VERSION = "2.1.0";
this.BASE_URL = "https://open.hikvisionauto.com:14021/v2/";
}
getBaseParams() {
const params = {
SignatureMethod: this.SIGNATURE_METHOD,
SignatureNonce: uuidv4(),
AccessKey: this.ACCESS_KEY,
Timestamp: Date.now().toString(),
Version: this.VERSION,
RegionId: this.REGION_ID
};
return Object.keys(params).sort().reduce((acc, key) => {
acc = params;
return acc;
}, {});
}
specialUrlEncode(value) {
return querystring.escape(value)
.replace(/\+/g, '%20')
.replace(/\*/g, '%2A')
.replace(/%7E/g, '~');
}
getStringToSign(params, method) {
const sortedParams = Object.keys(params).sort().reduce((acc, key) => {
acc = params;
return acc;
}, {});
const sortQueryStringTmp = Object.entries(sortedParams)
.map(() => `&${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
.join('');
return `${method}&${this.specialUrlEncode('/')}&${this.specialUrlEncode(sortQueryStringTmp.substring(1))}`;
}
sign(accessSecret, params, method) {
const stringToSign = this.getStringToSign(params, method);
const hmac = crypto.createHmac('sha1', accessSecret);
hmac.update(stringToSign);
return hmac.digest('base64');
}
async preview(deviceCode, channelID) {
const url = this.BASE_URL + "device/preview/";
const BASE_PARAMS = this.getBaseParams();
BASE_PARAMS.deviceCode = deviceCode;
if (channelID !== undefined) {
BASE_PARAMS.channelNo = channelID.toString();
}
BASE_PARAMS.streamType = "0"; // 0:主码流 1:子码流
const sign = this.sign(this.ACCESS_SECRET + "&", BASE_PARAMS, "GET");
const allParams = { ...BASE_PARAMS, Signature: sign };
const queryString = Object.entries(allParams)
.map(() => `${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
.join('&');
const fullUrl = `${url}?${queryString}`;
console.log("sendurl:", fullUrl);
try {
const response = await axios.get(fullUrl);
if (response.data.status !== 0) {
throw new Error(`API Error: ${response.data.msg} (status: ${response.data.status})`);
}
const previewUrl = response.data.data;
console.log("previewUrl:", previewUrl);
return previewUrl;
} catch (error) {
console.error('Error in preview:', error);
throw error;
}
}
async list(deviceCode, startTime, endTime) {
const url = this.BASE_URL + "device/videoList/";
const BASE_PARAMS = this.getBaseParams();
BASE_PARAMS.deviceCode = deviceCode;
BASE_PARAMS.startTime = startTime;
BASE_PARAMS.endTime = endTime;
const sortQueryStringTmp = Object.entries(BASE_PARAMS)
.map(() => `&${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
.join('');
const sign = this.sign(this.ACCESS_SECRET + "&", BASE_PARAMS, "GET");
const fullUrl = url + `?Signature=${this.specialUrlEncode(sign)}${sortQueryStringTmp}`;
try {
const response = await axios.get(fullUrl);
console.log(response.data);
return response.data;
} catch (error) {
console.error('Error in list:', error);
throw error;
}
}
async replay(deviceCode, startTime, endTime, fileSize) {
const url = this.BASE_URL + "device/videoReplay/";
const BASE_PARAMS = this.getBaseParams();
BASE_PARAMS.deviceCode = deviceCode;
BASE_PARAMS.startTime = startTime;
BASE_PARAMS.endTime = endTime;
BASE_PARAMS.fileSize = fileSize.toString();
const sortQueryStringTmp = Object.entries(BASE_PARAMS)
.map(() => `&${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
.join('');
const sign = this.sign(this.ACCESS_SECRET + "&", BASE_PARAMS, "GET");
const fullUrl = url + `?Signature=${this.specialUrlEncode(sign)}${sortQueryStringTmp}`;
try {
const response = await axios.get(fullUrl);
console.log(response.data);
return response.data;
} catch (error) {
console.error('Error in replay:', error);
throw error;
}
}
}
以上代码中,ACCESS_KEY 和 ACCESS_SECRET 需替换为开发者真实密钥。preview() 方法返回一个 RTSP 预览 URL,该 URL 将在后续 FFmpeg 处理中使用。SDK 的签名算法遵循海康汽车云 API 规范,采用 HMAC-SHA1,参数按字典序排序后进行 URL 编码。
接下来是 Node.js 视频流转发服务器,它基于 Express 和 WebSocket 实现。服务器监听 8002 端口,通过车牌号从数据库映射获取设备 ID,调用 SDK 得到 RTSP 地址,然后用 FFmpeg 将其转为 FLV 并通过 WebSocket 管道发送给客户端。
// RTSP 视频流 WebSocket 转发服务器
var express = require("express");
var expressWebSocket = require("express-ws");
var ffmpeg = require("fluent-ffmpeg");
ffmpeg.setFfmpegPath("C:\\ffmpeg-7.1.1-full_build\\bin\\ffmpeg.exe");
var webSocketStream = require("websocket-stream/stream");
var WebSocket = require("websocket-stream");
var http = require("http");
// 车牌号到设备 ID 的映射表(从数据库或配置中获取)
const carToDeviceDict = initFromDB();
function localServer() {
let app = express();
app.use(express.static(__dirname));
expressWebSocket(app, null, {
perMessageDeflate: true
});
app.ws("/rtsp/", rtspRequestHandle)
app.listen(8002);
console.log("express listened")
}
async function rtspRequestHandle(ws, req) {
console.log("rtsp request handle");
const stream = webSocketStream(ws, {
binary: true,
browserBufferTimeout: 1000000
}, {
browserBufferTimeout: 1000000
});
let carID = req.query.carID;
let channel = req.query.channel;
console.log("car number:", carID);
const dict = initFromDB();
const deviceId = dict;
if (!deviceId) {
console.error("Device ID not found for car:", carID);
ws.close();
return;
}
console.log("deviceId:", deviceId, "channel:", channel);
const CarSDKApp = require("./carsdkapp");
const carSDKApp = new CarSDKApp();
try {
const url = await carSDKApp.preview(deviceId, channel);
console.log("url:", url);
ffmpeg(url)
.addInputOption("-rtsp_transport", "tcp", "-buffer_size", "102400")
.addInputOption("-fflags", "nobuffer")
.addInputOption("-flags", "low_delay")
.addInputOption("-strict", "experimental")
.addOutputOption("-f", "flv")
.addOutputOption("-preset", "ultrafast")
.addOutputOption("-tune", "zerolatency")
.addOutputOption("-g", "30")
.addOutputOption("-keyint_min", "30")
.addOutputOption("-sc_threshold", "0")
.videoCodec("libx264")
.noAudio()
.format("flv")
.on("start", function (commandLine) {
console.log("FFmpeg started with command:", commandLine);
})
.on("codecData", function (data) {
console.log("Stream codecData:", data);
})
.on("error", function (err) {
console.log("FFmpeg error:", err.message);
})
.on("end", function () {
console.log("Stream ended");
})
.on("stderr", function (stderrLine) {
console.log("FFmpeg stderr:", stderrLine);
})
.pipe(stream, { end: true });
} catch (error) {
console.log("Error getting preview URL or starting ffmpeg:", error);
}
}
localServer();
注意 FFmpeg 路径需要根据实际安装位置修改。服务器启动后,前端页面通过 WebSocket 连接 ws://localhost:8002/rtsp/?carID=xxx&channel=1 来获取特定通道的 FLV 流。前端页面使用 flv.js 播放,HTML 结构包含六个视频容器和对应的控制按钮,每个视频框内有一个
video
元素和加载提示。
<!-- 页面核心部分 -->
<div class="video-grid">
<div class="video-container">
<div class="video-header">通道 1</div>
<div class="video-box">
<video controls="controls" class="demo-video" id="player1" muted></video>
<div id="loading1" class="loading" style="display: block;">加载中...</div>
</div>
</div>
<!-- 其他通道类似,共6个 -->
</div>
<script src="./flv.js"></script>
<script>
// 播放器初始化逻辑
// 每个通道创建 FlvPlayer 实例,连接对应 WebSocket 地址
// 参考 flv.js 官方文档创建 player
// 并在 onerror/onstop 时重新连接
</script>
该方案的关键在于:通过海康云 API 获取 RTSP 地址(而非直接使用设备 RTSP 地址,避免公网直接暴露),再利用 FFmpeg 转码为 FLV,WebSocket 传输实现了低延迟、多通道并发播放。实测中需要确保 FFmpeg 版本与系统兼容,并根据网络状况调整缓冲区大小和 GOP 间隔。若需要更高实时性,可将
-g
和
-keyint_min
值调低,但会增大带宽消耗。
Re: Node.js + FFmpeg 拉取海康行车记录仪 RTSP 流并 WebSocket 转发至网页播放
这篇方案实现得很扎实,从 API 签名到 FFmpeg 转码再到 WebSocket 推送,整个链路描述清晰,代码封装也规整。对于需要脱离海康云平台、自己在 Web 端直接播放行车记录仪视频的场景,这个思路非常实用。尤其多通道并发和可调缓冲的设计,在实际部署中能灵活应对网络波动。能分享下你选择的 FFmpeg 转码参数(比如 264/265、帧率、关键帧间隔)具体怎么配置来平衡延迟和画质的吗?Re: Node.js + FFmpeg 拉取海康行车记录仪 RTSP 流并 WebSocket 转发至网页播放
感谢楼主的分享,这套方案思路很清晰,直接绕过了海康云平台的限制,用 Node.js + FFmpeg 转码后通过 WebSocket 推流,省去了额外的云存储费用。有几个问题想请教: 1. 文中提到“调节缓冲延时”,实际测试中楼主一般把 FFmpeg 的 `-rtsp_transport tcp` 和 `-re` 参数调到什么值,才能兼顾实时性和流畅度? 2. 海康汽车电子云 API 的 `ACCESS_KEY` 和 `ACCESS_SECRET` 是不是需要先在开放平台申请设备权限?会不会遇到并发拉流被限流的情况? 3. 如果前端 flv.js 在部分浏览器上不支持,有没有考虑过用 HLS 作为备选方案? 再次感谢分享,这套代码可以直接作为工业级项目的基础框架了。Re: Node.js + FFmpeg 拉取海康行车记录仪 RTSP 流并 WebSocket 转发至网页播放
这个方案看起来很实用,特别是绕开了海康云平台的限制,直接用API获取RTSP地址再通过FFmpeg转码,灵活性高了不少。代码里签名和请求的部分也封装得挺清楚,方便复用。想请问一下,在实际测试中,多个通道同时并发拉流时,FFmpeg进程的资源占用大概到什么程度?以及有没有针对网络波动或设备离线做过重连或者保活的处理?
页:
[1]