zhaoen пре 2 недеља
родитељ
комит
3c97e3c889

+ 1 - 1
babel.config.js

@@ -10,4 +10,4 @@ module.exports = {
       'plugins': ['dynamic-import-node']
     }
   }
-}
+}

+ 9 - 0
src/api/service/streamProxy.js

@@ -63,3 +63,12 @@ export function selectStreamProxyItem(data) {
     params: data
   })
 }
+
+// 获取媒体服务器ID
+export function getStreamServerTypeList(data) {
+  return request({
+    url: `/sis/sisStreamProxy/selectMediaServerList`,
+    method: 'post',
+    data
+  })
+}

+ 355 - 176
src/components/WebrtcPlayer/index.vue

@@ -1,26 +1,44 @@
 <template>
   <div id="app" ref="app" style="width: 100%;height: 100%">
-    <video v-show="videoShow" :id="videoId" ref="jswebrtc" :controls="controls" @click="clickVideo"
-      @dblclick="dbclickVideo" style="width: 100%;height: 100%;object-fit: fill" muted></video>
+    <video
+      v-show="videoShow"
+      :id="videoId"
+      ref="jswebrtc"
+      :controls="controls"
+      @click="clickVideo"
+      @dblclick="dbclickVideo"
+      style="width: 100%;height: 100%;object-fit: fill"
+      muted
+      playsinline
+      autoplay
+    ></video>
     <!-- 拖拽选择框 -->
     <div ref="rectArea" class="rect"></div>
     <!--放大后视频区域-->
     <div ref="videoZoom" class="video-zoom">
-      <video v-show="videoZoomShow" :id="'zoom' + videoId" ref="jswebrtcZoom" :controls="controls"
-        style="width: 100%;height: 100%;object-fit: fill" muted></video>
+      <video
+        v-show="videoZoomShow"
+        :id="'zoom' + videoId"
+        ref="jswebrtcZoom"
+        :controls="controls"
+        style="width: 100%;height: 100%;object-fit: fill"
+        muted
+        playsinline
+        autoplay
+      ></video>
     </div>
 
     <canvas id="myCanvas" ref="myCanvas"></canvas>
     <canvas id="line" ref="line"></canvas>
 
-    <div v-show="videoShow" class='buttons-box' id='buttonsBox'>
-      <div class='buttons-box-left'>
+    <div v-show="videoShow" class="buttons-box" id="buttonsBox">
+      <div class="buttons-box-left">
       </div>
-      <div class='buttons-box-right'>
-        <i class='el-icon-crop jessibuca-btn' @click='screenshot' style='font-size: 1rem !important'></i>
-        <i v-if="!recording" class='el-icon-video-camera jessibuca-btn' @click='recoder'></i>
-        <i v-if="recording" class='el-icon-video-camera-solid jessibuca-btn' @click='endRecoder'></i>
-        <i class='el-icon-switch-button jessibuca-btn' @click.stop='close'></i>
+      <div class="buttons-box-right">
+        <i class="el-icon-crop jessibuca-btn" @click="screenshot" style="font-size: 1rem !important"></i>
+        <i v-if="!recording" class="el-icon-video-camera jessibuca-btn" @click="recoder"></i>
+        <i v-if="recording" class="el-icon-video-camera-solid jessibuca-btn" @click="endRecoder"></i>
+        <i class="el-icon-switch-button jessibuca-btn" @click.stop="close"></i>
       </div>
     </div>
     <!-- <div class='buttons-box2' style="" id='buttonsBox2'>
@@ -37,10 +55,12 @@
 </template>
 
 <script>
-import { formatDate } from "../../utils";
-import { mapGetters } from "vuex";
-import { downloadSnapshot } from "../../api/snap/snap"
+import { formatDate } from '../../utils'
+import { mapGetters } from 'vuex'
+import { downloadSnapshot } from '../../api/snap/snap'
 import { configPage } from '/public/config'
+// import { WebRTCPlayer } from '@eyevinn/webrtc-player'
+
 // import FFmpeg from "@ffmpeg/ffmpeg";
 //
 // const {createFFmpeg, fetchFile} = FFmpeg;
@@ -50,7 +70,7 @@ import { configPage } from '/public/config'
 // });
 
 export default {
-  name: "webrtcPlayer",
+  name: 'webrtcPlayer',
   props: {
     videoId: {
       type: Number,
@@ -111,7 +131,7 @@ export default {
       reconnectTimer: null, // 用于存放重连的定时器
       reconnectCount: 0,  // 当前重连次数
       maxReconnectAttempts: 5, // 设置最大重连次数,防止无限重连
-      shouldSaveOnStop: true,
+      shouldSaveOnStop: true
     }
   },
   computed: {
@@ -119,27 +139,25 @@ export default {
   },
   mounted() {
     this.$watch('videoSrc', (newData, oldData) => {
-      if (newData !== '' && newData !== oldData) {
-        this.initVideo(this.videoSrc, this.videoId);
-        this.rectZoomInit()
-
-
-
-      } else {
-        this.stop()
-      }
-    },
+        console.log('l==-[[];', newData, oldData)
+        if (newData !== '' && newData !== oldData) {
+          this.initVideo(this.videoSrc, this.videoId)
+          this.rectZoomInit()
+        } else {
+          this.stop()
+        }
+      },
       { immediate: true })
 
     // 动态设置myCanvas宽高
-    let videoPlayer = this.$refs.app, myCanvas = this.$refs.myCanvas, line = this.$refs.line;
-    let width = videoPlayer.offsetWidth, height = videoPlayer.offsetHeight;
+    let videoPlayer = this.$refs.app, myCanvas = this.$refs.myCanvas, line = this.$refs.line
+    let width = videoPlayer.offsetWidth, height = videoPlayer.offsetHeight
     // let lineContext = line.getContext('2d');
     line.width = myCanvas.width = width
     line.height = myCanvas.height = height
   },
   created() {
-    console.log(this.fullscreen,"fullscreen")
+    console.log(this.fullscreen, 'fullscreen')
     this.getTimes()
   },
   watch: {
@@ -150,12 +168,12 @@ export default {
     },
     active(newData, oldData) {
       setTimeout(() => {
-        let videoPlayer = this.$refs.app, myCanvas = this.$refs.myCanvas, line = this.$refs.line;
-        let width = videoPlayer.offsetWidth, height = videoPlayer.offsetHeight;
+        let videoPlayer = this.$refs.app, myCanvas = this.$refs.myCanvas, line = this.$refs.line
+        let width = videoPlayer.offsetWidth, height = videoPlayer.offsetHeight
         line.width = myCanvas.width = width
         line.height = myCanvas.height = height
       }, 300)
-    },
+    }
 
   },
 
@@ -178,106 +196,265 @@ export default {
         this.time = new Date().getTime()
       }, 1000)
     },
-    initVideo(url, videoId) {
-      //   关闭流
-      if (this.player) {
-        this.player.pc.close()
-        this.player = null
-      }
-      let videoDom = document.getElementById(videoId)
-
-      this.player = new ZLMRTCClient.Endpoint({
-        element: videoDom,//video标签
-        debug: false,//是否打印日志
-        zlmsdpUrl: url,//流地址
-        simulcast: true,
-        useCamera: false,
-        audioEnable: true,
-        videoEnable: true,
-        recvOnly: true,
-        resolution: {
-          w: 600,
-          h: 340
-        },
-        usedatachannel: true,
+    async initVideo(url, videoId) {
+      const config = {
+        sdpSemantics: "unified-plan",
+        iceServers: [{ urls: "stun:stun.l.google.com:19302" }, { urls: "stun:global.stun.twilio.com:3478" }],
+      };
+      let stream
+
+      this.player = new RTCPeerConnection(config);
+      this.player.addEventListener("iceconnectionstatechange", () => {
+        const state = this.player.iceConnectionState;
+        console.log(state)
+        if (state === "connected" || state === "completed") {
+          // updateStatus("已连接", true);
+          // showLoading(false);
+          // hideError();
+        } else if (state === "failed" || state === "disconnected" || state === "closed") {
+          // updateStatus("连接失败", false);
+          // showLoading(false);
+          if (state === "failed") {
+            // showError("ICE连接失败,请检查网络连接");
+          }
+        }
+      });
+      this.player.addEventListener("track", (event) => {
+        // addLog("收到媒体轨道", "success");
+        const videoElement = document.getElementById(videoId);
+
+        if (event.streams && event.streams[0]) {
+          stream = event.streams[0];
+          videoElement.srcObject = stream;
+
+          // 设置自动播放属性
+          videoElement.muted = false;
+
+          // addLog("视频流已附加到播放器", "success");
+
+          // 监听视频元数据加载
+          videoElement.addEventListener("loadedmetadata", () => {
+            // updateQualityInfo();
+            // addLog(`视频分辨率: ${videoElement.videoWidth}×${videoElement.videoHeight}`, "info");
+          });
+
+          // 定期更新质量信息
+          // setInterval(updateQualityInfo, 2000);
+        }
+      });
+      // 监听ICE候选
+      this.player.addEventListener("icecandidate", (event) => {
+        if (event.candidate) {
+          // addLog("生成 ICE candidate", "info");
+        }
       });
 
-      // this.player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, function (e) {
-      //   console.log('ICE协商出错')
+      // 监听ICE收集状态
+      this.player.addEventListener("icegatheringstatechange", () => {
+        // addLog(`ICE收集状态: ${peerConnection.iceGatheringState}`, "info");
+      });
+
+      try {
+        // 创建PeerConnection
+        // this.player = createPeerConnection();
+
+        // 添加Transceiver用于接收视频
+        this.player.addTransceiver("video", {
+          direction: "recvonly",
+        });
+
+        this.player.addTransceiver("audio", {
+          direction: "recvonly",
+        });
+
+        // addLog("添加 transceiver 完成", "info");
+
+        // 创建Offer
+        const offer = await this.player.createOffer();
+        await this.player.setLocalDescription(offer);
+
+        // addLog("创建SDP offer成功", "success");
+
+        // 等待ICE候选收集完成
+        await new Promise((resolve) => {
+          if (this.player.iceGatheringState === "complete") {
+            resolve();
+          } else {
+            const checkState = () => {
+              if (this.player.iceGatheringState === "complete") {
+                this.player.removeEventListener("icegatheringstatechange", checkState);
+                resolve();
+              }
+            };
+            this.player.addEventListener("icegatheringstatechange", checkState);
+          }
+        });
+
+        // 发送SDP到WHEP服务器
+        const sdpOffer = this.player.localDescription.sdp;
+        console.log(url, videoId)
+
+        const response = await fetch('http://10.168.1.232:8889/38/SIS-1766510219282/whep', {
+          method: "POST",
+          headers: {
+            "Content-Type": "application/sdp",
+            Accept: "application/sdp",
+          },
+          body: sdpOffer,
+        });
+
+        if (!response.ok) {
+          throw new Error(`服务器响应错误: ${response.status} ${response.statusText}`);
+        }
+        const sdpAnswer = await response.text();
+        await this.player.setRemoteDescription({
+          type: "answer",
+          sdp: sdpAnswer,
+        });
+      } catch (error) {
+
+        if (this.player) {
+          this.player.close();
+          this.player = null;
+        }
+      }
+
+      // const videoDom = document.getElementById(videoId)
+      // this.player = new ZLMRTCClient.Endpoint({
+      //   element: videoDom,//video标签
+      //   debug: false,//是否打印日志
+      //   zlmsdpUrl: 'http://10.168.1.232:8889/38/SIS-1766510219282/whep', // url,//流地址
+      //   simulcast: true,
+      //   useCamera: false,
+      //   audioEnable: true,
+      //   videoEnable: true,
+      //   recvOnly: true,
+      //   resolution: {
+      //     w: 600,
+      //     h: 340
+      //   },
+      //   usedatachannel: true,
+      // });
+      //
+      // // this.player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, function (e) {
+      // //   console.log('ICE协商出错')
+      // // })
+      // this.player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, function (e) { //获取到了流,可以播放
+      //   videoDom.addEventListener('canplay', function (e) {
+      //     videoDom.play();
+      //     // console.log("获取到了流,可以播放",e)
+      //     // console.log(new Date().getTime())
+      //   })
       // })
-      this.player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, function (e) { //获取到了流,可以播放
-        videoDom.addEventListener('canplay', function (e) {
-          videoDom.play();
-          // console.log("获取到了流,可以播放",e)
-          // console.log(new Date().getTime())
-        })
-      })
-      // this.player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, function (e) {
-      //   console.log('offer answer交换失败', e)
+      // // this.player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, function (e) {
+      // //   console.log('offer answer交换失败', e)
+      // // })
+      // // this.player.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, function (e) {
+      // //   console.log('获取到了本地流')
+      // // })
+      // // this.player.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, function (e) {
+      // //   console.log('获取本地流失败')
+      // // })
+      // this.player.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (state) =>{
+      //   console.log('当前状态==>', state)
+      //   // 当连接失败、断开、关闭或超时时,启动重连机制
+      //   if (state === 'failed' || state === 'disconnected' || state === 'closed' || state === 'timeout') {
+      //     this.$message({ message: '视频流连接已断开,正在尝试重连...', type: "warning" });
+      //     this.handleReconnect();
+      //   } else if (state === 'connected') {
+      //     // 连接成功后,清除重连定时器并重置计数器
+      //     console.log("视频流连接成功!");
+      //     this.reconnectCount = 0;
+      //     if (this.reconnectTimer) {
+      //       clearTimeout(this.reconnectTimer);
+      //       this.reconnectTimer = null;
+      //     }
+      //   }
+      // });
+      // this.player.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_OPEN, function (e) {
+      //   // console.log('rtc datachannel 打开:', e)
       // })
-      // this.player.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, function (e) {
-      //   console.log('获取到了本地流')
+      // this.player.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_MSG, function (e) {
+      //   // console.log('rtc datachannel 消息:', e)
       // })
-      // this.player.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, function (e) {
-      //   console.log('获取本地流失败')
+      // this.player.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_ERR, function (e) {
+      //   // console.log('rtc datachannel 错误:', e)
       // })
-      this.player.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (state) =>{
-        console.log('当前状态==>', state)
-        // 当连接失败、断开、关闭或超时时,启动重连机制
-        if (state === 'failed' || state === 'disconnected' || state === 'closed' || state === 'timeout') {
-          this.$message({ message: '视频流连接已断开,正在尝试重连...', type: "warning" });
-          this.handleReconnect();
-        } else if (state === 'connected') {
-          // 连接成功后,清除重连定时器并重置计数器
-          console.log("视频流连接成功!");
-          this.reconnectCount = 0;
-          if (this.reconnectTimer) {
-            clearTimeout(this.reconnectTimer);
-            this.reconnectTimer = null;
-          }
-        }
-      });
-      this.player.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_OPEN, function (e) {
-        // console.log('rtc datachannel 打开:', e)
-      })
-      this.player.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_MSG, function (e) {
-        // console.log('rtc datachannel 消息:', e)
+      // this.player.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_CLOSE, function (e) {
+      //   // console.log('rtc datachannel 关闭:', e)
+      // })
+    },
+    // 绑定播放器事件
+    bindPlayerEvents() {
+      // 媒体超时
+      this.player.on('no-media', () => {
+        console.log('媒体流中断')
+        this.isPlaying = false
       })
-      this.player.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_ERR, function (e) {
-        // console.log('rtc datachannel 错误:', e)
+      // 媒体恢复
+      this.player.on('media-recovered', () => {
+        console.log('媒体流恢复')
+        this.isPlaying = true
       })
-      this.player.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_CLOSE, function (e) {
-        // console.log('rtc datachannel 关闭:', e)
+      // 播放错误
+      this.player.on('error', (err) => {
+        console.log(`播放错误: ${err.message}`)
+        this.isPlaying = false
       })
     },
+    // 切换播放/暂停
+    togglePlay() {
+      if (this.isPlaying) {
+        this.player.pause()
+        console.log('已暂停')
+      } else {
+        this.player.play()
+        console.log('播放中')
+      }
+      this.isPlaying = !this.isPlaying
+    },
+    // 销毁播放器
+    destroyPlayer() {
+      if (this.player) {
+        this.player.destroy()
+        this.player = null
+        this.isInitialized = false
+        this.isPlaying = false
+        console.log('播放器已销毁')
+      }
+    },
+
     handleReconnect() {
       if (this.reconnectCount < this.maxReconnectAttempts) {
-        this.reconnectCount++;
-        console.log(`正在进行第 ${this.reconnectCount} 次重连...`);
+        this.reconnectCount++
+        console.log(`正在进行第 ${this.reconnectCount} 次重连...`)
 
         // 清除之前的定时器
         if (this.reconnectTimer) {
-          clearTimeout(this.reconnectTimer);
+          clearTimeout(this.reconnectTimer)
         }
 
         // 设置一个延时后重连,避免过于频繁
         this.reconnectTimer = setTimeout(() => {
-          this.initVideo(this.videoSrc, this.videoId);
-        }, 3000); // 3秒后尝试重连
+          this.initVideo(this.videoSrc, this.videoId)
+        }, 3000) // 3秒后尝试重连
       } else {
-        this.$message({ message: `已达到最大重连次数 (${this.maxReconnectAttempts}次),停止重连。请手动刷新或检查网络。`, type: "error" });
+        this.$message({
+          message: `已达到最大重连次数 (${this.maxReconnectAttempts}次),停止重连。请手动刷新或检查网络。`,
+          type: 'error'
+        })
         // 停止重连后,清除定时器
         if (this.reconnectTimer) {
-          clearTimeout(this.reconnectTimer);
-          this.reconnectTimer = null;
+          clearTimeout(this.reconnectTimer)
+          this.reconnectTimer = null
         }
       }
     },
     stop() {
       // 手动停止播放时,也应清除重连定时器
       if (this.reconnectTimer) {
-        clearTimeout(this.reconnectTimer);
-        this.reconnectTimer = null;
+        clearTimeout(this.reconnectTimer)
+        this.reconnectTimer = null
       }
       let videoDom = document.getElementById(this.videoId)
       videoDom.pause()
@@ -288,27 +465,27 @@ export default {
     },
 
     getCurrentDateTime() {
-      const now = new Date();
+      const now = new Date()
 
-      const year = now.getFullYear();
-      const month = ('0' + (now.getMonth() + 1)).slice(-2); // Month is zero-based
-      const day = ('0' + now.getDate()).slice(-2);
-      const hours = ('0' + now.getHours()).slice(-2);
-      const minutes = ('0' + now.getMinutes()).slice(-2);
-      const seconds = ('0' + now.getSeconds()).slice(-2);
+      const year = now.getFullYear()
+      const month = ('0' + (now.getMonth() + 1)).slice(-2) // Month is zero-based
+      const day = ('0' + now.getDate()).slice(-2)
+      const hours = ('0' + now.getHours()).slice(-2)
+      const minutes = ('0' + now.getMinutes()).slice(-2)
+      const seconds = ('0' + now.getSeconds()).slice(-2)
 
-      return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
+      return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`
     },
 
     // 截图
     screenshot() {
-      const currentDateTime = this.getCurrentDateTime();
-      const item = this.boxList.find(box => box.boxId === this.videoId);
-      console.log(item);
+      const currentDateTime = this.getCurrentDateTime()
+      const item = this.boxList.find(box => box.boxId === this.videoId)
+      console.log(item)
 
-      const fileName = `${currentDateTime}-${this.cameraName}-${this.$store.getters.name}.jpg`;
-      const saveDir = "images";
-      const url = configPage.httpServe+`/downloadSnapshot?streamUrl=${item.rtsp}&fileName=${fileName}`;
+      const fileName = `${currentDateTime}-${this.cameraName}-${this.$store.getters.name}.jpg`
+      const saveDir = 'images'
+      const url = configPage.httpServe + `/downloadSnapshot?streamUrl=${item.rtsp}&fileName=${fileName}`
 
       fetch(url, {
         method: 'GET',
@@ -320,34 +497,31 @@ export default {
       })
         .then(response => {
           if (!response.ok) {
-            throw new Error(`HTTP error! status: ${response.status}`);
+            throw new Error(`HTTP error! status: ${response.status}`)
           }
-          return response.blob(); // 将响应体转换为 Blob 对象
+          return response.blob() // 将响应体转换为 Blob 对象
         })
         .then(blob => {
-          this.$message({ message: '截图成功', type: "success" })
+          this.$message({ message: '截图成功', type: 'success' })
           // 创建一个隐藏的可下载链接
-          const link = document.createElement('a');
-          link.href = URL.createObjectURL(blob);
-          link.setAttribute('download', fileName); // 设置文件名
-          document.body.appendChild(link);
-          link.click();
-          document.body.removeChild(link);
-          URL.revokeObjectURL(link.href); // 清理 URL 对象
+          const link = document.createElement('a')
+          link.href = URL.createObjectURL(blob)
+          link.setAttribute('download', fileName) // 设置文件名
+          document.body.appendChild(link)
+          link.click()
+          document.body.removeChild(link)
+          URL.revokeObjectURL(link.href) // 清理 URL 对象
         })
-        .catch(error => console.error('Error downloading file:', error));
+        .catch(error => console.error('Error downloading file:', error))
     },
 
-
-
-
     //合并流
     mergeStream() {
-      let videoPlayer = this.$refs.jswebrtc, myCanvas = this.$refs.myCanvas, line = this.$refs.line;
-      let width = videoPlayer.offsetWidth, height = videoPlayer.offsetHeight;
-      let context = myCanvas.getContext("2d");
+      let videoPlayer = this.$refs.jswebrtc, myCanvas = this.$refs.myCanvas, line = this.$refs.line
+      let width = videoPlayer.offsetWidth, height = videoPlayer.offsetHeight
+      let context = myCanvas.getContext('2d')
 
-      let videoStream = videoPlayer.captureStream(), lineStream = line.captureStream();
+      let videoStream = videoPlayer.captureStream(), lineStream = line.captureStream()
       let render = () => {
         if (videoStream) {
           context.drawImage(videoPlayer, 0, 0, width, height)
@@ -355,7 +529,7 @@ export default {
           window.requestAnimationFrame(render)
         }
       }
-      render();
+      render()
       // 创建新的媒体流
       let newStream = myCanvas.captureStream(25)
       // 合并音频
@@ -366,81 +540,81 @@ export default {
     // 开始录像
     // 替换 recoder() 方法
     recoder() {
-      this.$message({ message: '开始录像', type: "success" })
+      this.$message({ message: '开始录像', type: 'success' })
       this.rectZoomDestroy()
       let stream = this.mergeStream(), videoPlayer = this.$refs.jswebrtc
-      let mime = MediaRecorder.isTypeSupported("video/webm; codecs=vp9") ? "video/webm; codecs=vp9" : "video/webm"
+      let mime = MediaRecorder.isTypeSupported('video/webm; codecs=vp9') ? 'video/webm; codecs=vp9' : 'video/webm'
       this.recorder = new MediaRecorder(stream, {
         mimeType: mime
       })
 
       // [修改] 清空旧数据
-      this.videoData = [];
+      this.videoData = []
 
       this.recorder.ondataavailable = (e) => {
-        this.videoData.push(e.data);
+        this.videoData.push(e.data)
       }
 
       // [新增] onstop 事件处理器
       this.recorder.onstop = () => {
-        console.log("MediaRecorder.onstop 触发");
+        console.log('MediaRecorder.onstop 触发')
 
         if (parseInt((new Date().getTime() - this.startRecordTime) / 1000) <= 10) {
-          this.$message({ message: '无效录像,录像时间太短,大于10s录像开始保存', type: "warning" })
+          this.$message({ message: '无效录像,录像时间太短,大于10s录像开始保存', type: 'warning' })
         } else if (this.shouldSaveOnStop) {
           // 【核心】只有在需要保存时才执行
-          this.$message({ message: '结束录像,正在保存...', type: "success" })
+          this.$message({ message: '结束录像,正在保存...', type: 'success' })
 
           return new Promise((resolve) => {
             setTimeout(() => {
               let blob = new Blob(this.videoData, {
-                type: "video/mp4"
+                type: 'video/mp4'
               })
               console.log(blob)
-              let a = document.createElement("a")
-              a.download = `${formatDate(new Date().getTime())}-${this.cameraName}-${this.$store.getters.name}.mp4`;
-              a.href = URL.createObjectURL(blob);
+              let a = document.createElement('a')
+              a.download = `${formatDate(new Date().getTime())}-${this.cameraName}-${this.$store.getters.name}.mp4`
+              a.href = URL.createObjectURL(blob)
               document.body.appendChild(a)
               a.click()
               a.remove()
               window.URL.revokeObjectURL(a.href)
               resolve()
-            }, 0);
+            }, 0)
           })
         } else {
-          this.$message({ message: '录像已停止,未保存', type: "info" });
+          this.$message({ message: '录像已停止,未保存', type: 'info' })
         }
 
         // [新增] 无论是否保存,都清理
         this.recorder = null
         this.videoData = []
         this.startRecordTime = ''
-      };
+      }
 
-      this.recorder.start();
+      this.recorder.start()
 
       this.startRecordTime = new Date().getTime()
-      this.recording = true;
+      this.recording = true
       this.boxList[this.videoId - 1].recording = true
       console.log(this.boxList)
     },
     // 结束录像
     endRecoder() {
       // 按钮点击 = 总是保存
-      this.stopRecording(true);
+      this.stopRecording(true)
     },
     // [新增] 此方法
     stopRecording(shouldSave) {
-      if (!this.recording || !this.recorder) return;
+      if (!this.recording || !this.recorder) return
 
-      console.log(`webrtcPlayer ${this.videoId} 停止录像, shouldSave: ${shouldSave}`);
+      console.log(`webrtcPlayer ${this.videoId} 停止录像, shouldSave: ${shouldSave}`)
 
-      this.shouldSaveOnStop = shouldSave; // 设置标志位
-      this.recording = false;
-      this.boxList[this.videoId - 1].recording = false;
+      this.shouldSaveOnStop = shouldSave // 设置标志位
+      this.recording = false
+      this.boxList[this.videoId - 1].recording = false
 
       // 调用 stop() 会触发 onstop 事件
-      this.recorder.stop();
+      this.recorder.stop()
     },
     // async endRecoder() {
     //   this.rectZoomInit()
@@ -546,8 +720,8 @@ export default {
         this.mouseY2 = $event.clientY
         // A(左上) part
         if (this.mouseX2 < this.downX && this.mouseY2 < this.downY) {
-          this.rect.style.left = (this.mouseX2 - this.left) + "px"
-          this.rect.style.top = (this.mouseY2 - this.top) + "px"
+          this.rect.style.left = (this.mouseX2 - this.left) + 'px'
+          this.rect.style.top = (this.mouseY2 - this.top) + 'px'
           this.videoZoomFlag = false
         }
         // B(右上) part
@@ -564,22 +738,22 @@ export default {
         }
         // D(右下) part
         if (this.mouseX2 > this.downX && this.mouseY2 > this.downY) {
-          this.rect.style.left = (this.downX - this.left) + "px"
-          this.rect.style.top = (this.downY - this.top) + "px"
+          this.rect.style.left = (this.downX - this.left) + 'px'
+          this.rect.style.top = (this.downY - this.top) + 'px'
           this.videoZoomFlag = true
         }
         // 选择框大小
-        this.rect.style.width = Math.abs(this.mouseX2 - this.downX) + "px"
-        this.rect.style.height = Math.abs(this.mouseY2 - this.downY) + "px"
+        this.rect.style.width = Math.abs(this.mouseX2 - this.downX) + 'px'
+        this.rect.style.height = Math.abs(this.mouseY2 - this.downY) + 'px'
         // 选择框显示
-        this.rect.style.visibility = "visible"
+        this.rect.style.visibility = 'visible'
       }
     },
     //  鼠标抬起
     up() {
       //鼠标抬起后不允许处理鼠标移动事件
       this.select = false
-      if (this.rect.style.visibility !== "hidden") {
+      if (this.rect.style.visibility !== 'hidden') {
         //获取选择框大小
         this.rectInfo.rectWidth = Math.abs(this.mouseX2 - this.downX)
         this.rectInfo.rectHeight = Math.abs(this.mouseY2 - this.downY)
@@ -628,11 +802,11 @@ export default {
           this.$refs.videoZoom.style.width = this.rectInfo.videoWidth * times + 'px'
           this.$refs.videoZoom.style.height = 9 / 16 * this.rectInfo.videoWidth * times + 'px'
           // 移动放大后视频使框选区域显示在原播放窗口
-          this.$refs.videoZoom.style.top = - (this.rectInfo.rectCenterOffsetY - this.rectInfo.rectHeight / 2) * times + 'px'
-          this.$refs.videoZoom.style.left = - (this.rectInfo.rectCenterOffsetX - this.rectInfo.rectWidth / 2) * times + 'px'
+          this.$refs.videoZoom.style.top = -(this.rectInfo.rectCenterOffsetY - this.rectInfo.rectHeight / 2) * times + 'px'
+          this.$refs.videoZoom.style.left = -(this.rectInfo.rectCenterOffsetX - this.rectInfo.rectWidth / 2) * times + 'px'
           //  隐藏原视频,显示放大后视频
 
-          this.initVideo(this.videoSrc, 'zoom' + this.videoId);
+          this.initVideo(this.videoSrc, 'zoom' + this.videoId)
           this.videoShow = false
           this.videoZoomShow = true
 
@@ -641,7 +815,7 @@ export default {
         if (this.videoZoomShow) {
           this.$refs.videoZoom.style.width = 0 + 'px'
           this.$refs.videoZoom.style.height = 0 + 'px'
-          this.initVideo(this.videoSrc, this.videoId);
+          this.initVideo(this.videoSrc, this.videoId)
           this.videoShow = true
           this.videoZoomShow = false
         }
@@ -650,7 +824,7 @@ export default {
     //重置选择框
     resetRect() {
       document.removeEventListener('mouseup', this.up)
-      this.rect.style.visibility = "hidden"
+      this.rect.style.visibility = 'hidden'
       this.rect.style.width = '0px'
       this.rect.style.height = '0px'
       this.top = 0
@@ -668,12 +842,12 @@ export default {
         rectCenterOffsetX: 0,
         rectCenterOffsetY: 0
       }
-    },
+    }
   },
   beforeDestroy() {
     // 组件销毁前,清除重连定时器
     if (this.reconnectTimer) {
-      clearTimeout(this.reconnectTimer);
+      clearTimeout(this.reconnectTimer)
     }
     if (this.player) {
       this.player.pc.close()
@@ -700,6 +874,7 @@ video::-webkit-media-controls-panel {
   left: 0;
   overflow: hidden;
 }
+
 .video-zoom {
   position: relative;
 }
@@ -776,14 +951,17 @@ canvas {
   font-size: 0.8rem !important;
   transition: transform 0.1s, color 0.1s; /* 添加过渡效果 */
 }
+
 .jessibuca-btn:hover {
   transform: scale(1.3); /* 放大 */
   color: #409EFF; /* 高亮 */
 }
-::v-deep .el-icon-video-camera-solid{
+
+::v-deep .el-icon-video-camera-solid {
   color: #ff0000;
   animation: breathe 2s infinite;
 }
+
 @keyframes breathe {
   0% {
     transform: scale(1);
@@ -795,6 +973,7 @@ canvas {
     transform: scale(1);
   }
 }
+
 .buttons-box-right {
   position: absolute;
   right: 0;

+ 3 - 1
src/main.js

@@ -43,9 +43,10 @@ import VueMeta from 'vue-meta'
 import DictData from '@/components/DictData'
 // Json格式化组件
 import JsonViewer from 'vue-json-viewer'
-
 // 引入图标库
 import { OhVueIcon } from "oh-vue-icons";
+// vue-video-player插件
+import videoPlayer from 'vue-video-player'
 
 // 全局方法挂载
 Vue.prototype.getDicts = getDicts
@@ -73,6 +74,7 @@ Vue.use(directive)
 Vue.use(plugins)
 Vue.use(VueMeta)
 Vue.use(JsonViewer)
+Vue.use(videoPlayer)
 
 DictData.install()
 

+ 28 - 2
src/views/livePreview/components/videoBox.vue

@@ -25,24 +25,49 @@
         @dragend="handleDragEnd($event)"
       >
         <div class="video-wrapper">
+<!--          :video-src='active===1 ? item.mainUrl : item.auxiliaryUrl'-->
           <webrtc-player
             v-if="(item.code === 'H264' || item.code !== 'H265') && item.mainUrl"
-            :video-src='active===1 ? item.mainUrl : item.auxiliaryUrl'
+            :video-src="active===1 ? item.mainUrl : item.auxiliaryUrl"
             :video-id="item.boxId"
             :camera-name="item.name"
             ref='player'
             @destroy="close"
             class="player-instance"
           />
+<!--          :video-src='active===1 ? item.mainUrl : item.auxiliaryUrl'-->
           <jessibucaBox
             v-else-if="item.code === 'H265' && item.mainUrl"
-            :video-src='active===1 ? item.mainUrl : item.auxiliaryUrl'
+            :video-src="active===1 ? item.mainUrl : item.auxiliaryUrl"
             :video-id="item.boxId"
             :camera-name="item.name"
             ref='player'
             @destroy="close"
             class="player-instance"
           ></jessibucaBox>
+<!--          <video-player-->
+<!--            v-else-if="item.hls"-->
+<!--            class="video-player-box"-->
+<!--            style="width: 100%; height: 90%"-->
+<!--            ref="videoPlayer"-->
+<!--            :options="{-->
+<!--            playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度-->
+<!--            autoplay: true, // 禁止自动播放(浏览器限制)-->
+<!--            muted: true, // 静音-->
+<!--            controls: true, // 显示控制栏-->
+<!--            preload: 'auto',-->
+<!--            techOrder: ['html5', 'hls'], // 优先用 HLS 插件解析-->
+<!--            width: '100%',-->
+<!--            height: '100%',-->
+<!--            sources: [-->
+<!--              {-->
+<!--                type: 'application/x-mpegURL', // m3u8 对应的 MIME 类型-->
+<!--                src:  'http://10.168.1.232:8889/38/SIS-1766510219282/whep'-->
+<!--              }-->
+<!--            ],-->
+<!--            notSupportedMessage: '当前浏览器不支持播放此视频'-->
+<!--          }"-->
+<!--          />-->
 
           <div v-else class="empty-state">
             <i class="el-icon-video-camera"></i>
@@ -237,6 +262,7 @@ export default {
     },
 
     setActiveBox(item) {
+      console.log(this.$store.state.video)
       this.$store.commit('updateSelectedMonitor', item);
       if (item.cameraId) {
         this.$emit('setCurrentKeyTree', item.cameraId);

+ 3 - 2
src/views/livePreview/index.vue

@@ -797,7 +797,7 @@ export default {
     },
     // 节点双击事件
     async handleNodedbClick(node) {
-      console.log('dbclick')
+      console.log('dbclick', node)
       this.fishEyeType = '鱼眼'
       const debounceTimer = this.timer
       if (debounceTimer) {
@@ -1001,6 +1001,7 @@ export default {
         })
     },
     getPlayUrl(item) {
+      console.log(item, '-=-=-=0=0=')
       // 清空流地址
       this.$store.commit('updateSelectedMonitor', {
         ...item,
@@ -1207,7 +1208,7 @@ export default {
     async startPolling(duration, interval) {
       const canProceed = await this.checkRecordingAndProceed({
         targetBox: null, // 检查所有
-        actionName: '开始轮'
+        actionName: '开始轮'
       });
 
       if (!canProceed) {

+ 252 - 135
src/views/service/flowIngestionAndForwarding/index.vue

@@ -36,17 +36,17 @@
       :data="streamList"
       border
     >
-      <el-table-column label="序号" align="center" type="index" width="55" />
+      <el-table-column label="序号" align="center" type="index" width="55"/>
       <el-table-column label="通道名称" align="center" prop="app" min-width="120" :show-overflow-tooltip="true"/>
-      <el-table-column label="摄像头名称" align="center" prop="cameraName" min-width="120" :show-overflow-tooltip="true"/>
-
+      <el-table-column label="摄像头名称" align="center" prop="cameraName" min-width="120"
+                       :show-overflow-tooltip="true"
+      />
       <el-table-column label="状态" align="center" prop="status" width="100">
         <template v-slot="scope">
-          <el-tag effect="dark" type="success" v-if="scope.row.status=='1'">在线</el-tag>
+          <el-tag effect="dark" type="success" v-if="scope.row.status == '1'">在线</el-tag>
           <el-tag effect="dark" type="danger" v-else>离线</el-tag>
         </template>
       </el-table-column>
-
       <el-table-column label="启用" align="center" prop="enable" width="80">
         <template v-slot="scope">
           <el-switch
@@ -57,24 +57,22 @@
           ></el-switch>
         </template>
       </el-table-column>
-
       <el-table-column label="接入类型" align="center" prop="typeOfBitstream" width="100">
         <template v-slot="scope">
-          <el-tag v-if="scope.row.typeOfBitstream=='0'" type="primary">主码流</el-tag>
+          <el-tag v-if="scope.row.typeOfBitstream == '0'" type="primary">主码流</el-tag>
           <el-tag v-else type="warning">辅码流</el-tag>
         </template>
       </el-table-column>
-
       <el-table-column label="协议" align="center" prop="type" width="80">
         <template v-slot="scope">
           <el-tag effect="plain" type="info">{{ scope.row.type }}</el-tag>
         </template>
       </el-table-column>
-
       <el-table-column label="RTSP地址" align="center" prop="url" :show-overflow-tooltip="true" min-width="200"/>
-      <el-table-column label="录像存储路径" align="center" prop="savePath" :show-overflow-tooltip="true" min-width="150"/>
+      <el-table-column label="录像存储路径" align="center" prop="savePath" :show-overflow-tooltip="true"
+                       min-width="150"
+      />
       <el-table-column label="添加时间" align="center" prop="createTime" width="160"/>
-
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="180">
         <template v-slot="scope">
           <el-button
@@ -111,6 +109,7 @@
       @pagination="getList"
     />
 
+    <!-- 新增或修改 -->
     <el-dialog :title="title" :visible.sync="open" width="850px" append-to-body top="5vh">
       <el-form ref="form" :model="form" :rules="rules" label-width="130px" class="custom-form">
 
@@ -151,6 +150,67 @@
               </el-form-item>
             </el-col>
           </el-row>
+          <el-row>
+            <el-col :span="12">
+              <el-form-item label="媒体服务器类型" prop="streamServerType">
+                <el-select
+                  v-model="form.streamServerType"
+                  placeholder="请选择媒体服务器类型"
+                  @change="serverChange"
+                  clearable
+                  style="width: 100%"
+                >
+                  <el-option
+                    v-for="dict in dict.type.stream_server_type"
+                    :key="dict.value"
+                    :label="dict.label"
+                    :value="dict.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="媒体服务器ID" prop="mediaServerId">
+                <el-select
+                  v-model="form.mediaServerId"
+                  placeholder="请选择媒体服务器ID"
+                  clearable
+                  style="width: 100%"
+                >
+                  <el-option
+                    v-for="item in mediaServerlist"
+                    :key="item.id"
+                    :label="item.secret"
+                    :value="item.id"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="12">
+              <el-form-item label="录像服务器引擎" prop="recordEngine">
+                <el-select
+                  v-model="form.recordEngine"
+                  placeholder="请选择录像服务器引擎"
+                  clearable
+                  style="width: 100%"
+                >
+                  <el-option
+                    v-for="dict in dict.type.stream_server_type"
+                    :key="dict.value"
+                    :label="dict.label"
+                    :value="dict.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="录像节点ID" prop="recordNodeId">
+                <el-input v-model="form.recordNodeId" placeholder="请输入录像节点ID"/>
+              </el-form-item>
+            </el-col>
+          </el-row>
         </div>
 
         <div class="form-section">
@@ -158,7 +218,9 @@
           <el-divider></el-divider>
           <el-row v-if="form.equType==='1'">
             <el-col :span="24">
-              <el-form-item label="RTSP地址" prop="url" :rules="[{required: true, message: '自定义地址不能为空', trigger: 'blur'}]">
+              <el-form-item label="RTSP地址" prop="url"
+                            :rules="[{required: true, message: '自定义地址不能为空', trigger: 'blur'}]"
+              >
                 <el-input v-model="form.url" placeholder="rtsp://..." show-word-limit maxlength="255"/>
               </el-form-item>
             </el-col>
@@ -189,7 +251,9 @@
             </el-col>
             <el-col :span="12">
               <el-form-item label="通道启用" prop="enable">
-                <el-switch v-model="form.enable" active-value="1" inactive-value="0" active-text="启用" inactive-text="禁用"></el-switch>
+                <el-switch v-model="form.enable" active-value="1" inactive-value="0" active-text="启用"
+                           inactive-text="禁用"
+                ></el-switch>
               </el-form-item>
             </el-col>
           </el-row>
@@ -201,7 +265,9 @@
           <el-row>
             <el-col :span="24">
               <el-form-item label="录像存储路径" prop="savePath">
-                <el-input v-model="form.savePath" placeholder="例如: /record/live/stream1 (留空则使用默认路径)" maxlength="255">
+                <el-input v-model="form.savePath" placeholder="例如: /record/live/stream1 (留空则使用默认路径)"
+                          maxlength="255"
+                >
                   <i slot="prefix" class="el-input__icon el-icon-folder"></i>
                 </el-input>
               </el-form-item>
@@ -217,8 +283,12 @@
               </el-form-item>
             </el-col>
             <el-col :span="12">
-              <el-form-item v-if="form.processing==='1'" label="覆盖周期" prop="retainDays" :rules="[{required: true, message: '不能为空', trigger: 'blur'}]">
-                <el-input-number v-model="form.retainDays" :min="1" :step="1" controls-position="right" size="small" style="width: 120px"></el-input-number>
+              <el-form-item v-if="form.processing==='1'" label="覆盖周期" prop="retainDays"
+                            :rules="[{required: true, message: '不能为空', trigger: 'blur'}]"
+              >
+                <el-input-number v-model="form.retainDays" :min="1" :step="1" controls-position="right" size="small"
+                                 style="width: 120px"
+                ></el-input-number>
                 <span class="ml10">天前视频</span>
               </el-form-item>
             </el-col>
@@ -231,8 +301,12 @@
               </el-form-item>
             </el-col>
             <el-col :span="12">
-              <el-form-item v-if="form.deleteNot==='1'" label="删除周期" prop="deleteNumber" :rules="[{required: true, message: '不能为空', trigger: 'blur'}]">
-                <el-input-number v-model="form.deleteNumber" :min="1" :step="1" controls-position="right" size="small" style="width: 120px"></el-input-number>
+              <el-form-item v-if="form.deleteNot==='1'" label="删除周期" prop="deleteNumber"
+                            :rules="[{required: true, message: '不能为空', trigger: 'blur'}]"
+              >
+                <el-input-number v-model="form.deleteNumber" :min="1" :step="1" controls-position="right" size="small"
+                                 style="width: 120px"
+                ></el-input-number>
                 <span class="ml10">天</span>
               </el-form-item>
             </el-col>
@@ -246,8 +320,12 @@
             </el-col>
             <template v-if="form.isYuhang=='1'">
               <el-col :span="12">
-                <el-form-item label="选择设备" prop="yuhangId" :rules="[{required: true, message: '请选择存储设备', trigger: 'change'}]">
-                  <el-select v-model="form.yuhangId" placeholder="请选择" @change="form.yuhangNum=1" style="width: 100%">
+                <el-form-item label="选择设备" prop="yuhangId"
+                              :rules="[{required: true, message: '请选择存储设备', trigger: 'change'}]"
+                >
+                  <el-select v-model="form.yuhangId" placeholder="请选择" @change="form.yuhangNum=1"
+                             style="width: 100%"
+                  >
                     <el-option
                       v-for="item in yuhangOptions"
                       :key="item.id"
@@ -258,8 +336,12 @@
                 </el-form-item>
               </el-col>
               <el-col :span="12" :offset="12">
-                <el-form-item label="存储通道" prop="yuhangNum" :rules="[{required: true, message: '不能为空', trigger: 'blur'}]">
-                  <el-input-number :disabled="!form.yuhangId" v-model="form.yuhangNum" :min="1" :max="16" controls-position="right" size="small"></el-input-number>
+                <el-form-item label="存储通道" prop="yuhangNum"
+                              :rules="[{required: true, message: '不能为空', trigger: 'blur'}]"
+                >
+                  <el-input-number :disabled="!form.yuhangId" v-model="form.yuhangNum" :min="1" :max="16"
+                                   controls-position="right" size="small"
+                  ></el-input-number>
                 </el-form-item>
               </el-col>
             </template>
@@ -295,6 +377,7 @@
       </div>
     </el-dialog>
 
+    <!-- 预览 -->
     <el-dialog :title="title" :visible.sync="openView" width="800px" append-to-body @close="cancel">
       <el-descriptions :column="2" border label-class-name="view-label">
         <el-descriptions-item label="通道编号">{{ form.id }}</el-descriptions-item>
@@ -319,7 +402,8 @@
           <el-tag v-if="form.deleteNot=='1'" type="success">是</el-tag>
           <el-tag v-else type="info">否</el-tag>
         </el-descriptions-item>
-        <el-descriptions-item v-if="form.deleteNot=='1'" label="自动删除天数">{{ form.deleteNumber }} 天</el-descriptions-item>
+        <el-descriptions-item v-if="form.deleteNot=='1'" label="自动删除天数">{{ form.deleteNumber }} 天
+        </el-descriptions-item>
         <el-descriptions-item label="创建时间">{{ form.createTime }}</el-descriptions-item>
       </el-descriptions>
     </el-dialog>
@@ -332,14 +416,15 @@ import {
   delStreamProxy,
   listStreamProxy,
   selectEquId,
-  updateStreamProxy
-} from "@/api/service/streamProxy";
-import {cameraSelect} from "@/api/equipment/camera";
-import {yuhangSelect} from "../../../api/equipment/yuhang";
+  updateStreamProxy,
+  getStreamServerTypeList
+} from '@/api/service/streamProxy'
+import { cameraSelect } from '@/api/equipment/camera'
+import { yuhangSelect } from '../../../api/equipment/yuhang'
 
 export default {
-  name: "cameraEquipmentMGT",
-  dicts: ['sys_normal_disable', "sys_yes_no","sys_online_offline"],
+  name: 'cameraEquipmentMGT',
+  dicts: ['sys_normal_disable', 'sys_yes_no', 'sys_online_offline', 'stream_server_type'],
   data() {
     return {
       // 遮罩层
@@ -351,7 +436,7 @@ export default {
       // 总条数
       total: 0,
       // 弹出层标题
-      title: "",
+      title: '',
       // 是否显示弹出层
       open: false,
       openView: false,
@@ -368,210 +453,236 @@ export default {
       // 表单校验
       rules: {
         app: [
-          {required: true, message: "通道名称不能为空", trigger: "blur"}
+          { required: true, message: '通道名称不能为空', trigger: 'blur' }
         ],
         cameraId: [
-          {required: true, message: "摄像头名称不能为空", trigger: "change"}
+          { required: true, message: '摄像头名称不能为空', trigger: 'change' }
         ],
         typeOfBitstream: [
-          {required: true, message: "码流类型不能为空", trigger: "blur"}
+          { required: true, message: '码流类型不能为空', trigger: 'blur' }
         ],
         type: [
-          {required: true, message: "传输协议不能为空", trigger: "blur"}
+          { required: true, message: '传输协议不能为空', trigger: 'blur' }
         ],
         enable: [
-          {required: true, message: "请选择", trigger: "blur"}
+          { required: true, message: '请选择', trigger: 'blur' }
         ],
         enableAudio: [
-          {required: true, message: "请选择", trigger: "blur"}
+          { required: true, message: '请选择', trigger: 'blur' }
         ],
         timeoutMs: [
-          {required: true, message: "超时时间不能为空", trigger: "blur"}
+          { required: true, message: '超时时间不能为空', trigger: 'blur' }
         ],
         equType: [
-          {required: true, message: "类型不能为空", trigger: "blur"}
+          { required: true, message: '类型不能为空', trigger: 'blur' }
         ],
         processing: [
-          {required: true, message: "请选择", trigger: "blur"}
+          { required: true, message: '请选择', trigger: 'blur' }
         ],
         deleteNot: [
-          {required: true, message: "请选择", trigger: "blur"}
+          { required: true, message: '请选择', trigger: 'blur' }
         ],
         isYuhang: [
-          {required: true, message: "请选择", trigger: "blur"}
+          { required: true, message: '请选择', trigger: 'blur' }
         ],
         savePath: [
           { max: 255, message: '长度不能超过255个字符', trigger: 'blur' }
-        ]
+        ],
+        streamServerType: [{ required: true, message: '媒体服务器类型不能为空', trigger: 'change' }],
+        mediaServerId: [{ required: false, message: '媒体服务器ID不能为空', trigger: 'change' }],
+        recordEngine: [{ required: true, message: '录像服务器引擎不能为空', trigger: 'change' }],
+        recordNodeId: [{ required: true, message: '录像节点ID不能为空', trigger: 'blur' }]
       },
       //   设备下拉框
       cameraOptions: [],
       //   玉航存储下拉框
-      yuhangOptions:[],
-    };
+      yuhangOptions: [],
+
+      mediaServerlist: []
+    }
   },
-  watch:{
-    'form.processing': function(val){
-      if(val==='0'){
-        this.$set(this.form,'retainDays',null)
+  watch: {
+    'form.processing': function(val) {
+      if (val === '0') {
+        this.$set(this.form, 'retainDays', null)
       }
     },
-    'form.deleteNot': function(val){
-      if(val==='0'){
-        this.$set(this.form,'deleteNumber',null)
+    'form.deleteNot': function(val) {
+      if (val === '0') {
+        this.$set(this.form, 'deleteNumber', null)
       }
-    },
+    }
   },
   created() {
-    this.getList();
-    this.getCameraOptions();
-    this.getYuhangOptions();
+    this.getList()
+    this.getCameraOptions()
+    this.getYuhangOptions()
   },
   methods: {
+    // 媒体服务器类型切换
+    serverChange(data) {
+      this.form.mediaServerId = null
+      this.getServerType(data)
+    },
+    // 获取媒体服务器类型
+    getServerType(id) {
+      const param = {
+        streamServerType: id
+      }
+      getStreamServerTypeList(param)
+        .then(res => {
+          this.mediaServerlist = res.data || []
+        })
+        .catch(() => {
+        })
+    },
     /** 查询设备列表 */
     getList() {
-      this.loading = true;
+      this.loading = true
       listStreamProxy(this.queryParams).then(response => {
-        this.streamList = response.rows;
-        this.total = response.total;
-        this.loading = false;
-      });
+        this.streamList = response.rows
+        this.total = response.total
+        this.loading = false
+      })
     },
     /** 查询下拉框 */
     getCameraOptions() {
       cameraSelect().then(response => {
-        this.cameraOptions = response.rows;
-      });
+        this.cameraOptions = response.rows
+      })
     },
     getYuhangOptions() {
       yuhangSelect().then(response => {
-        this.yuhangOptions = response;
-      });
+        this.yuhangOptions = response
+      })
     },
     // 取消按钮
     cancel() {
-      this.open = false;
-      this.openView = false;
-      this.reset();
+      this.open = false
+      this.openView = false
+      this.reset()
     },
     // 表单重置
     reset() {
       this.form = {
         savePath: null
-      };
-      this.resetForm("form");
+      }
+      this.resetForm('form')
     },
     /** 搜索按钮操作 */
     handleQuery() {
-      this.getList();
+      this.getList()
     },
     /** 重置按钮操作 */
     resetQuery() {
-      this.resetForm("queryForm");
-      this.handleQuery();
+      this.resetForm('queryForm')
+      this.handleQuery()
     },
     /** 新增按钮操作 */
     handleAdd() {
-      this.reset();
+      this.reset()
       selectEquId().then(response => {
-        this.form.stream = response.msg;
+        this.form.stream = response.msg
         this.form.timeoutMs = 10
-        this.$set(this.form,'typeOfBitstream','1')
-        this.$set(this.form,'type','TCP')
-        this.$set(this.form,'enable','1')
-        this.$set(this.form,'enableAudio','0')
+        this.$set(this.form, 'typeOfBitstream', '1')
+        this.$set(this.form, 'type', 'TCP')
+        this.$set(this.form, 'enable', '1')
+        this.$set(this.form, 'enableAudio', '0')
         this.form.processing = '1'
-        this.$set(this.form,'deleteNot','0')
-        this.$set(this.form,'isUsedSis','1')
-        this.$set(this.form,'recordShow','1')
-        this.$set(this.form,'equType','0')
-        this.$set(this.form,'isYuhang','0')
-        if(this.yuhangOptions && this.yuhangOptions.length > 0) {
-          this.$set(this.form,'yuhangId',this.yuhangOptions[0].id)
+        this.$set(this.form, 'deleteNot', '0')
+        this.$set(this.form, 'isUsedSis', '1')
+        this.$set(this.form, 'recordShow', '1')
+        this.$set(this.form, 'equType', '0')
+        this.$set(this.form, 'isYuhang', '0')
+        if (this.yuhangOptions && this.yuhangOptions.length > 0) {
+          this.$set(this.form, 'yuhangId', this.yuhangOptions[0].id)
         }
-        this.$set(this.form,'yuhangNum',1)
+        this.$set(this.form, 'yuhangNum', 1)
         this.$set(this.form, 'savePath', null)
+        // this.$set(this.form, 'streamServerType', 'zlm')
+        this.$set(this.form, 'mediaServerId', '')
+        // this.$set(this.form, 'recordEngine', '')
+        this.$set(this.form, 'recordNodeId', '')
 
-        this.open = true;
-        this.title = "添加通道";
-      });
-
+        this.open = true
+        this.title = '添加通道'
+      })
     },
     /** 修改按钮操作 */
     handleUpdate(row) {
-      this.reset();
-      this.form = JSON.parse(JSON.stringify(row));
-      this.form.alertThreshold = Number(row.alertThreshold);
-      this.form.cameraId = Number(row.cameraId);
-      if(row.yuhangId==='' || row.yuhangId===null){
+      this.reset()
+      this.form = JSON.parse(JSON.stringify(row))
+      this.form.alertThreshold = Number(row.alertThreshold)
+      this.form.cameraId = Number(row.cameraId)
+      if (row.yuhangId === '' || row.yuhangId === null) {
         this.form.yuhangId = null
-      }else{
+      } else {
         this.form.yuhangId = Number(row.yuhangId)
       }
 
-      this.open = true;
-      this.title = "修改通道";
+      this.open = true
+      this.title = '修改通道'
     },
     /** 详细按钮操作 */
     handleView(row) {
-      this.reset();
-      this.form = JSON.parse(JSON.stringify(row));
-      this.title = "通道详情";
-      this.openView = true;
+      this.reset()
+      this.form = JSON.parse(JSON.stringify(row))
+      this.title = '通道详情'
+      this.openView = true
     },
     handleStatusChange(row) {
-      let text = row.enable === "1" ? "确认要启用吗?" : "确认要停用吗?停用后无法进行流的转发和录制!";
-      this.$modal.confirm(text).then(function () {
-        return updateStreamProxy(row);
+      let text = row.enable === '1' ? '确认要启用吗?' : '确认要停用吗?停用后无法进行流的转发和录制!'
+      this.$modal.confirm(text).then(function() {
+        return updateStreamProxy(row)
       }).then(() => {
-        this.$modal.msgSuccess("成功");
-      }).catch(function () {
-        row.enable = row.enable === "0" ? "1" : "0";
-      });
+        this.$modal.msgSuccess('成功')
+      }).catch(function() {
+        row.enable = row.enable === '0' ? '1' : '0'
+      })
     },
     /** 提交按钮 */
-    submitForm: function () {
-      this.$refs["form"].validate(valid => {
+    submitForm: function() {
+      this.$refs['form'].validate(valid => {
         if (valid) {
-          this.$modal.loading("正在加载,请稍候!");
+          this.$modal.loading('正在加载,请稍候!')
           if (this.form.id != undefined) {
             updateStreamProxy(this.form).then(response => {
-              this.$modal.msgSuccess("修改成功");
-              this.$modal.closeLoading();
-              this.open = false;
-              this.getList();
-              return;
-            }).catch(()=>{
-              this.$modal.msgError("修改失败");
-              this.$modal.closeLoading();
+              this.$modal.msgSuccess('修改成功')
+              this.$modal.closeLoading()
+              this.open = false
+              this.getList()
+              return
+            }).catch(() => {
+              this.$modal.msgError('修改失败')
+              this.$modal.closeLoading()
             })
           } else {
             addStreamProxy(this.form).then(response => {
-              this.$modal.msgSuccess("新增成功");
-              this.$modal.closeLoading();
-              this.open = false;
-              this.getList();
-              return;
-            }).catch(()=>{
-              this.$modal.msgError("新增失败");
-              this.$modal.closeLoading();
+              this.$modal.msgSuccess('新增成功')
+              this.$modal.closeLoading()
+              this.open = false
+              this.getList()
+              return
+            }).catch(() => {
+              this.$modal.msgError('新增失败')
+              this.$modal.closeLoading()
             })
           }
         }
-      });
+      })
     },
     /** 删除按钮操作 */
     handleDelete(row) {
-      this.$modal.confirm('是否确认删除名称为"' + row.app + '"的数据项?').then(function () {
-        return delStreamProxy(row.id);
+      this.$modal.confirm('是否确认删除名称为"' + row.app + '"的数据项?').then(function() {
+        return delStreamProxy(row.id)
       }).then(() => {
-        this.getList();
-        this.$modal.msgSuccess("删除成功");
+        this.getList()
+        this.$modal.msgSuccess('删除成功')
       }).catch(() => {
-      });
+      })
     }
   }
-};
+}
 </script>
 
 <style scoped>
@@ -579,12 +690,15 @@ export default {
 .mb8 {
   margin-bottom: 8px;
 }
+
 .ml10 {
   margin-left: 10px;
 }
+
 .text-danger {
   color: #ff4949;
 }
+
 /* 表单分组标题样式 */
 .section-title {
   font-size: 15px;
@@ -595,14 +709,17 @@ export default {
   margin-bottom: 10px;
   display: inline-block;
 }
+
 /* 调整分割线间距 */
 .el-divider--horizontal {
   margin: 12px 0 20px 0;
 }
+
 /* 自定义表单项间距优化 */
 .form-section {
   margin-bottom: 5px;
 }
+
 /* 输入框背景微调 */
 .gray-bg-input >>> .el-input__inner {
   background-color: #f5f7fa;