ffmpeg (fluent-ffmpeg)+Jsmpeg+ws实现web端多画面播放海康威视摄像头rtsp流

1,487 阅读1分钟

前言

需求给到几个摄像头的trsp流地址,希望前端实现在多用户在页面中观看多个摄像头画面,大体上实现的思路是:

流程图-导出 (2).png

在拿到rtsp地址时可以使用VLC播放器测试画面是否正常。

jsmpeg.js采用软解码方式,仅支持mpeg1格式视频、mp2格式音频 ,将视频流解码成图片并渲染到canvas上,别的格式就不要用jsmpeg了。

前端

  1. npm引入JSMpeg npm install jsmpeg-player -S
  2. 在main中把它挂到原型上 import JSMpeg from 'jsmpeg-player'; Vue.prototype.$JSMpeg = JSMpeg;
  3. 页面
// 遍历创建摄像头画面
<div v-for="i in Monitoreo" :key="i.url">
    <canvas :id="`jsmpeg-canvas${i.cameraid}`" style="width: 500px;height: 300px;"></canvas >
</div>

// Monitoreo数组里装的是摄像头端口和摄像头id
Monitoreo:[{  url: 171,cameraid:171 },{ url: 189,cameraid:189 },{ url: 180,cameraid:180 },{ url: 181, cameraid:181 }]

mounted() {
    this.Monitoreo.forEach(item => {
        let url = encodeURI(`rtspUrl=rtsp://账号:密码@172.31.111.${item.url}:554/h264/ch1/main/av_stream`);
        let uuid = encodeURI(`uuid=${uuidv4()}`);  //uuid作为本次链接的id
        let cameraid=`cameraId=${item.cameraid}`;  //cameraid为每个摄像头的编号
        new this.$JSMpeg.Player(`ws://${window.location.hostname}:7730/lisscamera/lab?${url}&${uuid}&${cameraid}`, {
            canvas: document.getElementById(`jsmpeg-canvas${item.cameraid}`),
            autoplay:true,//是否自动播放
            loop:false,
        })
    });
},

服务端

前端要起ffmpeg服务需要用到一个库fluent-ffmpeg,它能直接在服务端起ffmpeg服务,不需要让后端再去起了。

官方文档:www.npmjs.com/package/flu…

中文翻译文档:www.manongjc.com/detail/54-i…

  1. 做一些基础的配置
// 引入fluent-ffmpeg
const ffmpeg = require('fluent-ffmpeg')
const http = require('http')
const WebSocketServer = require('ws').Server
// 用于提取请求链接的参数
const parseUrl = require('url').parse
const {v4: uuidv4} = require('uuid')

const cfg = {
  wsPost: 7730
}
  1. 创建一个http服务用于拉取ffmpeg流
const httpServer = http.createServer((request,response)=>{
  // 获取请求中携带的摄像头id
  const cameraId = request.url.substring(1)
  console.log(`摄像头ID=${cameraId}`)

  request.on('data', data => {
    /*
      data为摄像头的流  
    */
    if(!wsServer || !wsServer._webClient[cameraId] || !wsServer._webClient[cameraId].length){
      console.log(`摄像头ID${cameraId} 无web端请求`)
      return
    }

    wsServer._webClient[cameraId].forEach(wsClient => {
      // 向该摄像头下的所有web端推流
      wsClient.send(data)
    })
  })

  request.on('end', () => {
    console.log(`摄像头ID${cameraId}流已结束,关闭它所有web端的连接`)
    wsServer._closeAll(cameraId)
  })
})

// 启动该http服务并监听7730端口
httpServer.listen(cfg.wsPost, ()=>{
  console.log(`http收流服务已启动,等待websocket连接,正在侦听${cfg.wsPost}`)
})
  1. 创建一个websocket服务,用于推流到web端
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/lisscamera/lab'
})

wsServer._webClient = {}; // 各摄像头画面流与web端的ws映射集
wsServer._rtspClient = {}; // 摄像头画面流对象
  1. 当ws有连接时
wsServer.on('connection', (wsClient, request)=>{
  console.log("——————————————————————————————有ws请求连接——————————————————————————")
  const {rtspUrl, cameraId} = parseUrl(request.url, true).query
  wsClient._cameraId = cameraId
  wsClient._rtspUrl = rtspUrl
  wsClient._uuid = `${cameraId}-${uuidv4()}-${Date.now()}`
  // 当没人请求过该摄像头画面时
  if (!wsServer._webClient[cameraId]) {
    console.log(`摄像头ID${cameraId}没有创建过流,创建流`)
    wsServer._webClient[cameraId] = []
    // _rtspClient中存储的是摄像头id对应该画面流实例,如果最多有100个摄像头,对应的这里最多也会有100个对象属性
    wsServer._rtspClient[cameraId] = wsServer._createFfmpeg(rtspUrl, cameraId)
    wsServer._rtspClient[cameraId].run()
  }
  console.log(`有客户端和摄像头ID${cameraId}建立了ws连接`)
  wsServer._webClient[cameraId].push(wsClient)
  wsClient.on('close', () => {
    console.log(`有客户端主动关闭了和摄像头ID${cameraId}的ws连接`)
    wsServer._closeAll(cameraId, wsClient._uuid)
  })
})
  1. 启动ffmpeg服务
//创建ffmpeg服务进行收流,把rtsp流转成mpeg1格式视频、mp2格式音频的流
wsServer._createFfmpeg = (rtspUrl, cameraId) => {
  return ffmpeg(rtspUrl)
    .setFfmpegPath('./ffmpeg/ffmpeg.exe')
    .addOption([
      //'-rtsp_transport tcp',
      '-q 0',
      '-f mpegts',
      '-codec:v mpeg1video',
      '-s 500x300',
      '-b:v 128k',
      '-an'
    ])
    .output(`http://127.0.0.1:${cfg.wsPost}/${cameraId}`)
    .on('start', () => {
      console.log(`已启动${rtspUrl}拉流转码,cameraId=${cameraId}`)
    })
    .on('error', (err) => {
      console.log(`${rtspUrl}拉流转码错误,cameraId=${cameraId}`, err.message)
      wsServer._closeAll(cameraId)
    })
    .on('end',  () => {
      console.log(`${rtspUrl}拉流转码结束,cameraId=${cameraId}`)
      wsServer._closeAll(cameraId)
    })
}
  1. ws的关闭方法
wsServer._closeAll = function (cameraId, uuid) {
  console.log("cameraId,uuid",cameraId, uuid)

  if(uuid) {
    console.log(`有客户端单独关闭了和${cameraId}的ws`)
    let wsClient = null
    let wsCIndex = wsServer._webClient[cameraId].findIndex(item => {
      if (item._uuid === uuid) {
        wsClient = item
        return true
      }
      return false
    })

    if(wsCIndex > -1) {
      wsClient.close(4000, '连接已关闭')
      wsServer._webClient[cameraId].splice(wsCIndex, 1)
    }
  }

  // 无uuid传进来代表整个客户端直接关闭了
  if(!uuid) {
    if(!wsServer._webClient[cameraId]) return
    console.log(`关闭所有cameraId=${cameraId}`)
    wsServer._webClient[cameraId].forEach(wsClient => {
      wsClient.close(4000, '连接已关闭')
    })
    wsServer._webClient[cameraId]=[]
  }

  if(!wsServer._webClient[cameraId].length) {
    // 如果某一摄像头画面无任何终端请求,则结束该摄像头的rtsp流
    if(wsServer._rtspClient[cameraId]){
      wsServer._rtspClient[cameraId].kill('SIGKILL')
      delete wsServer._rtspClient[cameraId]
    }
    delete wsServer._webClient[cameraId]
    console.log(`结束了摄像头${cameraId}的视频流`)
  }
}