nodejs vue rtsp流网页播放方案

支持摄像头多开,点击全屏播放,窗口拖拽,操控摄像头上下左右旋转放大缩小等操作
1安装

  1. Ffmpeg,用来解码视频,下载完后需添加环境变量
https://ffmpeg.org/releases/ffmpeg-4.0.1.tar.bz2
  1. Node.js,搭建webSocket服务器,下载完后需添加环境变量
https://nodejs.org/dist/v8.11.3/node-v8.11.3-x64.msi
  1. jsmpeg,运行主程序
https://codeload.github.com/phoboslab/jsmpeg/zip/master

2使用

2-1.运行jsmpeg

运行jsmpeg内部的websocket-relay.js

在运行websocket-relay.js之前node需要安装webSocket模块
在cmd控制台输入:

 npm install ws -g

jsmpeg所在路径,执行:

node websocket-relay.js supersecret 8081 8082

Supersecret是密码

8081是ffmpeg推送端口

8082是前端webSocket端口

2-2.运行ffmpeg

ffmpeg -rtsp_transport tcp -i rtsp://admin:Szzgkon2016@192.168.1.50:554/h264/ch1/sub/av_stream  -c copy -q 0 -map 0:0 -f mpegts -codec:v mpeg1video http://127.0.0.1:9991/supersecret

关键点:

-rtsp_transport tcp:使用tcp强解码rtsp流,防止防火墙之类的问题造成推流中断

-c copy 操作rtsp流,直接复制推流,不写会报找不到rtsp解码器的错(因为ffmpeg不知道用什么处理rtsp)

-map 0:0:-map指定哪些流做为输入, 0:0 表示第0个输入文件的第0个流(解决10秒延迟问题)

-f mpegts -codec:v mpeg1video:编码方式(必须这样写jsmpeg才能识别)

3:html

通过使用node-onvif操作onvif协议的摄像头


https://github.com/futomi/node-onvif
$ npm install -s node-onvif

4:实战项目请看下一篇

技术需求请看上一篇文章,这篇使用vue实现视频监控(可直接复制代码运行)
支持摄像头多开,点击全屏播放,窗口拖拽,操控摄像头上下左右旋转放大缩小等操作
vue父组件页面

<template>
  <div class="monitorPageBox">
    <div class="controlHide" v-if="controlShow">
      <i
        class="el-icon-d-arrow-right"
        @click="controlShow = false"
        title="展开控制台"
        style="cursor:pointer"
      ></i>
    </div>
    <div class="monitorList" v-else>
      <div class="monitorListTitleBox">
        <div class="monitorListTitle">
          当前摄像头
        </div>
        <div class="monitorListIcon">
          <i
            class="el-icon-d-arrow-left"
            @click="controlShow = true"
            title="隐藏控制台"
            style="cursor:pointer"
          ></i>
        </div>
      </div>
      <div class="displaysTypeBox">
        <div
          class="displaysNumber"
          v-for="(item, index) in displaysType"
          :key="index"
          :class="displaysTypeStyle(index)"
        >
          <div @click="displaysTypeClick(index)" style="cursor:pointer">
            {{ item }}
          </div>
        </div>
      </div>
      <div class="searchInputBox">
        <el-input placeholder="输入摄像头名称" v-model="monitorName"
          ><el-button
            slot="append"
            icon="el-icon-search"
            @click="queryEquipment"
          ></el-button
        ></el-input>
      </div>
      <div class="monitorEquipmentDataBox">
        <el-scrollbar style="height: 100%">
          <div
            class="monitorEquipmentBox"
            v-for="(item, index) in videoList"
            :key="index"
            @dblclick="monitorControl(item)"
            style="cursor:pointer"
            :title="
              monitorPlay(item.id) ? `双击关闭${item.ip}` : `双击播放${item.ip}`
            "
          >
            <div class="monitorIPText" draggable="true" @dragstart="drag(item)">
              <span>{{ item.ip }} </span>
              <span v-if="monitorPlay(item.id)"
                >--摄像头{{ playMonitor(item.id) }}</span
              >
            </div>
            <div class="monitorControlBox">
              <i
                class="el-icon-circle-close"
                v-if="monitorPlay(item.id)"
                style="color:#ff3b30;"
              ></i>
              <i class="el-icon-video-play" style="color:#1c9eff;" v-else></i>
            </div>
          </div>
        </el-scrollbar>
      </div>
      <div class="controlBox" v-if="selectMonitorID">
        <div class="directionBox">
          <div
            class="directionButton"
            v-for="(item, index) in directionData"
            :key="index"
            @click="index == 4 ? rotatePreset('右') : ''"
            @mousedown="index != 4 ? move($event, index) : ''"
            @mouseup="end('direction')"
            @mouseout="end('direction')"
          >
            {{ item }}
          </div>
        </div>
        <div class="focusingBox">
          <div
            class="equipmentOperationBox"
            v-for="(item, index) in equipmentOperationData"
            :key="index"
            @mousedown="equipmentOperation($event, index)"
            @mouseup="index < 2 ? end('operation') : ''"
            @mouseout="index < 2 ? end('operation') : ''"
          >
            {{ item }}
          </div>
          <div class="rotateTimeoutBox">
            <el-input-number
              v-model="timeout"
              style="width: 100px"
            ></el-input-number>
          </div>
        </div>
      </div>
      <div class="speedSliderBox" v-if="selectMonitorID">
        <el-slider v-model="moveSpeed" :min="20"></el-slider>
      </div>
      <div class="presuppositionBox" v-if="selectMonitorID">
        <el-scrollbar style="height: 100%">
          <div
            class="presupposition"
            v-for="item in presuppositionData"
            :key="item.$.token"
          >
            <div
              class="presuppositionText"
              :style="existencePreset(item.PTZPosition) ? 'color:#bbbbbb' : ''"
            >
              {{ item.Name }}
            </div>
            <div
              class="presuppositionIcon"
              v-if="presetSetUp(item.$.token)"
              :style="existencePreset(item.PTZPosition) ? 'color:#bbbbbb' : ''"
            >
              <div class="IconBox">
                <i
                  class="el-icon-more"
                  title="功能"
                  @click="openPreset = item.$.token"
                ></i>
              </div>
            </div>
            <div class="presuppositionIcon" v-else>
              <div class="IconBox">
                <i
                  class="el-icon-s-tools"
                  title="设置"
                  @click="setPreset(item)"
                  :style="
                    existencePreset(item.PTZPosition) ? 'color:#bbbbbb' : ''
                  "
                ></i>
              </div>
              <!-- <div class="IconBox">
                <i
                  title="删除"
                  @click="removePreset(item)"
                  class="el-icon-error"
                ></i>
              </div> -->
              <div class="IconBox">
                <i
                  v-if="!existencePreset(item.PTZPosition)"
                  title="前往"
                  @click="gotoPreset(item)"
                  class="el-icon-s-promotion"
                ></i>
              </div>
            </div>
          </div>
        </el-scrollbar>
      </div>
    </div>
    <div class="monitorShowBox" id="monitorBox">
      <div
        v-for="item in displaysNumber"
        :key="item"
        :style="monitorNumberStyle(canvasStyle)"
        class="monitorRevealBox"
        :class="
          selectMonitor(videoData[item - 1] ? videoData[item - 1].id : false)
        "
        @dragover.prevent="allowDrop($event)"
        @drop="drop(item - 1)"
      >
        <div
          v-if="videoPlay(item - 1)"
          @click="selectVideo(videoData[item - 1].id)"
        >
          <canvasVideo
            :videoId="`video-canvas` + videoData[item - 1].id"
            :canvasData="videoData[item - 1]"
            :width="canvasStyle.canvasWidth"
            :height="canvasStyle.canvasHeight"
          ></canvasVideo>
        </div>
        <div v-else>请添加摄像头{{ item }}</div>
      </div>
    </div>
  </div>
</template>

<script>
import canvasVideo from "../../components/videoPage/canvasVideo"; //视频测试页
import elementResizeDetectorMaker from "element-resize-detector"; //element元素宽高变化
export default {
  name: "monitorPage",
  components: {
    canvasVideo
  },
  data() {
    return {
      videoData: [""], //视频的数组
      monitorName: "", //查询的设备名称
      canvasStyle: {
        //摄像头样式
        canvasWidth: `0px`,
        canvasHeight: `0px`
      },
      //摄像头方向数据
      directionData: [
        "左上",
        "向上",
        "上右",
        "向左",
        "旋转",
        "向右",
        "左下",
        "向下",
        "下右"
      ],
      //设备功能数据
      equipmentOperationData: ["放大", "缩小", "左旋", "右旋"],
      controlShow: false, //控制台显影
      //摄像头列表
      videoList: [
        {
          id: 90,
          name: `admin`,
          password: `Szzgkon2016`,
          ip: `192.168.1.50`,
          flow: "ch1"
        },
        {
          id: 91,
          name: `admin`,
          password: `Szzgkon@2016`,
          ip: `192.168.1.51`,
          flow: "ch1"
        },
        {
          id: 92,
          name: `admin`,
          password: `Szzgkon2016`,
          ip: `192.168.1.50`,
          flow: "ch1"
        },
        {
          id: 93,
          name: `admin`,
          password: `Szzgkon@2016`,
          ip: `192.168.1.51`,
          flow: "ch1"
        },
        {
          id: 94,
          name: `admin`,
          password: `Szzgkon2016`,
          ip: `192.168.1.50`,
          flow: "ch1"
        },
        {
          id: 95,
          name: `admin`,
          password: `Szzgkon@2016`,
          ip: `192.168.1.51`,
          flow: "ch1"
        },
        {
          id: 96,
          name: `admin`,
          password: `Szzgkon2016`,
          ip: `192.168.1.50`,
          flow: "ch1"
        },
        {
          id: 97,
          name: `admin`,
          password: `Szzgkon@2016`,
          ip: `192.168.1.51`,
          flow: "ch1"
        },
        {
          id: 98,
          name: `admin`,
          password: `Szzgkon2016`,
          ip: `192.168.1.50`,
          flow: "ch1"
        },
        {
          id: 99,
          name: `admin`,
          password: `szzgkon@2016`,
          ip: `192.168.0.55`,
          flow: "ch33"
        }
      ],
      displaysType: ["1*1", "2*2", "3*2", "3*3"], //摄像头的展示数量
      displaysTypeIndex: 0, //选中的摄像头展示数量
      displaysNumber: 0, //展示摄像头的数量
      moveData: {}, //拖拽的数据
      moveTimer: null, //定时器
      moveSpeed: 75, //默认移动速度
      selectMonitorID: null, //选中的摄像头
      device: { xaddr: "", user: "", pass: "" }, //当前选中的摄像头数据
      presuppositionData: [], //预测点数据
      openPreset: null, //设置预置点
      timeout: 10 //摄像头旋转时间
    };
  },
  mounted() {
    //添加element动态改变摄像展示页大小
    this.erd = elementResizeDetectorMaker();
    let _this = this;
    _this.$nextTick(() => {
      _this.erd.listenTo(document.getElementById("monitorBox"), element => {
        let timer = setTimeout(function() {
          if (!_this.checkFull()) {
            _this.canvasStyleChange();
          }
        }, 100);
      });
    });
    _this.displaysNumber = 1;
    this.$socket.open();//局部引入socket
  },
  beforeDestroy() {
    this.$socket.close();//退出组件时关闭socket
  },
  sockets: {
    // 连接后台socket
    connect() {
      console.log("socket 连接成功");
    },
    //获取测试数据
    devicePresupposition(data) {
      this.presuppositionData = data.GetPresetsResponse.Preset;
    }
  },
  watch: {
    selectMonitorID(nval, oval) {
      let deviceData = this.videoList.filter(item => {
        return item.id == this.selectMonitorID;
      });
      this.device = {
        xaddr: `http://${deviceData[0].ip}/onvif/device_service`,
        user: deviceData[0].name,
        pass: deviceData[0].password
      };
    }
  },
  methods: {
    //摄像头旋转
    rotatePreset(type) {
      let speedX;
      if (type == "左") {
        speedX = -this.moveSpeed / 100;
      } else {
        speedX = this.moveSpeed / 100;
      }
      this.$socket.emit("rotatePreset", this.device, speedX, this.timeout);
    },
    //预置点是否存在
    existencePreset(item) {
      let x = item.PanTilt.$.x;
      let y = item.PanTilt.$.y;
      let z = item.Zoom.$.x;
      if (x == y && z == 0) {
        return true;
      } else {
        return false;
      }
    },
    //设置预设点
    setPreset(data) {
      this.openPreset = null;
      this.$socket.emit("setPreset", this.device, data);
    },
    //删除预置点
    removePreset(data) {
      this.openPreset = null;
      this.$socket.emit("removePreset", this.device, data);
    },
    //前往预设点
    gotoPreset(data) {
      this.openPreset = null;
      this.$socket.emit("gotoPreset", this.device, data);
    },
    //打开预置点功能区
    presetSetUp(token) {
      if (this.openPreset == token) {
        return false;
      } else {
        return true;
      }
    },
    //设备操作按钮
    equipmentOperation(e, index) {
      if (!this.selectMonitorID) {
        this.$message({
          message: "尚未选择摄像头",
          type: "warning"
        });
        return;
      }
      if (index == 0 || index == 1) {
        this.operatioDirection(index);
        this.moveTimer = setInterval(() => {
          this.operatioDirection(index);
        }, 500);
      } else {
        if (index == 2) {
          this.rotatePreset("左");
        } else if (index == 3) {
          this.rotatePreset("右");
        }
      }
    },
    //设备操作
    operatioDirection(index) {
      let z = 1;
      if (index == 0) {
        z = this.moveSpeed / 100;
      } else if (index == 1) {
        z = -this.moveSpeed / 100;
      }
      let speed = { x: 0, y: 0, z: z };
      this.$socket.emit("move", this.device, speed);
    },
    //按下移动摄像头
    move(e, index) {
      if (!this.selectMonitorID) {
        this.$message({
          message: "尚未选择摄像头",
          type: "warning"
        });
        return;
      }
      this.moveDirection(index);
      this.moveTimer = setInterval(() => {
        this.moveDirection(index);
      }, 500);
    },
    //释放停止移动摄像头
    end(type) {
      if (!this.selectMonitorID) {
        return;
      }
      if (!this.moveTimer) {
        return;
      }
      window.clearInterval(this.moveTimer);
      this.moveTimer = null;
      this.$socket.emit("stop", this.device);
    },
    //获取预设点
    getPresupposition() {
      this.$socket.emit("presupposition", this.device);
    },
    //摄像头移动的方向
    moveDirection(type) {
      let x = 0;
      let y = 0;
      if (type == 0) {
        x = -this.moveSpeed / 100;
        y = this.moveSpeed / 100;
      } else if (type == 1) {
        y = this.moveSpeed / 100;
      } else if (type == 2) {
        x = this.moveSpeed / 100;
        y = this.moveSpeed / 100;
      } else if (type == 3) {
        x = -this.moveSpeed / 100;
      } else if (type == 5) {
        x = this.moveSpeed / 100;
      } else if (type == 6) {
        x = -this.moveSpeed / 100;
        y = -this.moveSpeed / 100;
      } else if (type == 7) {
        y = -this.moveSpeed / 100;
      } else if (type == 8) {
        x = this.moveSpeed / 100;
        y = -this.moveSpeed / 100;
      }
      let speed = { x: x, y: y, z: 0 };
      this.$socket.emit("move", this.device, speed);
    },
    //当前选中的摄像头
    selectMonitor(id) {
      if (id == this.selectMonitorID) {
        return "selectMonitorClass";
      }
    },
    //点击选中的摄像头
    selectVideo(id) {
      this.selectMonitorID = id;
      this.getPresupposition();
    },
    //拖拽触发
    drag(item) {
      this.moveData = item;
    },
    //拖拽时触发
    allowDrop(e) {},
    //拖拽释放
    drop(index) {
      this.monitorModify(this.moveData, index);
    },
    //播放的摄像位置
    playMonitor(id) {
      let num;
      this.videoData.forEach((item, index) => {
        if (item.id == id) {
          num = index;
        }
      });
      return num + 1;
    },
    // 判断全屏
    checkFull() {
      //判断浏览器是否处于全屏状态 (需要考虑兼容问题)
      //火狐浏览器
      let isFull =
        document.mozFullScreen ||
        document.fullScreen ||
        //谷歌浏览器及Webkit内核浏览器
        document.webkitIsFullScreen ||
        document.webkitRequestFullScreen ||
        document.mozRequestFullScreen ||
        document.msFullscreenEnabled;
      if (isFull === undefined) {
        isFull = false;
      }
      return isFull;
    },
    //该视频是否正在播放
    monitorPlay(id) {
      let play = false;
      this.videoData.forEach(item => {
        if (item.id == id) {
          play = true;
        }
      });
      return play;
    },
    //当前播放的视频
    videoPlay(index) {
      let play = false;
      if (this.videoData[index]) {
        play = true;
      }
      if (this.videoData[index] == "") {
        play = false;
      }
      return play;
    },
    //操作摄像头
    monitorControl(item) {
      let _this = this;
      if (_this.monitorPlay(item.id)) {
        _this.monitorClose(item.id);
      } else {
        _this.monitorData(item);
      }
    },
    //查询摄像头
    queryEquipment() {
      console.log("查询" + this.monitorName);
    },
    //摄像头切换
    pageChange(video) {
      let params = [];
      video.forEach(item => {
        params.push({
          id: item.id,
          ip: item.ip,
          rtsp: `rtsp://${item.name}:${item.password}@${item.ip}:554/h264/${item.flow}/sub/av_stream`
        });
      });
      this.axios
        .post("http://localhost:3120/open", params)
        .then(req => {
          let data = req.data;
          this.videoData = [];
          data.forEach(item => {
            this.videoData.push({ id: item.id, port: item.port });
          });
        })
        .catch(err => {
          console.log(err);
        });
    },
    //传递摄像头数据
    monitorData(video) {
      let play = true;
      for (let i = 0; i < this.videoData.length; i++) {
        if (this.videoData[i] == "") {
          play = false;
          break;
        }
      }
      if (play && this.videoData.length == this.displaysNumber) {
        this.$message({
          message: "页面摄像头数已满,请切换摄像头或关闭摄像头后再添加",
          type: "warning"
        });
        return;
      }
      let params = [];
      params.push({
        id: video.id,
        ip: video.ip,
        rtsp: `rtsp://${video.name}:${video.password}@${video.ip}:554/h264/${video.flow}/sub/av_stream`
      });
      console.log("params", params);
      this.selectMonitorID = video.id;
      this.axios
        .post("http://localhost:3120/open", params)
        .then(req => {
          let data = req.data;
          let video = true;
          for (let i = 0; i < this.videoData.length; i++) {
            if (this.videoData[i] == "") {
              this.videoData.splice(i, 1, data[0]);
              video = false;
              break;
            }
          }
          if (video) {
            this.videoData = this.videoData.concat(data);
          }
          this.getPresupposition();
        })
        .catch(err => {
          console.log(err);
        });
    },
    //关闭视频
    monitorClose(id) {
      for (let i = 0; i < this.videoData.length; i++) {
        if (this.videoData[i].id == id) {
          this.videoData.splice(i, 1, "");
          break;
        }
      }
      this.selectMonitorID = null;
      this.presuppositionData = [];
    },
    //切换视频
    monitorModify(item, index) {
      let rtsp = `rtsp://${item.name}:${item.password}@${item.ip}:554/h264/${item.flow}/sub/av_stream`;
      let params = { id: item.id, ip: item.ip, rtsp: rtsp };
      this.selectMonitorID = item.id;
      this.axios
        .post("http://localhost:3120/modify", params)
        .then(req => {
          let data = req.data;
          this.videoData.splice(index, 1, "");
          this.videoData.forEach((item, index) => {
            if (item.id == data.id) {
              this.videoData.splice(index, 1, "");
            }
          });
          this.$nextTick(() => {
            this.videoData.splice(index, 1, {
              id: data.id,
              port: data.port
            });
            this.getPresupposition();
          });
        })
        .catch(err => {
          console.log(err);
        });
    },
    //选中的摄像头展示类型
    displaysTypeStyle(index) {
      if (this.displaysTypeIndex == index) {
        return "selectDisplaysType";
      }
    },
    //摄像头的数量类型
    displaysTypeClick(index) {
      this.displaysTypeIndex = index;
      if (this.displaysTypeIndex == 0) {
        this.displaysNumber = 1;
      } else if (this.displaysTypeIndex == 1) {
        this.displaysNumber = 4;
      } else if (this.displaysTypeIndex == 2) {
        this.displaysNumber = 6;
      } else {
        this.displaysNumber = 9;
      }
      this.videoPlayNum(this.displaysNumber);
      this.canvasStyleChange();
    },
    //切换摄像头数量
    videoPlayNum(num) {
      let _this = this;
      let video = JSON.parse(JSON.stringify(_this.videoData));
      _this.videoData = [];
      this.$nextTick(() => {
        for (let i = 0; i < video.length; i++) {
          if (video[i] != "") {
            this.videoData.push(video[i]);
            if (_this.videoData.length == num) {
              break;
            }
          }
        }
        for (let i = 0; i < num; i++) {
          if (_this.videoData.length == num) {
            break;
          } else {
            _this.videoData.push("");
          }
        }
      });
    },
    //可展示的摄像组盒子
    monitorNumberStyle(canvasStyle) {
      let style;
      const monitorBox = document.getElementById("monitorBox");
      let width = monitorBox.getBoundingClientRect().width - 2;
      let height = monitorBox.getBoundingClientRect().height - 2;
      if (this.displaysTypeIndex == 0) {
        style = `width:${width}px; height:${height}px;`;
      } else if (this.displaysTypeIndex == 1) {
        style = `width:${width / 2}px; height:${height / 2}px;`;
      } else if (this.displaysTypeIndex == 2) {
        style = `width:${width / 3}px; height:${height / 2}px;`;
      } else {
        style = `width:${width / 3}px; height:${height / 3}px;`;
      }
      return style;
    },
    //摄像头展示数量
    monitorBoxStyle(canvasStyle) {
      return `width:${canvasStyle.canvasWidth}px; height:${canvasStyle.canvasHeight}px;float:left`;
    },
    //摄像页面宽高变化
    canvasStyleChange() {
      const monitorBox = document.getElementById("monitorBox");
      let width = monitorBox.getBoundingClientRect().width;
      let height = monitorBox.getBoundingClientRect().height;
      if (this.displaysTypeIndex == 0) {
        this.canvasStyle = {
          canvasWidth: width - 4 + `px`,
          canvasHeight: height - 4 + `px`
        };
      } else if (this.displaysTypeIndex == 1) {
        this.canvasStyle = {
          canvasWidth: (width - 6) / 2 + `px`,
          canvasHeight: (height - 6) / 2 + `px`
        };
      } else if (this.displaysTypeIndex == 2) {
        this.canvasStyle = {
          canvasWidth: (width - 8) / 3 + `px`,
          canvasHeight: (height - 6) / 2 + `px`
        };
      } else {
        this.canvasStyle = {
          canvasWidth: (width - 8) / 3 + `px`,
          canvasHeight: (height - 8) / 3 + `px`
        };
      }
    }
  }
};
</script>

<style lang="less" scoped>
.box(@width:"100%",@height:"100%",@size:"14px") {
  width: @width;
  height: @height;
  font-size: @size;
}
.textBox(@width,@height,@align:center) {
  width: @width;
  height: @height;
  line-height: @height;
  text-align: @align;
}
.monitorPageBox {
  display: flex;
  width: 100%;
  height: 87vh;
  overflow: auto;
  min-width: 1500px;
  background-color: #edf0ef;
  .controlHide {
    flex: 0.05;
    background-color: #fff;
    margin-right: 15px;
    border-radius: 5px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .monitorList {
    flex: 0.8;
    background-color: #fff;
    margin-right: 15px;
    border-radius: 5px;
    padding: 20px 10px;
    .monitorListTitleBox {
      margin-bottom: 0.677rem;
      display: flex;
      .monitorListTitle {
        font-size: 16px;
        user-select: none;
        flex: 9;
      }
      .monitorListIcon {
        flex: 1;
      }
    }

    .displaysTypeBox {
      .box(280px, 32px);
      margin-bottom: 0.677rem;
      display: flex;
      .displaysNumber {
        border: 1px solid #efefef;
        font-size: 0.73rem;
        flex: 1;
        text-align: center;
        line-height: 1.67rem;
      }
      .selectDisplaysType {
        background-color: #05c399;
        color: #fff;
      }
    }
    .searchInputBox {
      .box(280px, 32px);
      margin-bottom: 0.677rem;
    }
    .monitorEquipmentDataBox {
      height: calc(100% - 450px);
      .monitorEquipmentBox {
        font-size: 0.731rem;
        color: rgba(78, 82, 79, 1);
        line-height: 1.562rem;
        padding-left: 15px;
        .box(100%, 32px);
        display: flex;
        .monitorIPText {
          flex: 9;
        }
        .monitorControlBox {
          flex: 1;
        }
      }
    }
    .controlBox {
      display: flex;
      .directionBox {
        flex: 1.5;
        .box(150px, 150px);
        .directionButton {
          .textBox(50px, 50px);
          background-color: #edf0ef;
          cursor: pointer;
          border-radius: 50%;
          float: left;
          border: 1px #05c399 solid;
          -webkit-user-select: none;
          -moz-user-select: none;
          -ms-user-select: none;
          user-select: none;
        }
      }
      .focusingBox {
        flex: 1;
        .equipmentOperationBox {
          .textBox(50px, 50px);
          background-color: #edf0ef;
          cursor: pointer;
          border-radius: 50%;
          float: left;
          border: 1px #05c399 solid;
          -webkit-user-select: none;
          -moz-user-select: none;
          -ms-user-select: none;
          user-select: none;
        }
        .rotateTimeoutBox {
          .box(100px, 50px);
        }
        /deep/ .el-input-number__decrease {
          width: 20px;
        }
        /deep/ .el-input-number__increase {
          width: 20px;
        }
        /deep/ .el-input__inner {
          width: 100px;
          padding: 0;
        }
      }
    }
    .speedSliderBox {
    }
    .presuppositionBox {
      height: 150px;
      overflow: auto;
      .presupposition {
        padding: 0 15px;
        display: flex;
        .presuppositionText {
          flex: 1;
          font-size: 0.731rem;
          color: rgba(78, 82, 79, 1);
          line-height: 1.562rem;
        }
        .presuppositionIcon {
          flex: 1;
          .IconBox {
            .box(30px, 30px);
            padding: 7px;
            cursor: pointer;
            float: right;
          }
        }
      }
    }
  }
  .monitorShowBox {
    flex: 4;
    background-color: #fff;
    border-radius: 5px;
    overflow: hidden;
    border: 1px #333 solid;
    .monitorRevealBox {
      float: left;
      border: 1px solid #333;
    }
    .selectMonitorClass {
      border: 1px solid red;
    }
  }
}
</style>
<style>
.searchInputBox .el-input__inner {
  background: #fff;
  border-radius: 0.206rem;
  height: 1.67rem;
}
</style>
<style>
.el-slider__runway.disabled .el-slider__bar {
  background-color: #05c399;
}
.el-slider__button {
  background: #fff;
  border: #05c399 2px solid;
}
.el-slider__bar {
  background-color: #05c399;
}
.el-slider__runway {
  background-color: #edf0ef;
}
</style>

vue子组件页面

<template>
  <div class="video" ref="vcontainer" @dblclick="toggleFullscreen()">
    <canvas
      class="video__player"
      :id="videoId"
      :style="`width:${width};height:${height}`"
      >您的浏览器暂不支持Canvas,请更换浏览器后再试</canvas
    >
  </div>
</template>

<script>
import maskBox from "../layout/maskBox"; //弹窗层
export default {
  name: "canvasVideo",
  components: { maskBox },
  props: ["canvasData", "videoId", "width", "height"], //传入的视频连接
  data() {
    return {
      players: null //视频播放器
    };
  },
  mounted() {
    this.start();
  },
  destroyed() {
    this.players.destroy();
  },
  methods: {
    //加载视频
    start() {
      const canvas = document.getElementById(this.videoId);
      let urls = `ws://172.16.10.81:` + this.canvasData.port;
      this.players = new JSMpeg.Player(urls, {
        canvas: canvas,
        autoplay: true
      });
    },
    FontChart(res) {
      //获取到屏幕的宽度
      let clientWidth =
        window.innerWidth ||
        document.documentElement.clientWidth ||
        document.body.clientWidth;
      if (!clientWidth) return; //报错拦截:
      let fontSize = 1;
      if (clientWidth > 1920) fontSize = clientWidth / 1920;
      return res * fontSize;
    },
    //全屏播放
    toggleFullscreen() {
      const isFullscreen = document.webkitIsFullScreen || document.fullscreen;
      const canvas = document.getElementById(this.videoId);
      if (isFullscreen) {
        const exitFunc =
          document.exitFullscreen || document.webkitExitFullscreen;
        exitFunc.call(document);
        canvas.style = `width:${this.width};height:${this.height}`;
        window.onresize = "";
      } else {
        const element = this.$refs.vcontainer;
        const fullscreenFunc =
          element.requestFullscreen || element.webkitRequestFullScreen;
        fullscreenFunc.call(element);
        canvas.style = "";
        this.windowOnresize();
      }
    },
    //添加全屏监控事件
    windowOnresize() {
      const canvas = document.getElementById(this.videoId);
      let _this = this;
      window.onresize = () => {
        if (!_this.checkFull()) {
          canvas.style = `width:${_this.width};height:${_this.height}`;
          window.onresize = "";
        }
      };
    },
    // 判断全屏
    checkFull() {
      //判断浏览器是否处于全屏状态 (需要考虑兼容问题)
      //火狐浏览器
      let isFull =
        document.mozFullScreen ||
        document.fullScreen ||
        //谷歌浏览器及Webkit内核浏览器
        document.webkitIsFullScreen ||
        document.webkitRequestFullScreen ||
        document.mozRequestFullScreen ||
        document.msFullscreenEnabled;
      if (isFull === undefined) {
        isFull = false;
      }
      return isFull;
    }
  }
};
</script>
<style scoped>
.video {
  position: relative;
  width: 100%;
  height: 100%;
}
.video__player {
  width: 100%;
  height: 100%;
  display: flex;
}
</style>

Node页

//导入子进程模块
const child_process = require('child_process');
const exec = child_process.exec;
const cors = require('cors')
const bodyParser = require('body-parser');
const express = require('express');
const onvif = require('node-onvif');
const app = express();
// 开启socket服务
let server = app.listen(3120, () => {
    console.log("服务器3120启动");
})
// socket 初始化
const io = require("socket.io")(server, { cors: true })
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
var portId = 9000//起始的端口号
var openArr = []//正在工作的摄像机组
var onLinePort = []//在线的端口
//新增摄像机
app.post('/open', function (req, res) {
    var videoArr = []
    req.body.forEach((item) => {
        let portNum = null
        for (var i = 0; i < openArr.length; i++) {
            if (openArr[i].ip == item.ip) {
                portNum = openArr[i].portId
                break
            }
        }
        let port = portNum ? portNum : gainPortNum(item.id, item.ip, item.rtsp)
        videoArr.push({ id: item.id, port: port + 1 })
    })
    res.json(videoArr);
});
//切换摄像机
app.post('/modify', function (req, res) {
    let portNum = null
    for (var i = 0; i < openArr.length; i++) {
        if (openArr[i].ip == req.body.ip) {
            portNum = openArr[i].portId
            break
        }
    }
    let port = portNum ? portNum : gainPortNum(req.body.id, req.body.ip, req.body.rtsp)
    res.json({ id: req.body.id, port: port + 1 });
});

//connection为自带的方法,类似生命周期里面的创建,连接后就会触发
io.on("connection", function (socket) {
    console.log('一个用户与服务器建立连接', socket.handshake.query.id.toString())
    socket.join(socket.handshake.query.id.toString());
    // 接收到移动摄像头指令
    socket.on("move", function (deviceData, speed) {
        deviceMove(deviceData, speed)
    })
    // 接收到停止移动摄像头指令
    socket.on("stop", function (deviceData) {
        deviceStop(deviceData)
    })
    // 旋转摄像头指令
    socket.on("rotatePreset", function (deviceData, speedX, timeout) {
        deviceRotate(deviceData, speedX, timeout)
    })
    //摄像头预置点设置
    socket.on("setPreset", function (deviceData, data) {
        let device = new onvif.OnvifDevice({
            xaddr: deviceData.xaddr,
            user: deviceData.user,
            pass: deviceData.pass
        });
        device.init().then(() => {
            let ptz = device.services.ptz;
            if (!ptz) {
                throw new Error('当前ONVIF网络摄像机不支持云台服务');
            }
            let profile = device.getCurrentProfile();
            let params = {
                'ProfileToken': profile['token'],
                'PresetToken': data.$.token,
            };
            device.services.ptz.setPreset(params).then((result) => {
                let params2 = {
                    'ProfileToken': profile['token']
                };
                device.services.ptz.getPresets(params2).then((result) => {
                    socket.emit('devicePresupposition', result['data'])
                }).catch((error) => {
                    console.error(error);
                });
            }).catch((error) => {
                console.error(error);
            });
        }).catch((error) => {
            console.error(error);
        });
    })
    //摄像头删除预置点
    socket.on("removePreset", function (deviceData, data) {
        removeDevicePreset(deviceData, data)
    })
    //摄像头前往预置点
    socket.on("gotoPreset", function (deviceData, data) {
        gotoDevicePreset(deviceData, data)
    })
    // 接收到获取预设点指令
    socket.on("presupposition", function (deviceData) {
        let device = new onvif.OnvifDevice({
            xaddr: deviceData.xaddr,
            user: deviceData.user,
            pass: deviceData.pass
        });
        device.init().then(() => {
            let ptz = device.services.ptz;
            if (!ptz) {
                throw new Error('当前ONVIF网络摄像机不支持云台服务');
            }
            let profile = device.getCurrentProfile();
            let params = {
                'ProfileToken': profile['token']
            };
            device.services.ptz.getPresets(params).then((result) => {
                socket.emit('devicePresupposition', result['data'])
            }).catch((error) => {
                console.error(error);
            });
        }).catch((error) => {
            console.error("大错误", error);
        });
    })
    // 当关闭连接后触发 disconnect 事件
    socket.on('disconnect', function () {
        console.log(socket.handshake.query.id.toString(), '与服务器断开连接');
    });
})

//新增视频流
function videoAdd(id, ip, rtsp, portNumber) {
    var websocket = `node websocket-relay.js supersecret ${portNumber} ${portNumber + 1}`
    var ffmpeg = `ffmpeg -rtsp_transport tcp -i ${rtsp} -s 1280x720 -c copy -q 0 -map 0:0 -f mpegts -codec:v mpeg1video http://127.0.0.1:${portNumber}/supersecret`
    var websocket = execute('websocket', websocket, portNumber + 1);
    var ffmpeg = execute('ffmpeg', ffmpeg, portNumber)
    openArr.push({ ip: ip, id: id, portId: portNumber, ffmpeg: ffmpeg })
    onLinePort.push(portNumber + 1)
    onLinePort = onLinePort.sort((n1, n2) => { return n1 - n2; })
}
//获取使用的端口
function gainPortNum(id, ip, rtsp) {
    var port = (portId - 9000) / 2
    var portNum
    if (onLinePort.length != port) {
        var startNum = 8999
        for (var i = 0; i < onLinePort.length; i++) {
            if (startNum == onLinePort[i] - 2) {
                startNum = onLinePort[i]
                portNum = startNum + 1
            } else {
                portNum = startNum + 1
                break
            }
        }
    } else {
        portNum = portId
        portId = portId + 2
    }
    videoAdd(id, ip, rtsp, portNum)
    return portNum
}
/**
 * 执行cmd命令
 * @param {*} cmd 传入的cmd
 */
function execute(type, cmd, port) {
    var last = exec(cmd);
    last.stdout.on('data', function (data) {
        console.log(type + port + ' : ' + data);
        if (data.length == 6) {
            openArr.forEach((item, index) => {
                if (item.portId == (port - 1)) {
                    item.ffmpeg.stdin.write('q');
                    openArr.splice(index, 1);
                }
            })
        }
    });
    last.on('exit', function (code) {
        console.log(type + port + '已关闭,代码:' + code);
        if (type == 'websocket') {
            onLinePort.forEach((item, index) => {
                if (item == port) {
                    onLinePort.splice(index, 1);
                }
            })
            console.log("在线端口", onLinePort)
            if (onLinePort.length == 0) {
                portId = 9000
            }
        }
    });
    return last
}
//移动和缩放摄像头
function deviceMove(deviceData, speed) {
    let device = new onvif.OnvifDevice({
        xaddr: deviceData.xaddr,
        user: deviceData.user,
        pass: deviceData.pass
    });

    device.init().then(() => {
        let ptz = device.services.ptz;
        if (!ptz) {
            throw new Error('当前ONVIF网络摄像机不支持云台服务');
        }
        return device.ptzMove({
            'speed': {
                x: speed.x, // Speed of pan (in the range of -1.0 to 1.0)
                y: speed.y, // Speed of tilt (in the range of -1.0 to 1.0)
                z: speed.z  // Speed of zoom (in the range of -1.0 to 1.0)
            },
            'timeout': 1 // seconds
        });
    }).catch((error) => {
        console.error(error);
    });
}
//停止摄像头移动
function deviceStop(deviceData) {
    let device = new onvif.OnvifDevice({
        xaddr: deviceData.xaddr,
        user: deviceData.user,
        pass: deviceData.pass
    });
    device.init().then(() => {
        let ptz = device.services.ptz;
        if (!ptz) {
            throw new Error('当前ONVIF网络摄像机不支持云台服务');
        }
        device.ptzStop().catch((error) => {
            console.error(error);
        });
    }).catch((error) => {
        console.error(error);
    });
}
//摄像头旋转
function deviceRotate(deviceData, speedX, timeout) {
    let device = new onvif.OnvifDevice({
        xaddr: deviceData.xaddr,
        user: deviceData.user,
        pass: deviceData.pass
    });
    device.init().then(() => {
        let ptz = device.services.ptz;
        if (!ptz) {
            throw new Error('当前ONVIF网络摄像机不支持云台服务');
        }
        return device.ptzMove({
            'speed': { x: speedX, y: 0, z: 0 },
            'timeout': timeout
        });
    }).catch((error) => {
        console.error(error);
    });
}
//删除预置点
function removeDevicePreset(deviceData, data) {
    let device = new onvif.OnvifDevice({
        xaddr: deviceData.xaddr,
        user: deviceData.user,
        pass: deviceData.pass
    });
    device.init().then(() => {
        let ptz = device.services.ptz;
        if (!ptz) {
            throw new Error('当前ONVIF网络摄像机不支持云台服务');
        }
        let profile = device.getCurrentProfile();
        let params = {
            'ProfileToken': profile['token'],
            'PresetToken': data.$.token,
        };
        device.services.ptz.removePreset(params).catch((error) => {
            console.error(error);
        });
    }).catch((error) => {
        console.error(error);
    });
}
//前往预置点
function gotoDevicePreset(deviceData, data) {
    let device = new onvif.OnvifDevice({
        xaddr: deviceData.xaddr,
        user: deviceData.user,
        pass: deviceData.pass
    });
    device.init().then(() => {
        let ptz = device.services.ptz;
        if (!ptz) {
            throw new Error('当前ONVIF网络摄像机不支持云台服务');
        }
        let profile = device.getCurrentProfile();
        let params = {
            'ProfileToken': profile['token'],
            'PresetToken': data.$.token,
            'Speed': { 'x': 1, 'y': 1, 'z': 1 }
        };
        device.services.ptz.gotoPreset(params).catch((error) => {
            console.error(error);
        });
    }).catch((error) => {
        console.error(error);
    });
}

websocket-relay页

// Use the websocket-relay to serve a raw MPEG-TS over WebSockets. You can use
// ffmpeg to feed the relay. ffmpeg -> websocket-relay -> browser
// Example:
// node websocket-relay yoursecret 9081 9082
// ffmpeg -i <some input> -f mpegts http://localhost:8081/yoursecret

var fs = require('fs'),
    http = require('http'),
    WebSocket = require('ws');
//判断输入格式是否正确
if (process.argv.length < 3) {
    console.log(
        '输入格式: \n' +
        'node websocket-relay.js <secret> [<stream-port> <websocket-port>]'
    );
    process.exit();
}
var STREAM_SECRET = process.argv[2],//密码
    STREAM_PORT = process.argv[3] || 8081,//输入地址
    WEBSOCKET_PORT = process.argv[4] || 8082,//输出地址
    RECORD_STREAM = false;//是否录像
// Websocket Server
var socketServer = new WebSocket.Server({ port: WEBSOCKET_PORT, perMessageDeflate: false });
socketServer.connectionCount = 0;
var timer = null
socketServer.on('connection', function (socket, upgradeReq) {
    //一个新的socketServer加入
    socketServer.connectionCount++;
    if (timer != null) {
        clearInterval(timer);
    }
    console.log(
        '接入一个新WebSocket',
        // (upgradeReq || socket.upgradeReq).socket.remoteAddress,
        // (upgradeReq || socket.upgradeReq).headers['user-agent'],
        '现连接数:' + socketServer.connectionCount
    );
    //一个socketServer链接断开
    socket.on('close', function (code, message) {
        socketServer.connectionCount--;
        console.log(
            '一个WebSocket断开 现连接数:' + socketServer.connectionCount
        );
        if (socketServer.connectionCount == 0) {
            timer = setInterval(() =>
                socketClose(), 3000);
        }
    });
});
//是否关闭socket
function socketClose() {
    if (socketServer.connectionCount == 0) {
        console.log('关闭流传输')
    }
}
//socketServer广播
socketServer.broadcast = function (data) {
    socketServer.clients.forEach(function each(client) {
        if (client.readyState === WebSocket.OPEN) {
            client.send(data);
        }
    });
};
//HTTP服务器接受来自ffmpeg的输入MPEG-TS流
var streamServer = http.createServer(function (request, response) {
    var params = request.url.substr(1).split('/');
    if (params[0] !== STREAM_SECRET) {//判断密码是否正确
        console.log(
            '流连接失败: ' + request.socket.remoteAddress + ':' +
            request.socket.remotePort + ' - 密码错误'
        );
        response.end();
    }
    //连接流成功
    response.connection.setTimeout(0);
    console.log(
        '传输的流: ' +
        request.socket.remoteAddress + ':' +
        request.socket.remotePort
    );
    //传输流数据
    request.on('data', function (data) {
        socketServer.broadcast(data);
        if (request.socket.recording) {
            request.socket.recording.write(data);
        }
    });
    //传输流结束
    request.on('end', function () {
        console.log('传输流关闭');
        if (request.socket.recording) {
            request.socket.recording.close();
        }
        process.exit();
    });
    //将流记录到本地文件
    if (RECORD_STREAM) {
        var path = 'recordings/' + Date.now() + '.ts';
        request.socket.recording = fs.createWriteStream(path);
    }
})
//保持套接字打开以进行流式处理
streamServer.headersTimeout = 0;//不等待请求头
streamServer.listen(STREAM_PORT);//创建输出流服务器
// console.log('监听MPEG-TS流 http://127.0.0.1:' + STREAM_PORT + '/<secret>');
// console.log('正在等待上的WebSocket连接 ws://127.0.0.1:' + WEBSOCKET_PORT + '/');
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值