356 lines
8.5 KiB
Vue
356 lines
8.5 KiB
Vue
<template>
|
||
<div class="camera-container">
|
||
<div class="top-header">
|
||
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter">
|
||
<el-option label="全部" value="all"></el-option>
|
||
<el-option label="在线" value="online"></el-option>
|
||
<el-option label="离线" value="offline"></el-option>
|
||
</el-select>
|
||
<div class="top-text">警戒点位</div>
|
||
<el-select v-model="selectedCameraId" placeholder="搜索摄像头名称" @change="selectCameraById" clearable filterable
|
||
class="camera-select">
|
||
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
|
||
<span>{{ camera.id }}.</span> 名称: {{ camera.name }}
|
||
</el-option>
|
||
</el-select>
|
||
|
||
</div>
|
||
|
||
<!-- 播放列表,设置为可滚动 -->
|
||
<div class="camera-grid">
|
||
<div v-for="(camera, index) in selectedCameras" :key="camera.id" class="grid-item">
|
||
<div class="stream-control">
|
||
<p class="camera-name-title">{{ camera.name }}</p>
|
||
<div class="close-button" @click="closeStream(camera)">×</div>
|
||
</div>
|
||
|
||
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
|
||
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
|
||
<el-icon size="48">
|
||
<VideoCameraFilled />
|
||
</el-icon>
|
||
</div>
|
||
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
||
class="camera-snapshot" />
|
||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
||
@click="openDialog(camera)">
|
||
<el-icon>
|
||
<VideoPlay v-if="!camera.playing" />
|
||
<VideoPause v-if="camera.playing" />
|
||
</el-icon>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 弹窗播放视频 -->
|
||
<el-dialog v-model="dialogVisible" width="50%" @close="closeDialog">
|
||
<template #title>播放摄像头: {{ currentCamera?.name }}</template>
|
||
<canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||
import { BoxApi } from '@/utils/boxApi.ts';
|
||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||
|
||
const cameras = ref([]);
|
||
const selectedCameras = ref([]);
|
||
const showButton = ref(false);
|
||
const apiInstance = new BoxApi();
|
||
const canvasRefs = ref({});
|
||
const selectedCameraId = ref(null);
|
||
const dialogVisible = ref(false); // 控制弹窗的显示与隐藏
|
||
const currentCamera = ref(null); // 当前选中的摄像头
|
||
|
||
// 弹窗中的canvas引用
|
||
const dialogCanvas = ref(null);
|
||
const filterStatus = ref("all");
|
||
|
||
const fetchCameras = async () => {
|
||
try {
|
||
const token = localStorage.getItem('alertToken');
|
||
const cameraData = await apiInstance.getAllCameras(token);
|
||
|
||
// 根据 filterStatus 筛选摄像头状态
|
||
if (filterStatus.value === "online") {
|
||
cameras.value = cameraData.filter(camera => camera.status === "online");
|
||
} else if (filterStatus.value === "offline") {
|
||
cameras.value = cameraData.filter(camera => camera.status === "offline");
|
||
} else {
|
||
cameras.value = cameraData; // 默认 "all" 显示全部摄像头
|
||
}
|
||
} catch (error) {
|
||
console.error('获取摄像头列表失败:', error);
|
||
}
|
||
};
|
||
|
||
const selectCameraById = (cameraId) => {
|
||
const camera = cameras.value.find(c => c.id === cameraId);
|
||
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
|
||
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
|
||
}
|
||
};
|
||
|
||
// 打开弹窗并开始播放
|
||
const openDialog = async (camera) => {
|
||
currentCamera.value = camera;
|
||
dialogVisible.value = true;
|
||
await nextTick();
|
||
startStreamInDialog(camera);
|
||
};
|
||
|
||
// 在弹窗中播放视频
|
||
const startStreamInDialog = async (camera) => {
|
||
const canvas = dialogCanvas.value;
|
||
|
||
if (!camera || !canvas) {
|
||
console.error('未找到对应的 canvas');
|
||
return;
|
||
}
|
||
|
||
const token = localStorage.getItem('alertToken');
|
||
const rememberedAddress = localStorage.getItem('rememberedAddress');
|
||
|
||
if (!rememberedAddress) {
|
||
alert('主机地址获取异常,请确认地址配置');
|
||
console.error('主机地址获取异常');
|
||
return;
|
||
}
|
||
try {
|
||
const response = await apiInstance.startCameraStream(token, camera.id);
|
||
camera.streamPort = response.port;
|
||
camera.playing = true;
|
||
|
||
const url = `ws://${rememberedAddress}:${camera.streamPort}/`;
|
||
// const url = `ws://192.168.28.33:${camera.streamPort}/`;
|
||
// console.log('播放路径:', url);
|
||
|
||
if (window.JSMpeg) {
|
||
const player = new window.JSMpeg.Player(url, {
|
||
canvas: canvas,
|
||
autoplay: true,
|
||
videoBufferSize: 15 * 1024 * 1024,
|
||
audioBufferSize: 5 * 1024 * 1024,
|
||
});
|
||
camera.player = player;
|
||
} else {
|
||
console.error('JSMpeg 未加载');
|
||
}
|
||
} catch (error) {
|
||
console.error('启动视频流失败:', error);
|
||
}
|
||
};
|
||
|
||
// 关闭弹窗并停止视频播放
|
||
const closeDialog = () => {
|
||
if (currentCamera.value) {
|
||
handleStopStream(currentCamera.value);
|
||
}
|
||
dialogVisible.value = false;
|
||
currentCamera.value = null;
|
||
};
|
||
|
||
const handleStopStream = async (camera) => {
|
||
const token = localStorage.getItem('alertToken');
|
||
try {
|
||
await apiInstance.stopCameraStream(token, camera.id);
|
||
camera.playing = false;
|
||
|
||
if (camera.player) {
|
||
camera.player.destroy();
|
||
camera.player = null;
|
||
}
|
||
} catch (error) {
|
||
console.error('停止视频流失败:', error);
|
||
}
|
||
};
|
||
|
||
const closeStream = (camera) => {
|
||
handleStopStream(camera);
|
||
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
|
||
};
|
||
|
||
onMounted(() => {
|
||
fetchCameras();
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
selectedCameras.value.forEach(camera => {
|
||
if (camera.player) {
|
||
camera.player.destroy();
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.camera-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.top-header {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: row;
|
||
margin: 0;
|
||
}
|
||
|
||
.status-filter {
|
||
position: relative;
|
||
top: 0vh;
|
||
left: 0vw;
|
||
width: 5vw;
|
||
margin-left: 1vh;
|
||
margin-top: 1vh;
|
||
|
||
}
|
||
|
||
::v-deep .status-filter .el-select__wrapper {
|
||
background-color: #001529;
|
||
box-shadow: 0 0 0 0 !important;
|
||
border-radius: 0;
|
||
}
|
||
|
||
::v-deep .camera-select .el-select__wrapper {
|
||
background-color: #001529;
|
||
box-shadow: 0 0 0 0 !important;
|
||
border-radius: 0;
|
||
}
|
||
|
||
::v-deep .camera-select .el-select__selected-item {
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
::v-deep .status-filter .el-select__selected-item {
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.camera-select {
|
||
position: relative;
|
||
top: 0vh;
|
||
left: 0vw;
|
||
width: 12vw;
|
||
margin-left: 0vh;
|
||
margin-top: 1vh;
|
||
}
|
||
|
||
|
||
|
||
|
||
.top-text {
|
||
display: block;
|
||
font-size: 15px;
|
||
width: 7vw;
|
||
margin: 1vh 0 0 0;
|
||
padding: 0 0 0 10vw;
|
||
/* justify-content: center; */
|
||
align-content: center;
|
||
background-color: #001529;
|
||
line-height: 0px;
|
||
color: aliceblue;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.stream-control {
|
||
display: flex;
|
||
align-items: center;
|
||
text-align: center;
|
||
background-color: black;
|
||
}
|
||
|
||
.camera-name-title {
|
||
padding: 0.2vh;
|
||
margin: 0;
|
||
font-size: 13px;
|
||
display: block;
|
||
color: white;
|
||
}
|
||
|
||
.camera-grid {
|
||
margin: 0vh 1vh;
|
||
padding: 0 0 2vh 0;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1vh 0vh;
|
||
width: 34vw;
|
||
height: 39vh;
|
||
max-height: 45vh;
|
||
overflow-y: scroll;
|
||
scrollbar-width: none;
|
||
background-color: #001529;
|
||
}
|
||
|
||
.camera-grid::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.grid-item {
|
||
margin: 1vh;
|
||
position: relative;
|
||
height: 17vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.camera-snapshot,
|
||
.camera-large {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.close-button {
|
||
position: absolute;
|
||
top: 1px;
|
||
right: 1px;
|
||
width: 15px;
|
||
height: 15px;
|
||
background-color: #000000;
|
||
color: aliceblue;
|
||
padding: 0 2px 2px 0;
|
||
border-radius: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.close-button:hover {
|
||
background-color: red;
|
||
}
|
||
|
||
.play-button-container {
|
||
position: relative;
|
||
height: 100%;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.play-button {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
z-index: 10;
|
||
opacity: 0.4;
|
||
transition: opacity 0.2s ease-in-out;
|
||
}
|
||
|
||
.play-button:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
.dialog-canvas {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
</style>
|
||
|