|
|
@@ -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;
|