添加GB2818的例子
This commit is contained in:
parent
500d5f688a
commit
6888d46ea2
10
src/App.vue
10
src/App.vue
|
|
@ -4,8 +4,9 @@ import WebSocketDemo from './components/WebSocketDemo.vue'
|
||||||
import Zlm from './components/Zlm.vue'
|
import Zlm from './components/Zlm.vue'
|
||||||
import Wvp from './components/Wvp.vue'
|
import Wvp from './components/Wvp.vue'
|
||||||
import GB2818Live from './components/GB2818Live.vue'
|
import GB2818Live from './components/GB2818Live.vue'
|
||||||
|
import GB2818Record from './components/GB2818Record.vue'
|
||||||
|
|
||||||
type TabType = 'websocket' | 'zlm' | 'wvp' | 'gb28181'
|
type TabType = 'websocket' | 'zlm' | 'wvp' | 'gb28181' | 'gb28181-record'
|
||||||
|
|
||||||
const activeTab = ref<TabType>('websocket')
|
const activeTab = ref<TabType>('websocket')
|
||||||
|
|
||||||
|
|
@ -41,6 +42,12 @@ const switchTab = (tab: TabType) => {
|
||||||
>
|
>
|
||||||
GB28181 Live
|
GB28181 Live
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
:class="{ active: activeTab === 'gb28181-record' }"
|
||||||
|
@click="switchTab('gb28181-record')"
|
||||||
|
>
|
||||||
|
GB28181 Record
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
|
@ -48,6 +55,7 @@ const switchTab = (tab: TabType) => {
|
||||||
<Zlm v-if="activeTab === 'zlm'" />
|
<Zlm v-if="activeTab === 'zlm'" />
|
||||||
<Wvp v-if="activeTab === 'wvp'" />
|
<Wvp v-if="activeTab === 'wvp'" />
|
||||||
<GB2818Live v-if="activeTab === 'gb28181'" />
|
<GB2818Live v-if="activeTab === 'gb28181'" />
|
||||||
|
<GB2818Record v-if="activeTab === 'gb28181-record'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,814 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const GATEWAY_URL = 'http://localhost:8080'
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const channelId = ref(1)
|
||||||
|
const startTime = ref('')
|
||||||
|
const endTime = ref('')
|
||||||
|
const queryLoading = ref(false)
|
||||||
|
const queryError = ref('')
|
||||||
|
const recordList = ref<any[]>([])
|
||||||
|
const selectedRecord = ref<any>(null)
|
||||||
|
|
||||||
|
// 初始化时间为今天全天
|
||||||
|
const initTime = () => {
|
||||||
|
const today = new Date()
|
||||||
|
const year = today.getFullYear()
|
||||||
|
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(today.getDate()).padStart(2, '0')
|
||||||
|
|
||||||
|
startTime.value = `${year}-${month}-${day} 00:00:00`
|
||||||
|
endTime.value = `${year}-${month}-${day} 23:59:59`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时间
|
||||||
|
initTime()
|
||||||
|
|
||||||
|
// 查询录像列表
|
||||||
|
const queryRecordList = async () => {
|
||||||
|
if (!channelId.value) {
|
||||||
|
queryError.value = '请输入通道ID'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startTime.value || !endTime.value) {
|
||||||
|
queryError.value = '请选择开始时间和结束时间'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queryLoading.value = true
|
||||||
|
queryError.value = ''
|
||||||
|
recordList.value = []
|
||||||
|
selectedRecord.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
channelId: channelId.value.toString(),
|
||||||
|
startTime: startTime.value,
|
||||||
|
endTime: endTime.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_URL}/wvp/api/common/channel/playback/query?${params}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.code === 0 && data.data) {
|
||||||
|
recordList.value = data.data
|
||||||
|
if (recordList.value.length === 0) {
|
||||||
|
queryError.value = '未找到录像记录'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queryError.value = data.msg || '查询录像列表失败'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
queryError.value = error instanceof Error ? error.message : '查询录像列表失败'
|
||||||
|
} finally {
|
||||||
|
queryLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放相关
|
||||||
|
const playLoading = ref(false)
|
||||||
|
const playError = ref('')
|
||||||
|
const playbackData = ref<any>(null)
|
||||||
|
const currentStreamId = ref<string>('')
|
||||||
|
|
||||||
|
// Jessibuca 播放器
|
||||||
|
let jessibucaPlayer: any = null
|
||||||
|
const playerContainerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const kBps = ref(0)
|
||||||
|
|
||||||
|
// 停止当前播放流(调用API)
|
||||||
|
const stopPlaybackStream = async () => {
|
||||||
|
if (!currentStreamId.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
channelId: channelId.value.toString(),
|
||||||
|
stream: currentStreamId.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_URL}/wvp/api/common/channel/playback/stop?${params}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('停止播放流失败:', response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.code === 0) {
|
||||||
|
console.log('停止播放流成功')
|
||||||
|
} else {
|
||||||
|
console.error('停止播放流失败:', data.msg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('停止播放流异常:', error)
|
||||||
|
} finally {
|
||||||
|
currentStreamId.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放录像
|
||||||
|
const playRecord = async (record: any) => {
|
||||||
|
selectedRecord.value = record
|
||||||
|
playLoading.value = true
|
||||||
|
playError.value = ''
|
||||||
|
|
||||||
|
// 先停止当前播放的流(如果有)
|
||||||
|
if (currentStreamId.value) {
|
||||||
|
await stopPlaybackStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止播放器
|
||||||
|
stopPlay()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
channelId: channelId.value.toString(),
|
||||||
|
startTime: record.startTime,
|
||||||
|
endTime: record.endTime
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_URL}/wvp/api/common/channel/playback?${params}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.code === 0 && data.data) {
|
||||||
|
playbackData.value = data.data
|
||||||
|
playError.value = ''
|
||||||
|
|
||||||
|
// 保存stream ID用于后续停止
|
||||||
|
if (data.data.stream) {
|
||||||
|
currentStreamId.value = data.data.stream
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动播放WebSocket-FLV流
|
||||||
|
const playUrl = getPlayUrl(data.data)
|
||||||
|
if (playUrl) {
|
||||||
|
setTimeout(() => {
|
||||||
|
playJessibuca(playUrl)
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
playError.value = '未找到可用的播放地址'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playError.value = data.msg || '获取播放地址失败'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
playError.value = error instanceof Error ? error.message : '获取播放地址失败'
|
||||||
|
} finally {
|
||||||
|
playLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取播放地址(优先使用WebSocket-FLV)
|
||||||
|
const getPlayUrl = (data: any) => {
|
||||||
|
// 根据协议选择合适的流地址
|
||||||
|
if (location.protocol === 'https:') {
|
||||||
|
return data.wss_flv || data.ws_flv || data.flv
|
||||||
|
} else {
|
||||||
|
return data.ws_flv || data.wss_flv || data.flv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Jessibuca播放器播放
|
||||||
|
const playJessibuca = (url: string) => {
|
||||||
|
playError.value = ''
|
||||||
|
|
||||||
|
if (!playerContainerRef.value) {
|
||||||
|
playError.value = '播放器容器未找到'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jessibucaPlayer) {
|
||||||
|
jessibucaPlayer.destroy()
|
||||||
|
jessibucaPlayer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
jessibucaPlayer = new window.Jessibuca({
|
||||||
|
container: playerContainerRef.value,
|
||||||
|
videoBuffer: 0,
|
||||||
|
isResize: true,
|
||||||
|
useMSE: true,
|
||||||
|
useWCS: false,
|
||||||
|
text: '',
|
||||||
|
controlAutoHide: false,
|
||||||
|
debug: false,
|
||||||
|
hotKey: true,
|
||||||
|
// @ts-ignore
|
||||||
|
decoder: '/static/js/jessibuca/decoder.js',
|
||||||
|
timeout: 10,
|
||||||
|
recordType: 'mp4',
|
||||||
|
isFlv: false,
|
||||||
|
forceNoOffscreen: true,
|
||||||
|
hasAudio: true,
|
||||||
|
heartTimeout: 5,
|
||||||
|
heartTimeoutReplay: true,
|
||||||
|
heartTimeoutReplayTimes: 3,
|
||||||
|
hiddenAutoPause: false,
|
||||||
|
isNotMute: true,
|
||||||
|
keepScreenOn: true,
|
||||||
|
loadingText: '请稍等, 视频加载中......',
|
||||||
|
loadingTimeout: 10,
|
||||||
|
loadingTimeoutReplay: true,
|
||||||
|
loadingTimeoutReplayTimes: 3,
|
||||||
|
operateBtns: {
|
||||||
|
fullscreen: false,
|
||||||
|
screenshot: false,
|
||||||
|
play: false,
|
||||||
|
audio: false,
|
||||||
|
recorder: false
|
||||||
|
},
|
||||||
|
showBandwidth: false,
|
||||||
|
supportDblclickFullscreen: false,
|
||||||
|
useWebFullSreen: true,
|
||||||
|
wasmDecodeErrorReplay: true
|
||||||
|
})
|
||||||
|
|
||||||
|
jessibucaPlayer.on('play', () => {
|
||||||
|
isPlaying.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
jessibucaPlayer.on('pause', () => {
|
||||||
|
isPlaying.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
jessibucaPlayer.on('kBps', (val: number) => {
|
||||||
|
kBps.value = Math.round(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
jessibucaPlayer.on('error', (error: any) => {
|
||||||
|
console.error('Jessibuca error:', error)
|
||||||
|
playError.value = '播放错误: ' + (error.message || error)
|
||||||
|
isPlaying.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
jessibucaPlayer.on('timeout', () => {
|
||||||
|
playError.value = '播放超时'
|
||||||
|
isPlaying.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
jessibucaPlayer.play(url)
|
||||||
|
} catch (error) {
|
||||||
|
playError.value = error instanceof Error ? error.message : '播放失败'
|
||||||
|
isPlaying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止播放(仅停止播放器,不调用API)
|
||||||
|
const stopPlay = () => {
|
||||||
|
if (jessibucaPlayer) {
|
||||||
|
jessibucaPlayer.pause()
|
||||||
|
jessibucaPlayer.destroy()
|
||||||
|
jessibucaPlayer = null
|
||||||
|
}
|
||||||
|
isPlaying.value = false
|
||||||
|
kBps.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完全停止播放(停止播放器并调用API)
|
||||||
|
const stopPlayComplete = async () => {
|
||||||
|
// 停止播放器
|
||||||
|
stopPlay()
|
||||||
|
|
||||||
|
// 调用API停止流
|
||||||
|
await stopPlaybackStream()
|
||||||
|
|
||||||
|
// 清空播放数据
|
||||||
|
playbackData.value = null
|
||||||
|
selectedRecord.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatFileSize = (size: number | null) => {
|
||||||
|
if (!size) return '-'
|
||||||
|
if (size < 1024) return size + ' B'
|
||||||
|
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'
|
||||||
|
if (size < 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(2) + ' MB'
|
||||||
|
return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清理播放器和停止流
|
||||||
|
onUnmounted(async () => {
|
||||||
|
stopPlay()
|
||||||
|
await stopPlaybackStream()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="gb28181-record-container">
|
||||||
|
<h2>GB28181 国标录像回放</h2>
|
||||||
|
|
||||||
|
<div class="api-section">
|
||||||
|
<h3>1. 查询录像列表</h3>
|
||||||
|
<div class="api-description">
|
||||||
|
<p>通过网关调用: <code>GET /wvp/api/common/channel/playback/query</code></p>
|
||||||
|
<p>实际转发到: <code>http://114.67.89.4:9090/api/common/channel/playback/query</code></p>
|
||||||
|
<p>网关会自动注入 access-token 请求头</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>通道ID (channelId) - 必填:</label>
|
||||||
|
<input
|
||||||
|
v-model.number="channelId"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="请输入通道ID"
|
||||||
|
/>
|
||||||
|
<small style="color: #666; display: block; margin-top: 5px;">
|
||||||
|
提示:通道ID是数据库主键ID(gbId),不是国标设备ID
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>开始时间 (startTime):</label>
|
||||||
|
<input
|
||||||
|
v-model="startTime"
|
||||||
|
type="text"
|
||||||
|
placeholder="2025-12-10 00:00:00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>结束时间 (endTime):</label>
|
||||||
|
<input
|
||||||
|
v-model="endTime"
|
||||||
|
type="text"
|
||||||
|
placeholder="2025-12-10 23:59:59"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button @click="queryRecordList" :disabled="queryLoading">
|
||||||
|
{{ queryLoading ? '查询中...' : '搜索' }}
|
||||||
|
</button>
|
||||||
|
<button @click="initTime" class="secondary-button">
|
||||||
|
重置为今天
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="queryError" class="error">
|
||||||
|
错误: {{ queryError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="recordList.length > 0" class="record-list">
|
||||||
|
<h4>录像列表 (共 {{ recordList.length }} 条):</h4>
|
||||||
|
<table class="record-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>序号</th>
|
||||||
|
<th>开始时间</th>
|
||||||
|
<th>结束时间</th>
|
||||||
|
<th>文件大小</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(record, index) in recordList"
|
||||||
|
:key="index"
|
||||||
|
:class="{ 'selected': selectedRecord === record }"
|
||||||
|
>
|
||||||
|
<td>{{ index + 1 }}</td>
|
||||||
|
<td>{{ record.startTime }}</td>
|
||||||
|
<td>{{ record.endTime }}</td>
|
||||||
|
<td>{{ formatFileSize(record.fileSize) }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
@click="playRecord(record)"
|
||||||
|
class="play-button"
|
||||||
|
:disabled="playLoading && selectedRecord === record"
|
||||||
|
>
|
||||||
|
{{ (playLoading && selectedRecord === record) ? '加载中...' : '播放' }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-section" v-if="playbackData">
|
||||||
|
<h3>2. 录像回放</h3>
|
||||||
|
<div class="api-description">
|
||||||
|
<p>播放接口: <code>GET /wvp/api/common/channel/playback</code></p>
|
||||||
|
<p>实际转发到: <code>http://114.67.89.4:9090/api/common/channel/playback</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selected-record-info">
|
||||||
|
<h4>当前播放的录像:</h4>
|
||||||
|
<p><strong>开始时间:</strong> {{ selectedRecord?.startTime }}</p>
|
||||||
|
<p><strong>结束时间:</strong> {{ selectedRecord?.endTime }}</p>
|
||||||
|
<p><strong>文件大小:</strong> {{ formatFileSize(selectedRecord?.fileSize) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-section">
|
||||||
|
<h4>视频播放器 (Jessibuca):</h4>
|
||||||
|
<div class="video-container">
|
||||||
|
<div
|
||||||
|
ref="playerContainerRef"
|
||||||
|
class="video-player"
|
||||||
|
style="width: 100%; height: 100%; background-color: #000; position: relative;"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-info" v-if="isPlaying">
|
||||||
|
<span class="info-text">播放中</span>
|
||||||
|
<span class="info-text" style="margin-left: 20px;">码率: {{ kBps }} kb/s</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button @click="stopPlayComplete" :disabled="!isPlaying" class="stop-button">
|
||||||
|
停止播放
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="playError" class="error">
|
||||||
|
播放错误: {{ playError }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="play-urls-info">
|
||||||
|
<h4>播放地址:</h4>
|
||||||
|
<div class="url-list">
|
||||||
|
<div v-if="playbackData.ws_flv" class="url-item">
|
||||||
|
<strong>WebSocket-FLV:</strong>
|
||||||
|
<code>{{ playbackData.ws_flv }}</code>
|
||||||
|
<button @click="playJessibuca(playbackData.ws_flv)" class="small-button">播放</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="playbackData.wss_flv" class="url-item">
|
||||||
|
<strong>WebSocket-FLV (SSL):</strong>
|
||||||
|
<code>{{ playbackData.wss_flv }}</code>
|
||||||
|
<button @click="playJessibuca(playbackData.wss_flv)" class="small-button">播放</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="playbackData.flv" class="url-item">
|
||||||
|
<strong>HTTP-FLV:</strong>
|
||||||
|
<code>{{ playbackData.flv }}</code>
|
||||||
|
</div>
|
||||||
|
<div v-if="playbackData.hls" class="url-item">
|
||||||
|
<strong>HLS:</strong>
|
||||||
|
<code>{{ playbackData.hls }}</code>
|
||||||
|
</div>
|
||||||
|
<div v-if="playbackData.rtmp" class="url-item">
|
||||||
|
<strong>RTMP:</strong>
|
||||||
|
<code>{{ playbackData.rtmp }}</code>
|
||||||
|
</div>
|
||||||
|
<div v-if="playbackData.rtsp" class="url-item">
|
||||||
|
<strong>RTSP:</strong>
|
||||||
|
<code>{{ playbackData.rtsp }}</code>
|
||||||
|
</div>
|
||||||
|
<div v-if="playbackData.rtc" class="url-item">
|
||||||
|
<strong>WebRTC:</strong>
|
||||||
|
<code>{{ playbackData.rtc }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h4>使用说明:</h4>
|
||||||
|
<p>1. 输入通道ID(数据库主键ID,不是国标设备ID)</p>
|
||||||
|
<p>2. 选择开始时间和结束时间,默认为今天全天</p>
|
||||||
|
<p>3. 点击"搜索"按钮查询录像列表</p>
|
||||||
|
<p>4. 在录像列表中点击"播放"按钮播放对应的录像</p>
|
||||||
|
<p>5. 录像会使用Jessibuca播放器自动播放WebSocket-FLV流</p>
|
||||||
|
<p><strong>注意:</strong> 时间格式为 YYYY-MM-DD HH:mm:ss,例如:2025-12-10 00:00:00</p>
|
||||||
|
<p><strong>播放器:</strong> 使用Jessibuca播放器,支持WebSocket-FLV协议,低延迟高性能</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gb28181-record-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #2196f3;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-description {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-description p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-description code {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
background-color: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button:hover:not(:disabled) {
|
||||||
|
background-color: #616161;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-button {
|
||||||
|
background-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-button:hover:not(:disabled) {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
padding: 5px 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-list h4 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table thead {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table th,
|
||||||
|
.record-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table td {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table tbody tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table tbody tr.selected {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-record-info {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-record-info h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-record-info p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-section h4 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
background-color: #000;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 500px;
|
||||||
|
max-height: 600px;
|
||||||
|
display: block;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
color: #2196f3;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-urls-info {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-urls-info h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-item strong {
|
||||||
|
min-width: 80px;
|
||||||
|
color: #2196f3;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-item code {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue