查看: 267|回复: 1

Node.js + FFmpeg 拉取海康行车记录仪 RTSP 流并 WebSocket 转发至网页播放

[复制链接]
发表于 昨天 12:00 | 显示全部楼层 |阅读模式
在 Web 页面中实时显示海康威视行车记录仪摄像头视频流,常见方案是利用海康云平台做转发和存储,再通过 SDK 截取流。本文介绍一种经实际验证的 Node.js 方案:通过海康汽车电子云 API 获取设备预览 URL(RTSP 地址),再使用 FFmpeg 将 RTSP 流转码为 FLV 格式,最后通过 WebSocket 推送到前端 flv.js 播放。该方案支持多通道并发,具有可调节的缓冲延时。
  1. // 海康汽车设备 SDK 封装类
  2. // 用于获取设备预览 URL、录像列表和回放地址
  3. const crypto = require('crypto');
  4. const axios = require('axios');
  5. const { v4: uuidv4 } = require('uuid');
  6. const querystring = require('querystring');
  7. class CarSDKApp {
  8.   constructor() {
  9.     this.ACCESS_KEY = "****************";
  10.     this.ACCESS_SECRET = "**************";
  11.     this.SIGNATURE_METHOD = "HMAC-SHA1";
  12.     this.DEFAULT_CHARSET = "UTF-8";
  13.     this.REGION_ID = "cn-hangzhou";
  14.     this.VERSION = "2.1.0";
  15.     this.BASE_URL = "https://open.hikvisionauto.com:14021/v2/";
  16.   }
  17.   getBaseParams() {
  18.     const params = {
  19.       SignatureMethod: this.SIGNATURE_METHOD,
  20.       SignatureNonce: uuidv4(),
  21.       AccessKey: this.ACCESS_KEY,
  22.       Timestamp: Date.now().toString(),
  23.       Version: this.VERSION,
  24.       RegionId: this.REGION_ID
  25.     };
  26.     return Object.keys(params).sort().reduce((acc, key) => {
  27.       acc[key] = params[key];
  28.       return acc;
  29.     }, {});
  30.   }
  31.   specialUrlEncode(value) {
  32.     return querystring.escape(value)
  33.       .replace(/\+/g, '%20')
  34.       .replace(/\*/g, '%2A')
  35.       .replace(/%7E/g, '~');
  36.   }
  37.   getStringToSign(params, method) {
  38.     const sortedParams = Object.keys(params).sort().reduce((acc, key) => {
  39.       acc[key] = params[key];
  40.       return acc;
  41.     }, {});
  42.     const sortQueryStringTmp = Object.entries(sortedParams)
  43.       .map(([key, value]) => `&${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
  44.       .join('');
  45.     return `${method}&${this.specialUrlEncode('/')}&${this.specialUrlEncode(sortQueryStringTmp.substring(1))}`;
  46.   }
  47.   sign(accessSecret, params, method) {
  48.     const stringToSign = this.getStringToSign(params, method);
  49.     const hmac = crypto.createHmac('sha1', accessSecret);
  50.     hmac.update(stringToSign);
  51.     return hmac.digest('base64');
  52.   }
  53.   async preview(deviceCode, channelID) {
  54.     const url = this.BASE_URL + "device/preview/";
  55.     const BASE_PARAMS = this.getBaseParams();
  56.     BASE_PARAMS.deviceCode = deviceCode;
  57.     if (channelID !== undefined) {
  58.       BASE_PARAMS.channelNo = channelID.toString();
  59.     }
  60.     BASE_PARAMS.streamType = "0"; // 0:主码流 1:子码流
  61.     const sign = this.sign(this.ACCESS_SECRET + "&", BASE_PARAMS, "GET");
  62.     const allParams = { ...BASE_PARAMS, Signature: sign };
  63.     const queryString = Object.entries(allParams)
  64.       .map(([key, value]) => `${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
  65.       .join('&');
  66.     const fullUrl = `${url}?${queryString}`;
  67.     console.log("sendurl:", fullUrl);
  68.     try {
  69.       const response = await axios.get(fullUrl);
  70.       if (response.data.status !== 0) {
  71.         throw new Error(`API Error: ${response.data.msg} (status: ${response.data.status})`);
  72.       }
  73.       const previewUrl = response.data.data;
  74.       console.log("previewUrl:", previewUrl);
  75.       return previewUrl;
  76.     } catch (error) {
  77.       console.error('Error in preview:', error);
  78.       throw error;
  79.     }
  80.   }
  81.   async list(deviceCode, startTime, endTime) {
  82.     const url = this.BASE_URL + "device/videoList/";
  83.     const BASE_PARAMS = this.getBaseParams();
  84.     BASE_PARAMS.deviceCode = deviceCode;
  85.     BASE_PARAMS.startTime = startTime;
  86.     BASE_PARAMS.endTime = endTime;
  87.     const sortQueryStringTmp = Object.entries(BASE_PARAMS)
  88.       .map(([key, value]) => `&${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
  89.       .join('');
  90.     const sign = this.sign(this.ACCESS_SECRET + "&", BASE_PARAMS, "GET");
  91.     const fullUrl = url + `?Signature=${this.specialUrlEncode(sign)}${sortQueryStringTmp}`;
  92.     try {
  93.       const response = await axios.get(fullUrl);
  94.       console.log(response.data);
  95.       return response.data;
  96.     } catch (error) {
  97.       console.error('Error in list:', error);
  98.       throw error;
  99.     }
  100.   }
  101.   async replay(deviceCode, startTime, endTime, fileSize) {
  102.     const url = this.BASE_URL + "device/videoReplay/";
  103.     const BASE_PARAMS = this.getBaseParams();
  104.     BASE_PARAMS.deviceCode = deviceCode;
  105.     BASE_PARAMS.startTime = startTime;
  106.     BASE_PARAMS.endTime = endTime;
  107.     BASE_PARAMS.fileSize = fileSize.toString();
  108.     const sortQueryStringTmp = Object.entries(BASE_PARAMS)
  109.       .map(([key, value]) => `&${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
  110.       .join('');
  111.     const sign = this.sign(this.ACCESS_SECRET + "&", BASE_PARAMS, "GET");
  112.     const fullUrl = url + `?Signature=${this.specialUrlEncode(sign)}${sortQueryStringTmp}`;
  113.     try {
  114.       const response = await axios.get(fullUrl);
  115.       console.log(response.data);
  116.       return response.data;
  117.     } catch (error) {
  118.       console.error('Error in replay:', error);
  119.       throw error;
  120.     }
  121.   }
  122. }
复制代码

以上代码中,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 管道发送给客户端。
  1. // RTSP 视频流 WebSocket 转发服务器
  2. var express = require("express");
  3. var expressWebSocket = require("express-ws");
  4. var ffmpeg = require("fluent-ffmpeg");
  5. ffmpeg.setFfmpegPath("C:\\ffmpeg-7.1.1-full_build\\bin\\ffmpeg.exe");
  6. var webSocketStream = require("websocket-stream/stream");
  7. var WebSocket = require("websocket-stream");
  8. var http = require("http");
  9. // 车牌号到设备 ID 的映射表(从数据库或配置中获取)
  10. const carToDeviceDict = initFromDB();
  11. function localServer() {
  12.   let app = express();
  13.   app.use(express.static(__dirname));
  14.   expressWebSocket(app, null, {
  15.     perMessageDeflate: true
  16.   });
  17.   app.ws("/rtsp/", rtspRequestHandle)
  18.   app.listen(8002);
  19.   console.log("express listened")
  20. }
  21. async function rtspRequestHandle(ws, req) {
  22.   console.log("rtsp request handle");
  23.   const stream = webSocketStream(ws, {
  24.     binary: true,
  25.     browserBufferTimeout: 1000000
  26.   }, {
  27.     browserBufferTimeout: 1000000
  28.   });
  29.   let carID = req.query.carID;
  30.   let channel = req.query.channel;
  31.   console.log("car number:", carID);
  32.   const dict = initFromDB();
  33.   const deviceId = dict[carID];
  34.   if (!deviceId) {
  35.     console.error("Device ID not found for car:", carID);
  36.     ws.close();
  37.     return;
  38.   }
  39.   console.log("deviceId:", deviceId, "channel:", channel);
  40.   const CarSDKApp = require("./carsdkapp");
  41.   const carSDKApp = new CarSDKApp();
  42.   try {
  43.     const url = await carSDKApp.preview(deviceId, channel);
  44.     console.log("url:", url);
  45.     ffmpeg(url)
  46.       .addInputOption("-rtsp_transport", "tcp", "-buffer_size", "102400")
  47.       .addInputOption("-fflags", "nobuffer")
  48.       .addInputOption("-flags", "low_delay")
  49.       .addInputOption("-strict", "experimental")
  50.       .addOutputOption("-f", "flv")
  51.       .addOutputOption("-preset", "ultrafast")
  52.       .addOutputOption("-tune", "zerolatency")
  53.       .addOutputOption("-g", "30")
  54.       .addOutputOption("-keyint_min", "30")
  55.       .addOutputOption("-sc_threshold", "0")
  56.       .videoCodec("libx264")
  57.       .noAudio()
  58.       .format("flv")
  59.       .on("start", function (commandLine) {
  60.         console.log("FFmpeg started with command:", commandLine);
  61.       })
  62.       .on("codecData", function (data) {
  63.         console.log("Stream codecData:", data);
  64.       })
  65.       .on("error", function (err) {
  66.         console.log("FFmpeg error:", err.message);
  67.       })
  68.       .on("end", function () {
  69.         console.log("Stream ended");
  70.       })
  71.       .on("stderr", function (stderrLine) {
  72.         console.log("FFmpeg stderr:", stderrLine);
  73.       })
  74.       .pipe(stream, { end: true });
  75.   } catch (error) {
  76.     console.log("Error getting preview URL or starting ffmpeg:", error);
  77.   }
  78. }
  79. localServer();
复制代码

注意 FFmpeg 路径需要根据实际安装位置修改。服务器启动后,前端页面通过 WebSocket 连接 ws://localhost:8002/rtsp/?carID=xxx&channel=1 来获取特定通道的 FLV 流。前端页面使用 flv.js 播放,HTML 结构包含六个视频容器和对应的控制按钮,每个视频框内有一个
  1. video
复制代码
元素和加载提示。
  1. <!-- 页面核心部分 -->
  2. <div class="video-grid">
  3.   <div class="video-container">
  4.     <div class="video-header">通道 1</div>
  5.     <div class="video-box">
  6.       <video controls="controls" class="demo-video" id="player1" muted></video>
  7.       <div id="loading1" class="loading" style="display: block;">加载中...</div>
  8.     </div>
  9.   </div>
  10.   <!-- 其他通道类似,共6个 -->
  11. </div>
  12. <script src="./flv.js"></script>
  13. <script>
  14. // 播放器初始化逻辑
  15. // 每个通道创建 FlvPlayer 实例,连接对应 WebSocket 地址
  16. // 参考 flv.js 官方文档创建 player
  17. // 并在 onerror/onstop 时重新连接
  18. </script>
复制代码

该方案的关键在于:通过海康云 API 获取 RTSP 地址(而非直接使用设备 RTSP 地址,避免公网直接暴露),再利用 FFmpeg 转码为 FLV,WebSocket 传输实现了低延迟、多通道并发播放。实测中需要确保 FFmpeg 版本与系统兼容,并根据网络状况调整缓冲区大小和 GOP 间隔。若需要更高实时性,可将
  1. -g
复制代码
  1. -keyint_min
复制代码
值调低,但会增大带宽消耗。
回复

使用道具 举报

发表于 昨天 12:10 | 显示全部楼层

Re: Node.js + FFmpeg 拉取海康行车记录仪 RTSP 流并 WebSocket 转发至网页播放

这篇方案实现得很扎实,从 API 签名到 FFmpeg 转码再到 WebSocket 推送,整个链路描述清晰,代码封装也规整。对于需要脱离海康云平台、自己在 Web 端直接播放行车记录仪视频的场景,这个思路非常实用。尤其多通道并发和可调缓冲的设计,在实际部署中能灵活应对网络波动。能分享下你选择的 FFmpeg 转码参数(比如 264/265、帧率、关键帧间隔)具体怎么配置来平衡延迟和画质的吗?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-14 03:07 , Processed in 0.031409 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部