|
|
@@ -9,11 +9,7 @@
|
|
|
<div class="config-section">
|
|
|
<el-form label-width="120px">
|
|
|
<el-form-item label="go2rtc 地址">
|
|
|
- <el-input
|
|
|
- v-model="config.go2rtcUrl"
|
|
|
- placeholder="go2rtc 服务地址"
|
|
|
- style="width: 400px"
|
|
|
- >
|
|
|
+ <el-input v-model="config.go2rtcUrl" placeholder="go2rtc 服务地址" style="width: 400px">
|
|
|
<template #prepend>http://</template>
|
|
|
</el-input>
|
|
|
<el-text type="info" style="margin-left: 10px">默认端口 1984</el-text>
|
|
|
@@ -36,25 +32,127 @@
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 播放器区域 -->
|
|
|
- <div class="player-section">
|
|
|
- <div v-if="!isPlaying" class="player-placeholder">
|
|
|
- <el-icon :size="60" color="#ddd"><VideoPlay /></el-icon>
|
|
|
- <p>请配置 go2rtc 地址和流名称后点击播放</p>
|
|
|
+ <!-- 播放器和PTZ控制区域 -->
|
|
|
+ <div class="player-ptz-container">
|
|
|
+ <!-- 播放器区域 -->
|
|
|
+ <div class="player-section">
|
|
|
+ <div v-if="!isPlaying" class="player-placeholder">
|
|
|
+ <el-icon :size="60" color="#ddd"><VideoPlay /></el-icon>
|
|
|
+ <p>请配置 go2rtc 地址和流名称后点击播放</p>
|
|
|
+ </div>
|
|
|
+ <VideoPlayer
|
|
|
+ v-else
|
|
|
+ ref="playerRef"
|
|
|
+ player-type="webrtc"
|
|
|
+ :go2rtc-url="fullGo2rtcUrl"
|
|
|
+ :stream-name="config.streamName"
|
|
|
+ :autoplay="playConfig.autoplay"
|
|
|
+ :muted="playConfig.muted"
|
|
|
+ :controls="true"
|
|
|
+ @play="onPlay"
|
|
|
+ @pause="onPause"
|
|
|
+ @error="onError"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- PTZ 云台控制 -->
|
|
|
+ <div class="ptz-panel">
|
|
|
+ <div class="ptz-header">
|
|
|
+ <span>PTZ 云台控制</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- PTZ 配置 -->
|
|
|
+ <div class="ptz-config">
|
|
|
+ <el-form label-width="70px" size="small">
|
|
|
+ <el-form-item label="摄像头IP">
|
|
|
+ <el-input v-model="ptzConfig.host" placeholder="192.168.0.64" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户名">
|
|
|
+ <el-input v-model="ptzConfig.username" placeholder="admin" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="密码">
|
|
|
+ <el-input v-model="ptzConfig.password" type="password" show-password placeholder="密码" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- PTZ 方向控制 九宫格 -->
|
|
|
+ <div class="ptz-controls">
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('UP_LEFT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon><TopLeft /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('UP')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon><Top /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('UP_RIGHT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon><TopRight /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('LEFT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon><Back /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="ptz-btn ptz-center" @click="handlePTZStop">
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('RIGHT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon><Right /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('DOWN_LEFT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon><BottomLeft /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('DOWN')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon><Bottom /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('DOWN_RIGHT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon><BottomRight /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 速度控制 -->
|
|
|
+ <div class="ptz-speed">
|
|
|
+ <span>速度</span>
|
|
|
+ <el-slider v-model="ptzSpeed" :min="10" :max="100" :step="10" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <VideoPlayer
|
|
|
- v-else
|
|
|
- ref="playerRef"
|
|
|
- player-type="webrtc"
|
|
|
- :go2rtc-url="fullGo2rtcUrl"
|
|
|
- :stream-name="config.streamName"
|
|
|
- :autoplay="playConfig.autoplay"
|
|
|
- :muted="playConfig.muted"
|
|
|
- :controls="true"
|
|
|
- @play="onPlay"
|
|
|
- @pause="onPause"
|
|
|
- @error="onError"
|
|
|
- />
|
|
|
</div>
|
|
|
|
|
|
<!-- 播放控制 -->
|
|
|
@@ -89,21 +187,33 @@
|
|
|
<el-collapse-item title="go2rtc 配置说明" name="1">
|
|
|
<div class="info-content">
|
|
|
<h4>1. 下载 go2rtc</h4>
|
|
|
- <p>访问 <el-link type="primary" href="https://github.com/AlexxIT/go2rtc/releases" target="_blank">GitHub Releases</el-link> 下载对应系统版本</p>
|
|
|
+ <p>
|
|
|
+ 访问
|
|
|
+ <el-link type="primary" href="https://github.com/AlexxIT/go2rtc/releases" target="_blank">
|
|
|
+ GitHub Releases
|
|
|
+ </el-link>
|
|
|
+ 下载对应系统版本
|
|
|
+ </p>
|
|
|
|
|
|
<h4>2. 创建配置文件 go2rtc.yaml</h4>
|
|
|
- <pre class="code-block">streams:
|
|
|
+ <pre class="code-block">
|
|
|
+streams:
|
|
|
camera1: rtsp://admin:password@192.168.0.64:554/Streaming/Channels/101
|
|
|
|
|
|
webrtc:
|
|
|
candidates:
|
|
|
- - stun:stun.l.google.com:19302</pre>
|
|
|
+ - stun:stun.l.google.com:19302</pre
|
|
|
+ >
|
|
|
|
|
|
<h4>3. 启动服务</h4>
|
|
|
<pre class="code-block">./go2rtc -config go2rtc.yaml</pre>
|
|
|
|
|
|
<h4>4. 验证</h4>
|
|
|
- <p>访问 <el-link type="primary" href="http://localhost:1984" target="_blank">http://localhost:1984</el-link> 查看管理界面</p>
|
|
|
+ <p>
|
|
|
+ 访问
|
|
|
+ <el-link type="primary" href="http://localhost:1984" target="_blank">http://localhost:1984</el-link>
|
|
|
+ 查看管理界面
|
|
|
+ </p>
|
|
|
</div>
|
|
|
</el-collapse-item>
|
|
|
</el-collapse>
|
|
|
@@ -129,8 +239,20 @@ webrtc:
|
|
|
<script setup lang="ts">
|
|
|
import { ref, reactive, computed } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
-import { VideoPlay } from '@element-plus/icons-vue'
|
|
|
+import {
|
|
|
+ VideoPlay,
|
|
|
+ Top,
|
|
|
+ Bottom,
|
|
|
+ Back,
|
|
|
+ Right,
|
|
|
+ TopLeft,
|
|
|
+ TopRight,
|
|
|
+ BottomLeft,
|
|
|
+ BottomRight,
|
|
|
+ Refresh
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
import VideoPlayer from '@/components/VideoPlayer.vue'
|
|
|
+import { startPTZ, stopPTZ, PTZ_DIRECTIONS } from '@/api/ptz'
|
|
|
|
|
|
const playerRef = ref<InstanceType<typeof VideoPlayer>>()
|
|
|
|
|
|
@@ -146,6 +268,14 @@ const playConfig = reactive({
|
|
|
muted: true
|
|
|
})
|
|
|
|
|
|
+// PTZ 配置
|
|
|
+const ptzConfig = reactive({
|
|
|
+ host: '192.168.0.64',
|
|
|
+ username: 'admin',
|
|
|
+ password: 'Wxc767718929'
|
|
|
+})
|
|
|
+const ptzSpeed = ref(50)
|
|
|
+
|
|
|
// 播放状态
|
|
|
const isPlaying = ref(false)
|
|
|
|
|
|
@@ -270,6 +400,43 @@ function onPause() {
|
|
|
function onError(error: any) {
|
|
|
addLog(`播放错误: ${error?.message || JSON.stringify(error)}`, 'error')
|
|
|
}
|
|
|
+
|
|
|
+// PTZ 控制
|
|
|
+async function handlePTZ(direction: keyof typeof PTZ_DIRECTIONS) {
|
|
|
+ if (!ptzConfig.host || !ptzConfig.username || !ptzConfig.password) {
|
|
|
+ ElMessage.warning('请先配置摄像头信息')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await startPTZ(
|
|
|
+ {
|
|
|
+ host: ptzConfig.host,
|
|
|
+ username: ptzConfig.username,
|
|
|
+ password: ptzConfig.password
|
|
|
+ },
|
|
|
+ direction
|
|
|
+ )
|
|
|
+
|
|
|
+ if (result.success) {
|
|
|
+ addLog(`PTZ 移动: ${direction}`, 'info')
|
|
|
+ } else {
|
|
|
+ addLog(`PTZ 控制失败: ${result.error}`, 'error')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handlePTZStop() {
|
|
|
+ if (!ptzConfig.host) return
|
|
|
+
|
|
|
+ const result = await stopPTZ({
|
|
|
+ host: ptzConfig.host,
|
|
|
+ username: ptzConfig.username,
|
|
|
+ password: ptzConfig.password
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!result.success) {
|
|
|
+ addLog(`PTZ 停止失败: ${result.error}`, 'error')
|
|
|
+ }
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
@@ -295,14 +462,19 @@ function onError(error: any) {
|
|
|
|
|
|
.config-section {
|
|
|
margin-bottom: 20px;
|
|
|
- padding: 20px;
|
|
|
background-color: var(--bg-container);
|
|
|
border-radius: var(--radius-base);
|
|
|
}
|
|
|
|
|
|
+.player-ptz-container {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
.player-section {
|
|
|
+ flex: 1;
|
|
|
height: 500px;
|
|
|
- margin-bottom: 20px;
|
|
|
border-radius: var(--radius-base);
|
|
|
overflow: hidden;
|
|
|
background-color: #000;
|
|
|
@@ -322,6 +494,85 @@ function onError(error: any) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+.ptz-panel {
|
|
|
+ width: 280px;
|
|
|
+ background-color: var(--bg-container);
|
|
|
+ border-radius: var(--radius-base);
|
|
|
+ padding: 15px;
|
|
|
+
|
|
|
+ .ptz-header {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--text-primary);
|
|
|
+ margin-bottom: 15px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 1px solid var(--border-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .ptz-config {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ptz-controls {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ptz-btn {
|
|
|
+ aspect-ratio: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background-color: var(--bg-hover);
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ border-radius: var(--radius-sm);
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ color: var(--text-regular);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: var(--color-primary-light-9);
|
|
|
+ border-color: var(--color-primary);
|
|
|
+ color: var(--color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ background-color: var(--color-primary);
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ptz-center {
|
|
|
+ background-color: var(--bg-page);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: var(--color-primary-light-9);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ptz-speed {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+
|
|
|
+ span {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-secondary);
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-slider {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
.control-section {
|
|
|
padding: 15px 20px;
|
|
|
background-color: var(--bg-container);
|