Nodejs拉取??低曅熊囉涗泝x攝像頭視頻流的實(shí)現(xiàn)方法
1.背景
現(xiàn)有需求,將??低曅熊囉涗泝x攝像頭視頻流顯示在開發(fā)web頁(yè)面上,基于諸多方面的考慮,采用海康的云平臺(tái)作為轉(zhuǎn)發(fā)、存儲(chǔ)視頻流的平臺(tái),而沒(méi)有搭建私有的視頻解決方案。如何將視頻流顯示在自己的web頁(yè)面上呢,采用了利用??堤峁┑膕dk截取轉(zhuǎn)發(fā)視頻流的方式。在研發(fā)此方案的過(guò)程中,經(jīng)歷了諸多試錯(cuò),本方案只是諸多嘗試中的可以成功運(yùn)行的方案之一。
本方案采用的是nodejs方案,將RTSP視頻流通過(guò)FFmpeg轉(zhuǎn)換為FLV格式,并通過(guò)WebSocket轉(zhuǎn)發(fā)給前端播放器。優(yōu)點(diǎn)是支持多通道并發(fā)并能支持一定的延時(shí)緩沖。
2實(shí)現(xiàn)
2.1??灯嘢DK
??灯噑dk即??灯囯娮釉艫PI服務(wù),為開發(fā)者提供https接口,即開發(fā)者通過(guò)https方式發(fā)起檢索請(qǐng)求,獲取返回json數(shù)據(jù)。具體代碼可參見??档墓倬W(wǎng):https://open.hikvisionauto.com/#/developDoc/api/HttpsAPI
官網(wǎng)似乎進(jìn)行了改版,對(duì)代碼進(jìn)行了刪減。改版前有java語(yǔ)言的版本,功能較豐富。以下是根據(jù)java語(yǔ)言代碼改寫成的nodejs語(yǔ)言的版本,支持預(yù)覽,獲得錄像列表及查看錄像:
/**
* 海康威視汽車設(shè)備SDK封裝類
* 用于獲取設(shè)備預(yù)覽、視頻列表和回放功能
*/
const crypto = require('crypto');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const querystring = require('querystring');
class CarSDKApp {
/**
* 構(gòu)造函數(shù) - 初始化SDK配置參數(shù)
*/
constructor() {
// API訪問(wèn)密鑰
this.ACCESS_KEY = "****************";
// API訪問(wèn)密鑰
this.ACCESS_SECRET = "**************";
// 簽名方法
this.SIGNATURE_METHOD = "HMAC-SHA1";
// 默認(rèn)字符編碼
this.DEFAULT_CHARSET = "UTF-8";
// 區(qū)域ID
this.REGION_ID = "cn-hangzhou";
// API版本號(hào)
this.VERSION = "2.1.0";
// API基礎(chǔ)URL
this.BASE_URL = "https://open.hikvisionauto.com:14021/v2/";
}
/**
* 獲取基礎(chǔ)請(qǐng)求參數(shù)
* @returns {Object} 包含簽名所需基礎(chǔ)參數(shù)的對(duì)象
*/
getBaseParams() {
const params = {
SignatureMethod: this.SIGNATURE_METHOD,
SignatureNonce: uuidv4(), // 隨機(jī)唯一標(biāo)識(shí)符
AccessKey: this.ACCESS_KEY,
Timestamp: Date.now().toString(), // 當(dāng)前時(shí)間戳
Version: this.VERSION,
RegionId: this.REGION_ID
};
// 按鍵名排序參數(shù)
return Object.keys(params).sort().reduce((acc, key) => {
acc[key] = params[key];
return acc;
}, {});
}
/**
* 特殊URL編碼
* 將特殊字符按照API要求進(jìn)行編碼
* @param {string} value - 需要編碼的值
* @returns {string} 編碼后的字符串
*/
specialUrlEncode(value) {
return querystring.escape(value)
.replace(/\+/g, '%20')
.replace(/\*/g, '%2A')
.replace(/%7E/g, '~');
}
/**
* 生成待簽名字符串
* @param {Object} params - 請(qǐng)求參數(shù)對(duì)象
* @param {string} method - HTTP請(qǐng)求方法(GET/POST)
* @returns {string} 待簽名字符串
*/
getStringToSign(params, method) {
// 確保參數(shù)按鍵名排序
const sortedParams = Object.keys(params).sort().reduce((acc, key) => {
acc[key] = params[key];
return acc;
}, {});
// 構(gòu)建排序后的查詢字符串
const sortQueryStringTmp = Object.entries(sortedParams)
.map(([key, value]) => `&${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
.join('');
// 返回格式:HTTP方法&URL編碼的路徑&URL編碼的查詢字符串
return `${method}&${this.specialUrlEncode('/')}&${this.specialUrlEncode(sortQueryStringTmp.substring(1))}`;
}
/**
* 生成API簽名
* @param {string} accessSecret - 訪問(wèn)密鑰
* @param {Object} params - 請(qǐng)求參數(shù)
* @param {string} method - HTTP請(qǐng)求方法
* @returns {string} Base64編碼的簽名字符串
*/
sign(accessSecret, params, method) {
const stringToSign = this.getStringToSign(params, method);
console.log(`StringToSign = [${stringToSign}]`);
// 使用HMAC-SHA1算法生成簽名
const hmac = crypto.createHmac('sha1', accessSecret);
hmac.update(stringToSign);
return hmac.digest('base64');
}
/**
* 獲取設(shè)備實(shí)時(shí)預(yù)覽URL
* @param {string} deviceCode - 設(shè)備編碼
* @param {number} channelID - 通道ID(可選)
* @returns {Promise<string>} 預(yù)覽URL
*/
async preview(deviceCode, channelID) {
const url = this.BASE_URL + "device/preview/";
const BASE_PARAMS = this.getBaseParams();
// 添加業(yè)務(wù)參數(shù)
BASE_PARAMS.deviceCode = deviceCode;
if (channelID !== undefined) {
BASE_PARAMS.channelNo = channelID.toString();
}
BASE_PARAMS.streamType = "0"; // 0:主碼流 1:子碼流
// 生成簽名(注意:簽名時(shí)不包含 Signature 參數(shù)本身)
const sign = this.sign(this.ACCESS_SECRET + "&", BASE_PARAMS, "GET");
// 構(gòu)建完整的查詢字符串(包含簽名)
const allParams = { ...BASE_PARAMS, Signature: sign };
const queryString = Object.entries(allParams)
.map(([key, value]) => `${this.specialUrlEncode(key)}=${this.specialUrlEncode(value)}`)
.join('&');
const fullUrl = `${url}?${queryString}`;
console.log("sendurl:", fullUrl);
try {
const response = await axios.get(fullUrl);
console.log("response:", response.data);
// 檢查響應(yīng)狀態(tài)
if (response.data.status !== 0) {
throw new Error(`API Error: ${response.data.msg} (status: ${response.data.status})`);
}
// 提取預(yù)覽URL
const previewUrl = response.data.data;
console.log("previewUrl:", previewUrl);
return previewUrl;
} catch (error) {
console.error('Error in preview:', error);
throw error;
}
}
/**
* 獲取設(shè)備視頻列表
* @param {string} deviceCode - 設(shè)備編碼
* @param {string} startTime - 開始時(shí)間(格式:YYYY-MM-DD HH:mm:ss)
* @param {string} endTime - 結(jié)束時(shí)間(格式:YYYY-MM-DD HH:mm:ss)
* @returns {Promise<Object>} 視頻列表響應(yīng)數(shù)據(jù)
*/
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(([key, value]) => `&${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;
}
}
/**
* 獲取設(shè)備視頻回放URL
* @param {string} deviceCode - 設(shè)備編碼
* @param {string} startTime - 開始時(shí)間(格式:YYYY-MM-DD HH:mm:ss)
* @param {string} endTime - 結(jié)束時(shí)間(格式:YYYY-MM-DD HH:mm:ss)
* @param {number} fileSize - 文件大小(字節(jié))
* @returns {Promise<Object>} 回放URL響應(yīng)數(shù)據(jù)
*/
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(([key, value]) => `&${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;
}
}
}
2.2Nodejs視頻流服務(wù)器
首先是引用包
/**
* RTSP視頻流WebSocket轉(zhuǎn)發(fā)服務(wù)器
* 功能:將RTSP視頻流通過(guò)FFmpeg轉(zhuǎn)換為FLV格式,并通過(guò)WebSocket轉(zhuǎn)發(fā)給前端播放器
*/
var express = require("express");
var expressWebSocket = require("express-ws");
var ffmpeg = require("fluent-ffmpeg");
// 設(shè)置FFmpeg可執(zhí)行文件路徑
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");
然后是具體實(shí)現(xiàn)代碼。
// 獲取車牌號(hào)到設(shè)備ID的映射
const carToDeviceDict = initFromDB();
/**
* 啟動(dòng)本地WebSocket服務(wù)器
* 監(jiān)聽8002端口,處理RTSP視頻流轉(zhuǎn)發(fā)請(qǐng)求
*/
function localServer() {
let app = express();
// 提供靜態(tài)文件服務(wù)
app.use(express.static(__dirname));
// 添加WebSocket支持,啟用消息壓縮
expressWebSocket(app, null, {
perMessageDeflate: true
});
// 注冊(cè)WebSocket路由處理函數(shù)
app.ws("/rtsp/", rtspRequestHandle)
// 啟動(dòng)HTTP服務(wù)器監(jiān)聽8002端口
app.listen(8002);
console.log("express listened")
}
/**
* 處理RTSP視頻流WebSocket請(qǐng)求
* @param {WebSocket} ws - WebSocket連接對(duì)象
* @param {Request} req - HTTP請(qǐng)求對(duì)象
*/
async function rtspRequestHandle(ws, req) {
console.log("rtsp request handle");
// 將WebSocket轉(zhuǎn)換為可讀寫的流
const stream = webSocketStream(ws, {
binary: true, // 使用二進(jìn)制模式傳輸
browserBufferTimeout: 1000000 // 瀏覽器緩沖區(qū)超時(shí)時(shí)間(毫秒)
}, {
browserBufferTimeout: 1000000
});
// 從請(qǐng)求參數(shù)中獲取車牌號(hào)和通道號(hào)
let carID = req.query.carID;
let channel = req.query.channel;
console.log("car number:", carID);
// 將車牌號(hào)轉(zhuǎn)換為設(shè)備ID
const dict = initFromDB();
const deviceId = dict[carID];
// 檢查設(shè)備ID是否存在
if (!deviceId) {
console.error("Device ID not found for car:", carID);
ws.close();
return;
}
console.log("deviceId:", deviceId, "channel:", channel);
// 導(dǎo)入??低暺囋O(shè)備SDK
const CarSDKApp = require("./carsdkapp");
const carSDKApp = new CarSDKApp();
try {
// 調(diào)用SDK獲取設(shè)備實(shí)時(shí)預(yù)覽URL
const url = await carSDKApp.preview(deviceId, channel);
console.log("url:", url);
// 使用FFmpeg處理RTSP流并轉(zhuǎn)換為FLV格式
ffmpeg(url)
// 輸入選項(xiàng)配置
.addInputOption("-rtsp_transport", "tcp", "-buffer_size", "102400") // 使用TCP傳輸,設(shè)置緩沖區(qū)大小
.addInputOption("-fflags", "nobuffer") // 禁用輸入緩沖,減少延遲
.addInputOption("-flags", "low_delay") // 低延遲模式
.addInputOption("-strict", "experimental") // 允許實(shí)驗(yàn)性編解碼器
// 輸出選項(xiàng)配置
.addOutputOption("-f", "flv") // 輸出格式為FLV
.addOutputOption("-preset", "ultrafast") // 編碼預(yù)設(shè):最快速度
.addOutputOption("-tune", "zerolatency") // 調(diào)優(yōu):零延遲
.addOutputOption("-g", "30") // 關(guān)鍵幀間隔(GOP大?。?
.addOutputOption("-keyint_min", "30") // 最小關(guān)鍵幀間隔
.addOutputOption("-sc_threshold", "0") // 禁用場(chǎng)景切換檢測(cè)
// 視頻編解碼器設(shè)置
.videoCodec("libx264") // 使用H.264編碼
.noAudio() // 禁用音頻
.format("flv") // 輸出格式為FLV
// 事件監(jiān)聽器
.on("start", function (commandLine) {
console.log("FFmpeg started with command:", commandLine);
})
.on("codecData", function (data) {
console.log("Stream codecData:", data);
// 攝像機(jī)在線處理
})
.on("error", function (err) {
console.log("FFmpeg error:", err.message);
console.log("Error details:", err);
})
.on("end", function () {
console.log("Stream ended");
// 攝像機(jī)斷線的處理
})
.on("stderr", function (stderrLine) {
console.log("FFmpeg stderr:", stderrLine);
})
// 將處理后的流通過(guò)WebSocket發(fā)送給客戶端
.pipe(stream, { end: true });
} catch (error) {
console.log("Error getting preview URL or starting ffmpeg:", error);
}
}
// 啟動(dòng)服務(wù)器
localServer();
2.3頁(yè)面代碼
<body>
<!-- 頁(yè)面標(biāo)題 -->
<div class="header">
六通道視頻監(jiān)控系統(tǒng)
</div>
<!-- 系統(tǒng)狀態(tài)欄 -->
<div class="status-bar">
<span id="status">系統(tǒng)準(zhǔn)備就緒 - 正在初始化...</span>
</div>
<!-- 通道狀態(tài)指示器 -->
<div class="channel-status">
<div class="channel-indicator" id="indicator1">通道1: 斷開</div>
<div class="channel-indicator" id="indicator2">通道2: 斷開</div>
<div class="channel-indicator" id="indicator3">通道3: 斷開</div>
<div class="channel-indicator" id="indicator4">通道4: 斷開</div>
<div class="channel-indicator" id="indicator5">通道5: 斷開</div>
<div class="channel-indicator" id="indicator6">通道6: 斷開</div>
</div>
<!-- 視頻播放網(wǎng)格 -->
<div class="video-grid">
<!-- 通道1 -->
<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>
<!-- 通道2 -->
<div class="video-container">
<div class="video-header">通道 2</div>
<div class="video-box">
<video controls="controls" class="demo-video" id="player2" muted></video>
<div id="loading2" class="loading" style="display: block;">加載中...</div>
</div>
</div>
<!-- 通道3 -->
<div class="video-container">
<div class="video-header">通道 3</div>
<div class="video-box">
<video controls="controls" class="demo-video" id="player3" muted></video>
<div id="loading3" class="loading" style="display: block;">加載中...</div>
</div>
</div>
<!-- 通道4 -->
<div class="video-container">
<div class="video-header">通道 4</div>
<div class="video-box">
<video controls="controls" class="demo-video" id="player4" muted></video>
<div id="loading4" class="loading" style="display: block;">加載中...</div>
</div>
</div>
<!-- 通道5 -->
<div class="video-container">
<div class="video-header">通道 5</div>
<div class="video-box">
<video controls="controls" class="demo-video" id="player5" muted></video>
<div id="loading5" class="loading" style="display: block;">加載中...</div>
</div>
</div>
<!-- 通道6 -->
<div class="video-container">
<div class="video-header">通道 6</div>
<div class="video-box">
<video controls="controls" class="demo-video" id="player6" muted></video>
<div id="loading6" class="loading" style="display: block;">加載中...</div>
</div>
</div>
</div>
<!-- 控制按鈕區(qū)域 -->
<div class="controls">
<button class="btn" onclick="reconnectAll()">?? 重新連接所有通道</button>
<div class="btn-group">
<button class="btn" onclick="reconnectChannel(1)">重連通道1</button>
<button class="btn" onclick="reconnectChannel(2)">重連通道2</button>
<button class="btn" onclick="reconnectChannel(3)">重連通道3</button>
</div>
<div class="btn-group">
<button class="btn" onclick="reconnectChannel(4)">重連通道4</button>
<button class="btn" onclick="reconnectChannel(5)">重連通道5</button>
<button class="btn" onclick="reconnectChannel(6)">重連通道6</button>
</div>
</div>
<!-- 引入 flv.js 庫(kù) - 用于播放FLV格式的視頻流 -->
<script src="./flv.js"></script>
<script>
/**
* 視頻播放器類
* 封裝單個(gè)通道的視頻播放功能
*/
class VideoPlayer {
/**
* 構(gòu)造函數(shù)
* @param {number} channel - 通道編號(hào)(1-6)
*/
constructor(channel) {
this.channel = channel;
this.player = null; // flv.js播放器實(shí)例
this.loading = true; // 加載狀態(tài)標(biāo)志
this.videoElement = document.getElementById(`player${channel}`);
this.loadingElement = document.getElementById(`loading${channel}`);
// WebSocket視頻流地址
this.rtspUrl = `ws://localhost:8888/rtsp?deviceId=16065442039&channel=${channel}`;
this.init();
}
/**
* 初始化播放器
* 綁定事件監(jiān)聽器并開始播放
*/
init() {
// 綁定雙擊全屏事件
this.videoElement.addEventListener('dblclick', () => {
this.fullScreen();
});
// 等待 flv.js 加載完成后播放視頻
this.waitForFlvjs();
// 頁(yè)面卸載時(shí)清理資源
window.addEventListener('beforeunload', () => {
this.cleanup();
});
}
/**
* 等待flv.js庫(kù)加載完成
* 輪詢檢查直到flv.js可用
*/
waitForFlvjs() {
// 檢查 flv.js 是否已加載
if (typeof flvjs !== 'undefined') {
this.playVideo();
} else {
// 如果還沒(méi)加載,等待 100ms 后重試
setTimeout(() => {
this.waitForFlvjs();
}, 100);
}
}
/**
* 切換全屏播放
*/
fullScreen() {
const video = this.videoElement;
if (video.requestFullscreen) {
video.requestFullscreen();
} else if (video.mozRequestFullScreen) {
video.mozRequestFullScreen();
} else if (video.webkitRequestFullScreen) {
video.webkitRequestFullScreen();
} else if (video.msRequestFullscreen) {
video.msRequestFullscreen();
}
}
/**
* 播放視頻
* 創(chuàng)建flv.js播放器并開始播放FLV流
*/
playVideo() {
const time1 = new Date().getTime();
this.updateChannelStatus('connecting');
// 檢查瀏覽器是否支持FLV.js
if (flvjs.isSupported()) {
const video = this.videoElement;
if (video) {
// 如果已有播放器,先清理
if (this.player) {
this.player.unload();
this.player.destroy();
this.player = null;
this.loading = true;
this.loadingElement.style.display = 'block';
}
// 創(chuàng)建新的播放器
this.player = flvjs.createPlayer({
type: 'flv',
isLive: true, // 標(biāo)記為直播流
url: this.rtspUrl
});
// 將播放器綁定到video元素
this.player.attachMediaElement(video);
try {
// 加載并播放視頻
this.player.load();
this.player.play().then(() => {
console.log(`通道${this.channel}播放開始,耗時(shí):`, new Date().getTime() - time1, 'ms');
this.loading = false;
this.loadingElement.style.display = 'none';
this.updateChannelStatus('connected');
this.updateGlobalStatus(`通道${this.channel}連接成功`);
}).catch((error) => {
console.error(`通道${this.channel}播放失敗:`, error);
this.loadingElement.textContent = '播放失敗';
this.updateChannelStatus('disconnected');
this.updateGlobalStatus(`通道${this.channel}播放失敗`);
});
} catch (error) {
console.error(`通道${this.channel}加載失敗:`, error);
this.loadingElement.textContent = '加載失敗';
this.updateChannelStatus('disconnected');
this.updateGlobalStatus(`通道${this.channel}加載失敗`);
}
}
} else {
console.error('當(dāng)前瀏覽器不支持 FLV.js');
this.loadingElement.textContent = '當(dāng)前瀏覽器不支持 FLV 播放';
this.updateChannelStatus('disconnected');
this.updateGlobalStatus('瀏覽器不支持 FLV 播放');
}
}
/**
* 更新通道狀態(tài)指示器
* @param {string} status - 狀態(tài)值:connected/connecting/disconnected
*/
updateChannelStatus(status) {
const indicator = document.getElementById(`indicator${this.channel}`);
indicator.className = 'channel-indicator';
switch(status) {
case 'connected':
indicator.classList.add('connected');
indicator.textContent = `通道${this.channel}: 已連接`;
break;
case 'connecting':
indicator.classList.add('connecting');
indicator.textContent = `通道${this.channel}: 連接中...`;
break;
case 'disconnected':
default:
indicator.textContent = `通道${this.channel}: 斷開`;
break;
}
}
/**
* 更新全局狀態(tài)欄
* @param {string} message - 狀態(tài)消息
*/
updateGlobalStatus(message) {
const statusElement = document.getElementById('status');
const timestamp = new Date().toLocaleTimeString();
statusElement.textContent = `[${timestamp}] ${message}`;
}
/**
* 重新播放(用于重連等場(chǎng)景)
*/
replay() {
this.playVideo();
}
/**
* 清理播放器資源
*/
cleanup() {
if (this.player) {
this.player.unload();
this.player.destroy();
this.player = null;
}
}
}
// 全局變量存儲(chǔ)播放器實(shí)例
let videoPlayers = {};
/**
* 初始化所有視頻播放器
* 確保flv.js加載完成后創(chuàng)建六個(gè)通道的播放器
*/
function initVideoPlayers() {
if (typeof flvjs !== 'undefined') {
// 初始化六個(gè)通道的播放器
for (let i = 1; i <= 6; i++) {
videoPlayers[i] = new VideoPlayer(i);
}
console.log('六通道視頻播放器初始化完成');
updateGlobalStatus('六通道視頻播放器初始化完成');
// 注冊(cè)鍵盤快捷鍵
document.addEventListener('keydown', function(event) {
switch(event.key.toLowerCase()) {
case 'r':
// R鍵:重連所有通道
reconnectAll();
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
// 數(shù)字鍵1-6:重連對(duì)應(yīng)通道
reconnectChannel(parseInt(event.key));
break;
case 'a':
// A鍵:重連所有通道
reconnectAll();
break;
}
});
} else {
// 如果 flv.js 還沒(méi)加載,等待 100ms 后重試
setTimeout(initVideoPlayers, 100);
}
}
/**
* 重連所有通道
* 依次延遲啟動(dòng)每個(gè)通道,避免同時(shí)發(fā)起太多連接
*/
function reconnectAll() {
console.log('重新連接所有通道...');
updateGlobalStatus('正在重新連接所有通道...');
Object.values(videoPlayers).forEach((player, index) => {
if (player) {
// 延遲啟動(dòng),避免同時(shí)發(fā)起太多連接
setTimeout(() => {
player.replay();
}, index * 500);
}
});
}
/**
* 重連指定通道
* @param {number} channel - 通道編號(hào)(1-6)
*/
function reconnectChannel(channel) {
console.log(`重新連接通道${channel}...`);
if (videoPlayers[channel]) {
videoPlayers[channel].replay();
updateGlobalStatus(`正在重新連接通道${channel}...`);
}
}
/**
* 更新全局狀態(tài)
* @param {string} message - 狀態(tài)消息
*/
function updateGlobalStatus(message) {
const statusElement = document.getElementById('status');
const timestamp = new Date().toLocaleTimeString();
statusElement.textContent = `[${timestamp}] ${message}`;
}
/**
* 獲取連接統(tǒng)計(jì)信息
* @returns {Object} 包含已連接、連接中、斷開數(shù)量的對(duì)象
*/
function getConnectionStats() {
const indicators = document.querySelectorAll('.channel-indicator');
let connected = 0;
let connecting = 0;
let disconnected = 0;
indicators.forEach(indicator => {
if (indicator.classList.contains('connected')) {
connected++;
} else if (indicator.classList.contains('connecting')) {
connecting++;
} else {
disconnected++;
}
});
return { connected, connecting, disconnected };
}
// 定期更新連接統(tǒng)計(jì)(每5秒)
setInterval(() => {
const stats = getConnectionStats();
if (stats.connected > 0 || stats.connecting > 0) {
updateGlobalStatus(`連接狀態(tài): ${stats.connected}個(gè)已連接, ${stats.connecting}個(gè)連接中, ${stats.disconnected}個(gè)斷開`);
}
}, 5000);
// 頁(yè)面加載完成后初始化播放器
document.addEventListener('DOMContentLoaded', initVideoPlayers);
</script>
</body>
3總結(jié)
使用nodejs拉取rtsp視頻流,通過(guò)WebSocket轉(zhuǎn)發(fā)給前端,并使用flv播放,不失為一種可行方案。
到此這篇關(guān)于Nodejs拉取海康威視行車記錄儀攝像頭視頻流的文章就介紹到這了,更多相關(guān)Nodejs拉取??低曅熊囉涗泝x內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
NodeJs 實(shí)現(xiàn)簡(jiǎn)單WebSocket即時(shí)通訊的示例代碼
這篇文章主要介紹了NodeJs 實(shí)現(xiàn)簡(jiǎn)單WebSocket即時(shí)通訊的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
Mongoose中document與object的區(qū)別示例詳解
這篇文章主要給大家介紹了關(guān)于Mongoose中document與object區(qū)別的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09
express中創(chuàng)建 websocket 接口及問(wèn)題解答
本文主要介紹了express中創(chuàng)建 websocket 接口及問(wèn)題解答,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
nodejs清空/刪除指定文件夾下面所有文件或文件夾的方法示例
這篇文章主要介紹了nodejs清空/刪除指定文件夾下面所有文件或文件夾的方法,通過(guò)兩個(gè)具體案例形式分析了node.js同步刪除文件/文件夾,以及異步刪除文件/文件夾的相關(guān)實(shí)現(xiàn)技巧,涉及遞歸遍歷與文件判斷、回調(diào)等相關(guān)操作,需要的朋友可以參考下2023-04-04

