node实现简单的websocket服务器,基于ffmpeg将rtmp、rtsp流转码为ws流
【前言】 由于之前使用node-rtsp-stream这个插件每个播放流都需要开启一个端口来实现websocket服务,但是项目有需要播放很多视频所以自己写了一个工具类实现只用一个端口实现多个视频的转码播放
之前的文章:链接: node通过ffmpeg将多路rtsp、rtmp流媒体转换为多端口websocket流供前端播放
直接上代码
1 安装ffmpeg
ffmpeg官网下载:https://ffmpeg.org/
github下载:https://github.com/FFmpeg/FFmpeg
2 node代码(必须要提前安装ffmpeg)
注意 需提前安装FFmpeg,这是转码核心
注意logger 这个是我写的日志工具,实际使用时请把logger 全部删除
安装基本插件
npm i ws
npm i child_process
const WebSocket = require('ws')
const { spawn } = require('child_process')
const logger = require('./log')
class WebSocketServer {
constructor(port = 20000) {
this.port = port // WebSocket 服务器端口
this.ffmpegServerList = [] // 保存所有 FFmpeg 进程以及wss实例的映射关系
this.wss = null // WebSocket 服务器实例
this.init(port) // 初始化 WebSocket 服务器
}
init(port = this.port) {
this.wss = new WebSocket.Server({ port }) // 创建 WebSocket 服务器
logger.log('websocket服务启动') // 打印日志
// 监听 WebSocket 连接
this.wss.on('connection', (ws, req) => {
try {
const path = decodeURIComponent(req.url)?.split('?')[1]?.split('=')[1] // 获取rtsp/rtmp请求路径
let ffmpegServer
let ffmpeg // FFmpeg 进程实例
// 检查是否已经存在与相同路径的 FFmpeg 进程
if (this.ffmpegServerList.filter((item) => item.path === path).length > 0) {
ffmpegServer = this.ffmpegServerList.filter((item) => item.path === path)[0] // 获取当前路径的包含ffmpeg、计数器的object
ffmpeg = ffmpegServer.ffmpeg // 获取当前路径的ffmpeg实例
ffmpegServer['activeConnections'] = ffmpegServer?.activeConnections + 1 // 计数器加一,计数器指的是目前调用同一视频流的websocket连接数,类似与两个用户同时观看
} else {
// 如果ffmpegServerList不存在任务activeConnections==0,那么创建一个对象并保存至ffmpegServerList中
console.log('websocket服务进程启动path', path)
// 启动 FFmpeg 进程
ffmpeg = spawn('ffmpeg', ['-i', path, '-c:v', 'mpeg1video', '-c:a', 'mp2', '-f', 'mpegts', '-'])
this.ffmpegServerList.push({
activeConnections: 1,
path,
ws,
ffmpeg
})
}
// 将 FFmpeg 输出通过 WebSocket 发送
ffmpeg.stdout.on('data', (data) => {
// 发送 JSON 消息
ws.send(data)
})
// 监听子进程的标准错误
ffmpeg.stderr.on('data', (data) => {
// console.error(`转码异常警告: ${data}`)
})
// 监听子进程的关闭事件
ffmpeg.on('close', (code) => {
logger.log('ffmpeg关闭,关闭服务:code' + code + ' path:' + path)
this.ffmpegServerList = this.ffmpegServerList.filter((item) => item.path !== path)
})
ffmpeg.on('error', (error) => {
logger.log('ffmpeg异常终止,关闭服务:' + path + ' error:' + error)
ffmpeg.kill()
this.ffmpegServerList = this.ffmpegServerList.filter((item) => item.path !== path)
})
ws.on('close', () => {
console.log('websocket数据传输进程关闭')
ffmpegServer = this.ffmpegServerList.filter((item) => item.path === path)[0] // 获取当前路径的ffmpeg实例
if (ffmpegServer) {
ffmpegServer['activeConnections'] = ffmpegServer?.activeConnections - 1 // 计数器减一
if (ffmpegServer['activeConnections'] === 0) {
// 如果计数器为0,则关闭ffmpeg实例
ffmpeg.kill()
logger.log('终止一个ffmpeg转码' + ffmpegServer.path)
this.ffmpegServerList = this.ffmpegServerList.filter((item) => item.path !== path)
}
}
})
} catch (error) {
console.error('websocket服务异常,请检查代码(util/WebSocket.js):' + error)
logger.log('websocket服务异常,请检查代码(util/WebSocket.js)' + error)
}
})
}
// 清除所有连接
clear() {
this.ffmpegServerList.forEach(async (item) => {
// 关闭 WebSocket 连接
await item.ws.close()
})
// 清空列表
this.ffmpegServerList = []
}
restart() {
this.clear()
this.wss.close()
this.init()
}
}
module.exports = WebSocketServer
找个项目启动时的文件里加入该代码,注意WebSocket.js路径,port默认20000,自己定义
const WebSocketServer = require('../util/WebSocket.js')
SocketServer = new WebSocketServer(port)
3 前端代码
安装jsmpeg-player插件
npm i jsmpeg-player
设置rtsp-video.vue组件:
说明一下ws://${currentPageIP}:20000/socket?path=${encodeURIComponent(url)}
这个中currentPageIP是node服务地址ip,20000是端口号与node服务你写入的一致,url是rtmp和rtsp的地址url
<script setup>
import { defineProps, onMounted, onBeforeUnmount, watch } from 'vue'
/* api */
import { getWebsocketPort } from '@/api/index'
// jsmpeg播放器
import JSMpeg from 'jsmpeg-player'
// import WebSocket from 'ws';
const currentPageIP = window.location.hostname
const props = defineProps({
url: {
type: String,
default: ''
},
name: {
type: String,
default: 'name1'
},
show: {
type: Boolean,
default: false
}
})
let videoDom = null
/* 监听url变化 */
watch(
() => props.url,
(newVal) => {
if (newVal) {
// 创建 WebSocket 连接
props.url && initWebSocket(props.url, props.name)
}
}
)
const initWebSocket = (url, name) => {
getWebsocketPort().then((res) => {
// 获取 canvas 元素
const canvas = document.getElementById(name)
// 创建 JSMpeg.Player 实例
videoDom = new JSMpeg.Player(`ws://${currentPageIP}:20000/socket?path=${encodeURIComponent(url)}`, {
loop: true,
autoplay: true,
audio: false,
videoBufferSize: 512 * 1024, // 512KB
canvas
})
})
}
onMounted(() => {
props.url && initWebSocket(props.url, props.name)
})
onBeforeUnmount(() => {
if (videoDom) {
videoDom.stop()
videoDom.destroy()
console.log('视频连接已关闭')
}
})
</script>
<template>
<div class="video-container">
<canvas :id="props.name" style="width: 100%; height: 100%"></canvas>
<div class="tooltip">132</div>
</div>
</template>
<style scoped lang="scss">
.video-container {
position: relative;
width: 100%;
height: 100%;
.video-buttom {
display: none;
.icon {
padding: 0 15px;
width: 50px;
height: 20px;
cursor: pointer;
}
}
.tooltip {
width: 100%;
height: 40px;
display: none;
position: absolute;
top: 20px; /* 置于文本容器下方 */
right: 20px;
padding: 0 5px;
background-color: rgba($color: #3a3a3a, $alpha: 0.6);
color: #fff;
border-radius: 3px;
white-space: normal; /* 允许换行 */
z-index: 0; /* 确保tooltip在文本容器之下 */
font-size: 14px; /* 可以根据需要调整 */
line-height: 20px;
}
}
</style>
调用
<script setup>
/* 引入videojs样式 */
import 'video.js/dist/video-js.css'
import rtspVideo from '@/components/main/videowall/rtsp-video.vue'
</script>
<template>
<div style="width: 100%; height: 100%">
<rtsp-video :url="你的rtmp或rtsp地址" :name="name1"></rtsp-video>
</div>
</template>