前端rtsp转flv格式视频播放

服务端代码

  • 下载ffmpeg,并且配置环境变量

创建vue转换工程,创建package.json文件

{
  "name": "rtsp-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "babel src/index.js -d dist",
    "start": "npm run build && node dist/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "express-ws": "^4.0.0",
    "ffmpeg-static": "^5.1.0",
    "fluent-ffmpeg": "^2.1.2",
    "websocket-stream": "^5.5.0"
  },
  "devDependencies": {
    "@babel/cli": "^7.6.2",
    "@babel/core": "^7.6.2",
    "@babel/preset-env": "^7.6.2"
  }
}

 创建index.js处理逻辑

const express = require('express');
const expressWebSocket = require("express-ws");
import webSocketStream from "websocket-stream/stream";

// 在需要使用FFmpeg的地方使用ffmpegPath变量
const ffmpeg = require('fluent-ffmpeg');
// 注意一下:配置ffmpeg程序的路径(包含exe程序名)
ffmpeg.setFfmpegPath('D:/quniao/doc/ffmpeg/9-git-159b028df5-full_build/bin/ffmpeg');


function localServer() {
  let app = express();
  app.use(express.static(__dirname));
  // extend express app with app.ws()
  expressWebSocket(app, null, {
    perMessageDeflate: true
  });
  app.ws("/rtsp/:id/", rtspRequestHandle)
  app.listen(8888);
  console.log("express listened")
}

function rtspRequestHandle(ws, req) {
  console.log("rtsp request handle");
  // convert ws instance to stream
  const stream = webSocketStream(ws, {
    binary: true,
    browserBufferTimeout: 1000000
  }, {
    browserBufferTimeout: 1000000
  });
  let url = req.query.url;
  console.log("rtsp url:", url);
  console.log("rtsp params:", req.params);
  
  // ffmpet转码
  let ffmpegCommand = ffmpeg(url)
    .addInputOption("-rtsp_transport", "tcp", "-buffer_size", "102400")  // 这里可以添加一些 RTSP 优化的参数
.outputOptions([
    '-c:v libx264', // 设置视频编码器
    '-c:a aac', // 设置音频编码器
    '-b:v 2000k', // 设置视频比特率
    '-s 1280x720', // 设置视频分辨率
    '-b:a 128k', // 设置音频比特率
    '-ar 44100' // 设置音频采样率
    ])
    .on("start", function () {
      console.log(url, "Stream started.");
      ws.send('')
    })
    .on("codecData", function () {
      console.log(url, "Stream codecData.")
      // 摄像机在线处理
    })
    .on("error", function (err) {
      console.log(url, "An error occured: ", err.message);
      stream.end();
    })
    .on("end", function () {
      console.log(url, "Stream end!");
      stream.end();
      // 摄像机断线的处理
    })
    .outputFormat("flv").videoCodec("copy").noAudio(); // 输出格式flv 无音频

  stream.on("close", function () {
    ffmpegCommand.kill('SIGKILL');
  });
  try {
    ffmpegCommand.pipe(stream);
  } catch (error) {
    console.log(error);
  }
 
}

localServer()

服务过程创建完成后的结构:

 客户端代码

执行命令安装 "flv.js": "^1.6.2",

<template>
  <div style="background-color: #ececec; width: 100%; height: 100%; padding: 20px">
    <a-row :gutter="24">
      <a-col :span="14">
        <a-card :bordered="false" class="table-container">
          <div>
            <video ref="player" class="video" muted></video>
          </div>
        </a-card>
      </a-col>
      <a-col :span="5">
        <a-card :bordered="false" class="table-container">
          <a-row>
            <ptz-control @playflv="playflv" @turnPosition="turnPosition"> </ptz-control>
          </a-row>
        </a-card>
      </a-col>
      <a-col :span="5">
        <a-card :bordered="false" class="table-container">
          <a-button type="primary" @click="addRow" icon="plus" :disabled="disabledAddBtn">添加预置点</a-button>
          <table class="my-table">
            <thead>
              <tr>
                <th>序号</th>
                <th>预置点</th>
                <th>操作</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(item, index) in tableData" :key="index" @dblclick="handleDoubleClick(item)">
                <td>{{ index + 1 }}</td>
                <td>{{ '预置点' + (index + 1) }}</td>
                <td><a @click="delItem(item.id)">删除</a></td>
              </tr>
            </tbody>
          </table>
        </a-card>
      </a-col>
    </a-row>
  </div>
</template>
<script>
import flvjs from "flv.js";
import PtzControl from './PtzControl.vue'
import { postAction, getAction } from '@/api/manage'
import { deleteAction } from '../../../api/manage'
export default {
  components: {
    PtzControl,
  },
  data() {
    return {
      rtspUrl:'rtsp://192.168.18.4:8554/video',
    }
  },
  created() {
    
  },
  mounted() {
    if (flvjs.isSupported()) {
      let video = this.$refs.player
      if (video) {
        this.player = flvjs.createPlayer({
          type: 'flv',
          isLive: true,
          hasAudio: false,
          enableStashBuffer: false,
          #刚才搭建的服务端地址加端口,只替换url即可;  
          url: `ws://192.168.18.5:8888/rtsp/0/?url=`+this.rtspUrl
        })
        this.player.attachMediaElement(video)
        try {
          this.player.load()
          this.player.play()
          //console.log("play");
        } catch (error) {
          //console.log(error);
          this.$Notice.error({
            title: '播放错误',
            desc: error,
          })
      }
    }
    }
  },
}
</script>
<style lang="less" scoped>
.video {
  background-color: lightgray;
  height: 480px;
  width: 100%;
}
.table-container {
  height: 550px; /* 设置表格容器的最大高度 */
  overflow-y: auto; /* 启用垂直滚动条 */
}
.my-table {
  margin-top: 30px;
  width: 100%;
  border-collapse: collapse;
}

.my-table th,
.my-table td {
  padding: 10px;
  border: 1px solid #ccc;
}

.my-table th {
  background-color: #f0f0f0;
}

.my-table tbody tr:nth-child(even) {
  background-color: #f9f9f9;
}

.my-table tbody tr:hover {
  background-color: #eaeaea;
}
</style>

PTZ云台控件

<template>
  <div>
    <div class="pie">
      <!-- 上 -->
      <div class="slice-one slice" @click="turnPosition('FF01000800FF08','上')">
        <div class="dv"><div class="sj"></div></div>
      </div>
      <!-- 右上 -->
      <div class="slice-two slice" @click="turnPosition('FF01000A0F0F29','右上')">
        <div class="dv"><div class="sj"></div></div>
      </div>
      <!-- 右 -->
      <div class="slice-three slice" @click="turnPosition('FF010002FF0002','右')">
        <div class="dv"><div class="sj"></div></div>
      </div>
      <!-- 右下 -->
      <div class="slice-four slice" @click="turnPosition('FF0100120F0F31','右下')">
        <div class="dv"><div class="sj"></div></div>
      </div>
      <!-- 下 -->
      <div class="slice-five slice" @click="turnPosition('FF01001000FF10','下')">
        <div class="dv"><div class="sj"></div></div>
      </div>
      <!-- 左下 -->
      <div class="slice-six slice" @click="turnPosition('FF0100140F0F33','左下')">
        <div class="dv"><div class="sj"></div></div>
      </div>
      <!-- 左 -->
      <div class="slice-seven slice"  @click="turnPosition('FF010004FF0004','左')">
        <div class="dv"><div class="sj"></div></div>
      </div>
      <!-- 左上 -->
      <div class="slice-eight slice"  @click="turnPosition('FF01000C0F0F2B','左上')">
        <div class="dv"><div class="sj"></div></div>
      </div>
      <div class="center" @click="reload">
        <img src="../../../assets/reload.png" style="width: 50px; margin-left: 25%; margin-top: 25%;" />
      </div>
    </div>
    <div class="progress-bar row">
        <div style="width:150px">手动速度:</div>
       <slid :min="0" :max="100" @input="changeSpeed" v-model="obj.speed" :isDrag="true" bgColor="#268cf2"></slid>
    </div>
    <div class="line row">
        <div style="width:150px">变倍:
          <a-button type="primary" @click="turnPosition('FF010020000021','')" shape="circle" style="font-size: 20px; margin-left: 20px">+</a-button>
        </div>
        <a-button type="primary"   @click="turnPosition('FF010040000041','')" shape="circle" style="font-size: 20px; margin-left: 20px">-</a-button>
    </div>
    <div class="line row">
        <div style="width:150px">聚焦:
          <a-button type="primary" @click="turnPosition('FF010100000002','')" shape="circle" style="font-size: 20px; margin-left: 20px">+</a-button>
        </div>
        <a-button type="primary" @click="turnPosition('FF010080000081','')" shape="circle" style="font-size: 20px; margin-left: 20px">-</a-button>
    </div>
    <div class="line row">
      <div style="width:150px">光圈:
          <a-button type="primary" @click="turnPosition('FF010200000003','')" shape="circle" style="font-size: 20px; margin-left: 20px">+</a-button>
        </div>
        <a-button type="primary"   @click="turnPosition('FF010400000005','')" shape="circle" style="font-size: 20px; margin-left: 20px">-</a-button>
        <!-- <div style="width:150px">激光:
          <a-button type="primary" @click="turnPosition('FF01120900001C')" shape="circle" style="font-size: 20px; margin-left: 20px">开</a-button>
        </div>
        <a-button type="primary"   @click="turnPosition('FF01120A00001D')" shape="circle" style="font-size: 20px; margin-left: 20px">关</a-button> -->
    </div>
  </div>
</template>
<script>
import Slid from './Slid.vue'
export default {
  components:{
    Slid
  },
  data() {
    return {
      dragging: false,
      progress: 0,
      obj:{},
      SpeedType:'',
    }
  },
  methods: {
    reload() {
      this.$emit('playflv')
    },
    turnPosition(position,SpeedType) {
      this.obj.position = position;
      if(SpeedType) {
        this.SpeedType = SpeedType;
        if(this.obj.speed) {
          var speedParam = {
            Fun: 410,
            Cmd: 3,
            ControlType: 'SerialControl',
            ID:1,
          };
          this.getHVSpeed(speedParam,this.obj.speed);
          this.$emit('turnPosition',speedParam)
          return;
        }
      }
      this.$emit('turnPosition',this.obj)
    }, 
    changeSpeed(speed) {
      this.obj.speed = speed;
    },
    getHVSpeed(speedParam,speed) {
      var SpeedType =  this.SpeedType
      speedParam.SpeedType = SpeedType
      // HSpeed为横向速度,VSpeed为纵向速度
      if(SpeedType == '左上' || SpeedType == '左下' || SpeedType == '右上' || SpeedType == '右下') {
        speedParam.HSpeed = speed
        speedParam.VSpeed = speed
      }
      if(SpeedType == '上') {
        speedParam.SpeedType = '右上'
        speedParam.HSpeed = 0
        speedParam.VSpeed = speed
      } else if(SpeedType == '下') {
        speedParam.SpeedType = '右下'
        speedParam.HSpeed = 0
        speedParam.VSpeed = speed
      } else if(SpeedType == '左') {
        speedParam.SpeedType = '左下'
        speedParam.HSpeed = speed
        speedParam.VSpeed = 0
      } else if(SpeedType == '右') {
        speedParam.SpeedType = '右下'
        speedParam.HSpeed = speed
        speedParam.VSpeed = 0
      }
    }  
  },
}
</script>
<style lang="less" scoped>
.pie {
  transform: scale3d(1.22, 1.22, 1.22);
  position: relative;
  margin: 20px auto;
  padding: 0;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  list-style: none;
  overflow: hidden;
  // background: url('../../../assets/reload.png') no-repeat center center / 100% 100%;
  .center {
    position: absolute;
    width: 100px;
    height: 100px;
    top: 50px;
    left: 50px;
    border-radius: 50%;
    background-color: white;
    &::after {
      content: '';
      position: absolute;
      width: 50px;
      height: 50px;
      top: 25px;
      left: 25px;
      border-radius: 50%;
      z-index: 9;
      // background-color: #f1f5fd;
    }
    &:active {
      background-color: #bbc9d6;
    }
  }
  .slice {
    background-color: #f1f5fd;
    overflow: hidden;
    position: absolute;
    top: 0;
    right: 0;
    width: 50%;
    height: 50%;
    transform-origin: 0% 100%;
    border-left: 3px solid white;
    &:active {
      background-color: #268cf2;
      .dv .sj {
        border-bottom: 10px solid white;
      }
    }
    // border-bottom: 5px solid white;
    .dv {
      transform: skewY(45deg) rotate(22.5deg);
      margin-top: 105px;
      margin-left: 15px;
      .sj {
        position: fixed;
        bottom: 0;
        width: 0;
        height: 0;
        border-top: 10px solid transparent;
        border-right: 10px solid transparent;
        border-bottom: 10px solid #268cf2;
        border-left: 10px solid transparent;
      }
    }
    &.slice-one {
      transform: rotate(-22.5deg) skewY(-45deg);
    }
    &.slice-two {
      transform: rotate(22.5deg) skewY(-45deg);
    }
    &.slice-three {
      transform: rotate(67.5deg) skewY(-45deg);
    }
    &.slice-four {
      transform: rotate(112.5deg) skewY(-45deg);
    }
    &.slice-five {
      transform: rotate(157.5deg) skewY(-45deg);
    }
    &.slice-six {
      transform: rotate(202.5deg) skewY(-45deg);
    }
    &.slice-seven {
      transform: rotate(247.5deg) skewY(-45deg);
    }
    &.slice-eight {
      transform: rotate(292.5deg) skewY(-45deg);
    }
  }
}
.progress-bar {
  margin-top: 50px;
}
.line {
  margin-top: 20px;
}
.row {
  display: flex;
  align-items: center;
}
</style>

Slid进度条控件

<template>
  <div class="slider" ref="slider" @click.stop="handelClickSlider">
    <div class="process" :style="{ width,background:bgColor }"></div>
    <div class="thunk" ref="trunk" :style="{ left }">
      <div class="block" ref="dot"></div>
      {{per/10}}
    </div>
  </div>
</template>
<script>
/*
 * min 进度条最小值
 * max 进度条最大值
 * v-model 对当前值进行双向绑定实时显示拖拽进度
 * */
export default {
  props: {
    // 最小值
    min: {
      type: Number,
      default: 0,
    },
    // 最大值
    max: {
      type: Number,
      default: 100,
    },
    // 当前值
    value: {
      type: Number,
      default: 0,
    },
    // 进度条颜色
    bgColor: {
      type: String,
      default: "#4ab157",
    },
    // 是否可拖拽
    isDrag: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      slider: null, //滚动条DOM元素
      thunk: null, //拖拽DOM元素
      per: this.value, //当前值
    };
  },
  mounted() {
    this.slider = this.$refs.slider;
    this.thunk = this.$refs.trunk;
    var _this = this;
    if (!this.isDrag) return;
    this.thunk.onmousedown = function (e) {
      var width = parseInt(_this.width);
      var disX = e.clientX;
      document.onmousemove = function (e) {
        // value, left, width
        // 当value变化的时候,会通过计算属性修改left,width
        // 拖拽的时候获取的新width
        var newWidth = e.clientX - disX + width;
        // 计算百分比
        var scale = newWidth / _this.slider.offsetWidth;
        _this.per = Math.ceil((_this.max - _this.min) * scale + _this.min); //取整
        // 限制值大小
        _this.per = Math.max(_this.per, _this.min);
        _this.per = Math.min(_this.per, _this.max);
        _this.$emit("input", Math.ceil(_this.per/10));
      };
      document.onmouseup = function () {
        //当拖拽停止发送事件
        _this.$emit("stop", _this.per/10);
        //清除拖拽事件
        document.onmousemove = document.onmouseup = null;
      };
    };
  },
  methods: {
    handelClickSlider(event) {
      //禁止点击
      if (!this.isDrag) return;
      const dot = this.$refs.dot;
      if (event.target == dot) return;
      //获取元素的宽度l
      let width = this.slider.offsetWidth;
      //获取元素的左边距
      let ev = event || window.event;
      //获取当前点击位置的百分比
      let scale = ((ev.offsetX / width) * 100).toFixed(2);
      this.per = Math.ceil(scale);
      this.$emit("input", Math.ceil(this.per/10));
    },
  },
  computed: {
    // 设置一个百分比,提供计算slider进度宽度和trunk的left值
    // 对应公式为  当前值-最小值/最大值-最小值 = slider进度width / slider总width
    // trunk left =  slider进度width + trunk宽度/2
    scale() {
      return (this.per - this.min) / (this.max - this.min);
    },
    width() {
      return this.slider ? this.slider.offsetWidth * this.scale + "px" : "0px";
    },
    left() {
      return this.slider ? this.slider.offsetWidth * this.scale - this.thunk.offsetWidth / 2 +"px" : "0px";
    },
  },
};
</script>
<style scoped>
.box {
  margin: 100px auto 0;
  width: 80%;
}
.clear:after {
  content: "";
  display: block;
  clear: both;
}
.slider {
  position: relative;
  margin: 20px 0;
  width: 100%;
  height: 10px;
  top: 50%;
  background: #747475;
  border-radius: 5px;
  cursor: pointer;
  z-index: 99999;
}
.slider .process {
  position: absolute;
  left: 0;
  top: 0;
  width: 112px;
  height: 10px;
  border-radius: 5px;
  background: #4ab157;
  z-index: 111;
}
.slider .thunk {
  position: absolute;
  left: 100px;
  top: -4px;
  width: 10px;
  height: 6px;
  z-index: 122;
}
.slider .block {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #268cf2;
  transition: 0.2s all;
}
.slider .block:hover {
  transform: scale(1.1);
  opacity: 0.6;
}
</style>

最后效果

附带云台按钮和预置点页面,仅供参考

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值