当前位置: 首页 > news >正文

前端通过node本地转译rtsp流,配合hls实现浏览

最近遇到一个需要使用监控的项目,对方只提供rtsp流,监控品牌混用,真是让人苦恼。

因为浏览器并不能直接使用rtsp流,所以花了好久在网上查方法,很多都是需要借助第三方插件如vlc帮助浏览器播放,但是插件又依赖flash,现在主流高版本浏览器基本都不支持flash了,像chrome、edge根本就无法使用。

不想走后端帮忙转移,那只能前端自己使用点手段。

使用nodejs再借助ffmpeg进行本地转译。
将下载好的ffmpeg找个文件夹解压,然后配置环境变量 —> 系统变量,编辑path指向解压后bin文件夹。

然后配置弄得文件。
server.js

const express = require('express')
const { spawn } = require('child_process')
const path = require('path')
const app = express()
app.use(express.json())
const cors = require('cors');
app.use(cors());const PORT = 3030// 存储每个 cameraId 对应的 ffmpeg 进程
const ffmpegProcesses = new Map()// 确保 streams 目录存在
const fs = require('fs')// 使用	process.cwd()而不是用__dirname 防止打包exe使用出现问题
const streamsDir = path.join(process.cwd(), 'streams')
if (!fs.existsSync(streamsDir)) {fs.mkdirSync(streamsDir, { recursive: true })
}
app.use('/streams', express.static(streamsDir));// rtsp流存储位置
const camerasFilePath = path.join(process.cwd(), 'cameras.json');
// 读取摄像头配置
let allCameras = [];try {const rawData = fs.readFileSync(camerasFilePath, 'utf-8');allCameras = JSON.parse(rawData);console.log(`✅ 已加载 ${allCameras.length} 路摄像头配置`);
} catch (err) {console.error('❌ 读取摄像头配置失败:', err.message);
}// 启动摄像头:接收 cameraIds 数组,逐个启动 ffmpeg
app.post('/api/start-cameras', (req, res) => {const { cameraIds } = req.bodyconst cameraIdsArr = cameraIds.split(",")let started = []let errors = []cameraIdsArr.forEach(cameraId => {if (ffmpegProcesses.has(cameraId)) {console.log(`⚠️ 摄像头 ${cameraId} 的 ffmpeg 已经在运行,跳过启动`)started.push({ cameraId, status: 'already_running' })return}const rtspUrl = allCameras.find(r => r.id === cameraId)if (!rtspUrl) {console.error(`❌ 找不到摄像头 ${cameraId} 的 RTSP 配置`)errors.push({ cameraId, error: '未找到 RTSP 配置' })return}const outputPath = path.join(streamsDir, `${cameraId}.m3u8`)// ffmpeg 命令:拉取 RTSP,转 HLS,每 2 秒一个 ts 切片,hls_time 控制切片时长const args = ['-i', rtspUrl.rtsp,                    // 输入 RTSP'-analyzeduration', '2000000',  // 单位是微秒(2秒)'-probesize', '2000000',       // 单位是字节(2MB)// ✅ 强制选择视频流(stream 0)和音频流(stream 1),避免 FFmpeg 自动选择错误'-map', '0:0',       // 视频流:通常是索引 0(H.264)'-map', '0:1',       // 音频流:通常是索引 1(AAC)'-c:v', 'libx264',   // 重新编码视频为 H.264'-c:a', 'aac',       // 重新编码音频为 AAC(可选,如果不需要可以去掉)'-preset', 'veryfast',            // 编码速度与质量权衡'-tune', 'zerolatency',           // 低延迟'-f', 'hls',                      // 输出格式 HLS'-hls_time', '2',                 // 每个TS切片约2秒'-hls_list_size', '3',            // 播放列表只保留最近3个TS'-hls_flags', 'delete_segments',  // 自动删除旧的TS'-start_number', '1',outputPath                        // 输出 m3u8 文件路径]console.log(`🚀 正在启动摄像头 ${cameraId},RTSP: ${rtspUrl.rtsp} => HLS: ${outputPath}`)const ffmpeg = spawn('ffmpeg', args)ffmpeg.stderr.on('data', (data) => {console.log(`[ffmpeg-${cameraId} stderr测试]: ${data.toString().trim()}`)})ffmpeg.on('error', (err) => {console.error(`❌ 摄像头 ${cameraId} 的 ffmpeg 启动失败:`, err)errors.push({ cameraId, error: err.message })})ffmpeg.on('close', (code) => {console.log(`⏹️ 摄像头 ${cameraId} 的 ffmpeg 已退出,code=${code}`)ffmpegProcesses.delete(cameraId)})// 记录这个 ffmpeg 进程ffmpegProcesses.set(cameraId, ffmpeg)started.push({ cameraId, status: 'started' })})res.json({message: '启动摄像头请求已处理',started,errors})
})// 停止摄像头:接收 cameraIds 数组,逐个杀掉 ffmpeg
app.post('/api/stop-cameras', (req, res) => {const { cameraIds } = req.bodyconst cameraIdsArr = cameraIds.split(",")let stopped = []let notRunning = []cameraIdsArr.forEach(cameraId => {const ffmpeg = ffmpegProcesses.get(cameraId)if (!ffmpeg) {console.log(`⚠️ 摄像头 ${cameraId} 没有正在运行的 ffmpeg,跳过关闭`)notRunning.push({ cameraId, status: 'not_running' })return}console.log(`🛑 正在停止摄像头 ${cameraId} 的 ffmpeg 进程`)ffmpeg.kill('SIGTERM') // 也可以用 'SIGKILL' 强制终止ffmpegProcesses.delete(cameraId)stopped.push({ cameraId, status: 'stopped' })})res.json({message: '停止摄像头请求已处理',stopped,notRunning})
})// 可选:查看当前正在运行的摄像头列表
app.get('/api/running-cameras', (req, res) => {res.json({running: Array.from(ffmpegProcesses.keys())})
})// 停止所有正在运行的摄像头(杀掉所有 ffmpeg 进程)
app.post('/api/stop-all-cameras', (req, res) => {const allRunningCameraIds = Array.from(ffmpegProcesses.keys())let stopped = []allRunningCameraIds.forEach(cameraId => {const ffmpeg = ffmpegProcesses.get(cameraId)if (ffmpeg) {console.log(`🛑 正在停止所有摄像头中的 ${cameraId} 的 ffmpeg 进程`)ffmpeg.kill('SIGTERM') // 也可以使用 'SIGKILL' 强制终止ffmpegProcesses.delete(cameraId)stopped.push({ cameraId, status: 'stopped' })}})console.log(`✅ 已停止所有摄像头,共停止了 ${stopped.length} 个进程`)res.json({message: '已停止所有正在运行的摄像头转码进程',stopped})
})// 启动服务
app.listen(PORT, () => {console.log(`🟢 FFmpeg 管理服务已启动:http://localhost:${PORT}`)console.log(`📡 API 列表:`)console.log(`   • POST /api/start-cameras  --> 启动摄像头(传入 cameraIds 数组)`)console.log(`   • POST /api/stop-cameras   --> 停止摄像头(传入 cameraIds 数组)`)console.log(`   • GET  /api/running-cameras --> 查看当前正在运行的摄像头`)
})

cameras.json

[{"id": "cam1","name": "测试","rtsp": "rtsp://xxxx"},{"id": "cam2","name": "摄像头2","rtsp": "rtsp://xxxx"},{"id": "cam3","name": "摄像头3","rtsp": "rtsp://xxxx"}
]

package.json

{"name": "rtsp-to-hls-node","version": "1.0.0","description": "Node.js 服务:将 RTSP 摄像头流转为 HLS","main": "server.js","scripts": {"start": "node server.js","build": "pkg . --targets node18-win-x64 --out-path dist/"},"dependencies": {"cors": "^2.8.5","express": "^4.18.2","pkg": "^5.8.1"},"bin": "server.js","pkg": {"targets": ["node18-win-x64"],"outputPath": "dist"}
}

通过node server.js启动

<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>RTSP转HLS测试工具</title><style>body {font-family: Arial, sans-serif;margin: 20px;line-height: 1.6;}.container {max-width: 800px;margin: 0 auto;}.section {margin-bottom: 20px;padding: 15px;border: 1px solid #ddd;border-radius: 5px;}input[type="text"] {width: 100%;padding: 8px;margin: 5px 0;border: 1px solid #ccc;border-radius: 3px;}button {padding: 10px 15px;margin: 5px;border: none;border-radius: 3px;cursor: pointer;background-color: #007bff;color: white;}button:hover {background-color: #0056b3;}button.stop {background-color: #dc3545;}button.stop:hover {background-color: #c82333;}.result {margin-top: 10px;padding: 10px;background-color: #f8f9fa;border-radius: 3px;white-space: pre-wrap;}.video-container {margin-top: 20px;}video {width: 100%;max-width: 600px;border: 1px solid #ddd;}</style><!-- 简易打来地址复制到本地js文件 引用本地js,防止内网机器等情况无法使用 --><script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head><body><div class="container"><h1>RTSP转HLS测试工具</h1><div class="section"><h2>服务器状态</h2><button onclick="getRunningCameras()">获取运行中的摄像头</button><button class="stop" onclick="stopAllCameras()">停止所有摄像头</button><div id="runningStatus" class="result">点击按钮获取状态</div></div><div class="section"><h2>启动摄像头</h2><input type="text" id="cameraIds" placeholder="输入摄像头ID(多个用逗号分隔,如:1,2,3)"><button onclick="startCameras()">启动摄像头</button><div id="startResult" class="result"></div></div><div class="section"><h2>停止摄像头</h2><input type="text" id="stopCameraIds" placeholder="输入要停止的摄像头ID(多个用逗号分隔)"><button class="stop" onclick="stopCameras()">停止摄像头</button><div id="stopResult" class="result"></div></div><div class="section"><h2>视频预览</h2><input type="text" id="previewCameraId" placeholder="输入要预览的摄像头ID"><button onclick="previewCamera()">预览视频</button><div class="video-container"><video id="videoPlayer" controls><source id="videoSource" src="" type="application/x-mpegURL">您的浏览器不支持视频播放</video></div></div></div><script>const API_BASE = 'http://localhost:3030';// 显示结果function showResult(elementId, data) {const element = document.getElementById(elementId);element.innerHTML = JSON.stringify(data, null, 2);}// 获取运行中的摄像头async function getRunningCameras() {try {const response = await fetch(`http://localhost:3030/api/running-cameras`);const data = await response.json();showResult('runningStatus', data);} catch (error) {showResult('runningStatus', { error: error.message });}}// 启动摄像头async function startCameras() {const cameraIds = document.getElementById('cameraIds').value;if (!cameraIds) {alert('请输入摄像头ID');return;}const response = await fetch(`http://localhost:3030/api/start-cameras`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ cameraIds })});this.previewCamera()}// 停止摄像头async function stopCameras() {const cameraIds = document.getElementById('stopCameraIds').value;const response = await fetch(`http://localhost:3030/api/stop-cameras`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ cameraIds })});}// 停止所有摄像头async function stopAllCameras() {const response = await fetch(`http://localhost:3030/api/stop-all-cameras`, {method: 'POST',headers: {'Content-Type': 'application/json'}});}// 预览摄像头视频function previewCamera() {const cameraId = document.getElementById('previewCameraId').value;if (!cameraId) {alert('请输入摄像头ID');return;}const videoPlayer = document.getElementById('videoPlayer');const videoUrl = `http://localhost:3030/streams/${cameraId}.m3u8`;// 清除之前的Hls实例(如果存在)if (window.hls) {window.hls.destroy();}if (Hls.isSupported()) {// 创建新的Hls实例window.hls = new Hls({debug: false, // 关闭调试信息enableWorker: true, // 使用Web Worker提高性能lowLatencyMode: true // 低延迟模式});// 绑定视频元素window.hls.loadSource(videoUrl);window.hls.attachMedia(videoPlayer);// 监听事件window.hls.on(Hls.Events.MANIFEST_PARSED, function () {console.log('HLS流加载成功,开始播放');videoPlayer.play().catch(e => {console.log('自动播放被阻止,需要用户交互', e);});});window.hls.on(Hls.Events.ERROR, function (event, data) {console.error('HLS错误:', data);if (data.fatal) {switch (data.type) {case Hls.ErrorTypes.NETWORK_ERROR:console.log('网络错误,尝试重新加载...');window.hls.startLoad();break;case Hls.ErrorTypes.MEDIA_ERROR:console.log('媒体错误,尝试恢复...');window.hls.recoverMediaError();break;default:console.log('不可恢复的错误');window.hls.destroy();break;}}});} else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {// Safari浏览器原生支持HLSvideoPlayer.src = videoUrl;videoPlayer.addEventListener('loadedmetadata', function () {videoPlayer.play().catch(e => {console.log('自动播放被阻止,需要用户交互', e);});});} else {alert('您的浏览器不支持HLS视频播放');return;}showResult('startResult', {message: '开始预览',cameraId: cameraId,streamUrl: videoUrl,hlsSupported: Hls.isSupported()});}// 页面加载时获取一次运行状态window.onload = getRunningCameras;</script>
</body></html>

安装过并配置了package.json,可以直接使用npm run build进行打包exe,就可以双击使用了。
注意server.js中使用的rtsp流的json是同级的,同时将js、复制到本地的hls.js文件和exe放在同级,一起丢到可以访问rtsp的机器就可以测试使用了。
如果在本地测试有没有rtsp流使用,可以去网上找找,有提供的(但好像用来测试不好使),也可以下载vlc工具,通过流转MP4视频为rtsp流,在本地可以测试使用(这个好使)。

使用时要 注意 一点,这个转译好像因为ffmpeg比较吃cpu,使用视频的页面如果跳转到其他页面,可以将转译终止,不然转译的多了肯定吃不消。

下班下班!!!!

http://www.dtcms.com/a/349667.html

相关文章:

  • 【SQL】深入理解MySQL存储过程:从入门到实战
  • CUDA 工具包 13.0 正式发布:开启新一代 GPU 计算的基石!
  • 使用EasyExcel根据模板导出文件
  • QtExcel/QXlsx
  • 深入浅出 Java 多态:从原理到实践的全面解析
  • 【RAGFlow代码详解-5】配置系统
  • 基于深度学习的翻拍照片去摩尔纹在线系统设计与实现
  • UE5 HoudiniPivotPainter1.0使用
  • NFC 滤波网络设计考虑
  • 车载通信架构---通过CANoe做 SOME/IP 模拟的配置例如
  • 库存指标怎么算?一文讲清3大库存分析指标
  • 大数据治理域——离线数据开发
  • 小白成长之路-k8s部署项目(二)
  • Legion Y7000P IRX9 DriveList
  • 【数据可视化-100】使用 Pyecharts 绘制人口迁徙图:步骤与数据组织形式
  • 程序设计---状态机
  • KVM 虚拟化技术与部署
  • ZKmall开源商城多端兼容实践:鸿蒙、iOS、安卓全平台适配的技术路径
  • 朴素贝叶斯学习笔记
  • Selenium框架Java实践截图服务
  • 面向过程与面向对象
  • 了解检验和
  • 四,设计模式-原型模式
  • 设计模式5-代理模式
  • 无锁队列的设计与实现
  • jdbc相关内容
  • 基于TimeMixer的帕金森语音分类:WAV音频输入与训练全流程
  • 基于开源 AI 智能名片链动 2+1 模式 S2B2C 商城小程序的新开非连锁品牌店开业引流策略研究
  • 云计算之中间件与数据库
  • 蜂窝物联网模组在冷链运输行业的应用价值