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>

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PlanAPlanB

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值