添加ZLM样例代码

This commit is contained in:
孙小云 2025-12-10 10:57:09 +08:00
parent 024c01e248
commit de81b78cdc
3 changed files with 1015 additions and 1 deletions

View File

@ -1,18 +1,107 @@
<script setup lang="ts">
import { ref } from 'vue'
import WebSocketDemo from './components/WebSocketDemo.vue'
import Zlm from './components/Zlm.vue'
import Wvp from './components/Wvp.vue'
type TabType = 'websocket' | 'zlm' | 'wvp'
const activeTab = ref<TabType>('websocket')
const switchTab = (tab: TabType) => {
activeTab.value = tab
}
</script>
<template>
<div id="app">
<WebSocketDemo />
<div class="tabs">
<button
:class="{ active: activeTab === 'websocket' }"
@click="switchTab('websocket')"
>
WebSocket Demo
</button>
<button
:class="{ active: activeTab === 'zlm' }"
@click="switchTab('zlm')"
>
ZLM
</button>
<button
:class="{ active: activeTab === 'wvp' }"
@click="switchTab('wvp')"
>
WVP
</button>
</div>
<div class="tab-content">
<WebSocketDemo v-if="activeTab === 'websocket'" />
<Zlm v-if="activeTab === 'zlm'" />
<Wvp v-if="activeTab === 'wvp'" />
</div>
</div>
</template>
<style>
* {
box-sizing: border-box;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 20px;
width: 100%;
}
.tabs {
display: flex;
flex-direction: row;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 10px;
width: 100%;
}
.tabs button {
padding: 10px 20px;
background-color: #f5f5f5;
color: #666;
border: none;
border-radius: 4px 4px 0 0;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
white-space: nowrap;
}
.tabs button:hover {
background-color: #e0e0e0;
}
.tabs button.active {
background-color: #2196f3;
color: white;
}
.tab-content {
animation: fadeIn 0.3s ease;
width: 100%;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

36
src/components/Wvp.vue Normal file
View File

@ -0,0 +1,36 @@
<script setup lang="ts">
</script>
<template>
<div class="wvp-container">
<h2>WVP 管理</h2>
<div class="content">
<p>WVP 视频平台管理界面</p>
</div>
</div>
</template>
<style scoped>
.wvp-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h2 {
margin-bottom: 20px;
color: #333;
}
.content {
padding: 20px;
background-color: #f5f5f5;
border-radius: 4px;
min-height: 400px;
}
.content p {
color: #666;
font-size: 16px;
}
</style>

889
src/components/Zlm.vue Normal file
View File

@ -0,0 +1,889 @@
<script setup lang="ts">
import { ref } from 'vue'
const GATEWAY_URL = 'http://127.0.0.1:8080'
// getSnap
const snapUrl = ref('rtsp://127.0.0.1:10002/live/prod')
const timeoutSec = ref(10)
const expireSec = ref(1)
const snapLoading = ref(false)
const snapError = ref('')
const snapshotImage = ref('')
const getSnapshot = async () => {
snapLoading.value = true
snapError.value = ''
snapshotImage.value = ''
try {
const params = new URLSearchParams({
url: snapUrl.value,
timeout_sec: timeoutSec.value.toString(),
expire_sec: expireSec.value.toString()
})
const response = await fetch(`${GATEWAY_URL}/zlm/index/api/getSnap?${params}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const blob = await response.blob()
snapshotImage.value = URL.createObjectURL(blob)
} catch (error) {
snapError.value = error instanceof Error ? error.message : '获取快照失败'
} finally {
snapLoading.value = false
}
}
const downloadSnapshot = () => {
if (snapshotImage.value) {
const a = document.createElement('a')
a.href = snapshotImage.value
a.download = 'snapshot.jpg'
a.click()
}
}
// startRecord
const recordType = ref(1)
const recordVhost = ref('__defaultVhost__')
const recordApp = ref('live')
const recordStream = ref('prod')
const maxSecond = ref(86400)
const recordLoading = ref(false)
const recordError = ref('')
const recordResult = ref('')
const startRecord = async () => {
recordLoading.value = true
recordError.value = ''
recordResult.value = ''
try {
const params = new URLSearchParams({
type: recordType.value.toString(),
vhost: recordVhost.value,
app: recordApp.value,
stream: recordStream.value,
max_second: maxSecond.value.toString()
})
const response = await fetch(`${GATEWAY_URL}/zlm/index/api/startRecord?${params}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
recordResult.value = JSON.stringify(data, null, 2)
} catch (error) {
recordError.value = error instanceof Error ? error.message : '开始录制失败'
} finally {
recordLoading.value = false
}
}
// isRecording
const checkType = ref(1)
const checkVhost = ref('__defaultVhost__')
const checkApp = ref('live')
const checkStream = ref('prod')
const checkLoading = ref(false)
const checkError = ref('')
const checkResult = ref('')
const checkRecording = async () => {
checkLoading.value = true
checkError.value = ''
checkResult.value = ''
try {
const params = new URLSearchParams({
type: checkType.value.toString(),
vhost: checkVhost.value,
app: checkApp.value,
stream: checkStream.value
})
const response = await fetch(`${GATEWAY_URL}/zlm/index/api/isRecording?${params}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
checkResult.value = JSON.stringify(data, null, 2)
} catch (error) {
checkError.value = error instanceof Error ? error.message : '检查录制状态失败'
} finally {
checkLoading.value = false
}
}
// getMp4RecordFile
const mp4Vhost = ref('__defaultVhost__')
const mp4App = ref('live')
const mp4Stream = ref('prod')
const mp4Period = ref('2025-12-09')
const mp4Loading = ref(false)
const mp4Error = ref('')
const mp4Result = ref('')
const getMp4RecordFile = async () => {
mp4Loading.value = true
mp4Error.value = ''
mp4Result.value = ''
try {
const params = new URLSearchParams({
vhost: mp4Vhost.value,
app: mp4App.value,
stream: mp4Stream.value,
period: mp4Period.value
})
const response = await fetch(`${GATEWAY_URL}/zlm/index/api/getMp4RecordFile?${params}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
mp4Result.value = JSON.stringify(data, null, 2)
} catch (error) {
mp4Error.value = error instanceof Error ? error.message : '获取录制文件列表失败'
} finally {
mp4Loading.value = false
}
}
// stopRecord
const stopType = ref(1)
const stopVhost = ref('__defaultVhost__')
const stopApp = ref('live')
const stopStream = ref('prod')
const stopLoading = ref(false)
const stopError = ref('')
const stopResult = ref('')
const stopRecord = async () => {
stopLoading.value = true
stopError.value = ''
stopResult.value = ''
try {
const params = new URLSearchParams({
type: stopType.value.toString(),
vhost: stopVhost.value,
app: stopApp.value,
stream: stopStream.value
})
const response = await fetch(`${GATEWAY_URL}/zlm/index/api/stopRecord?${params}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
stopResult.value = JSON.stringify(data, null, 2)
} catch (error) {
stopError.value = error instanceof Error ? error.message : '停止录制失败'
} finally {
stopLoading.value = false
}
}
// downloadFile
const downloadFilePath = ref('/opt/media/bin/www/record/live/prod/2025-12-09/2025-12-09-16-49-44-0.mp4')
const downloadLoading = ref(false)
const downloadError = ref('')
const downloadSuccess = ref('')
const downloadFile = async () => {
downloadLoading.value = true
downloadError.value = ''
downloadSuccess.value = ''
try {
const params = new URLSearchParams({
file_path: downloadFilePath.value
})
const response = await fetch(`${GATEWAY_URL}/zlm/index/api/downloadFile?${params}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const blob = await response.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
//
const fileName = downloadFilePath.value.split('/').pop() || 'recording.mp4'
a.download = fileName
a.click()
URL.revokeObjectURL(url)
downloadSuccess.value = `文件 ${fileName} 下载成功`
} catch (error) {
downloadError.value = error instanceof Error ? error.message : '下载文件失败'
} finally {
downloadLoading.value = false
}
}
// deleteRecordDirectory
const deleteVhost = ref('__defaultVhost__')
const deleteApp = ref('live')
const deleteStream = ref('prod')
const deletePeriod = ref('2025-12-09')
const deleteLoading = ref(false)
const deleteError = ref('')
const deleteResult = ref('')
const deleteRecordDirectory = async () => {
deleteLoading.value = true
deleteError.value = ''
deleteResult.value = ''
try {
const params = new URLSearchParams({
vhost: deleteVhost.value,
app: deleteApp.value,
stream: deleteStream.value,
period: deletePeriod.value
})
const response = await fetch(`${GATEWAY_URL}/zlm/index/api/deleteRecordDirectory?${params}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
deleteResult.value = JSON.stringify(data, null, 2)
} catch (error) {
deleteError.value = error instanceof Error ? error.message : '删除录制目录失败'
} finally {
deleteLoading.value = false
}
}
</script>
<template>
<div class="zlm-container">
<h2>ZLM 流媒体服务器管理</h2>
<div class="api-section">
<h3>1. 获取流快照 (getSnap)</h3>
<div class="api-description">
<p>通过网关调用: <code>GET /zlm/index/api/getSnap</code></p>
<p>实际转发到: <code>http://114.67.89.4:8778/index/api/getSnap</code></p>
</div>
<div class="form-group">
<label>RTSP URL:</label>
<input
v-model="snapUrl"
type="text"
placeholder="rtsp://127.0.0.1:10002/live/prod"
/>
</div>
<div class="form-row">
<div class="form-group">
<label>超时时间 ():</label>
<input
v-model.number="timeoutSec"
type="number"
min="1"
max="60"
/>
</div>
<div class="form-group">
<label>过期时间 ():</label>
<input
v-model.number="expireSec"
type="number"
min="1"
max="3600"
/>
</div>
</div>
<div class="button-group">
<button @click="getSnapshot" :disabled="snapLoading">
{{ snapLoading ? '获取中...' : '获取快照' }}
</button>
<button
v-if="snapshotImage"
@click="downloadSnapshot"
class="secondary"
>
下载快照
</button>
</div>
<div v-if="snapError" class="error">
错误: {{ snapError }}
</div>
<div v-if="snapshotImage" class="snapshot-preview">
<h4>快照预览:</h4>
<img :src="snapshotImage" alt="Stream Snapshot" />
</div>
</div>
<div class="api-section">
<h3>2. 开始录制 (startRecord)</h3>
<div class="api-description">
<p>通过网关调用: <code>GET /zlm/index/api/startRecord</code></p>
<p>实际转发到: <code>http://114.67.89.4:8778/index/api/startRecord</code></p>
</div>
<div class="form-row">
<div class="form-group">
<label>类型 (type):</label>
<input
v-model.number="recordType"
type="number"
min="0"
max="1"
/>
</div>
<div class="form-group">
<label>虚拟主机 (vhost):</label>
<input
v-model="recordVhost"
type="text"
placeholder="__defaultVhost__"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>应用名 (app):</label>
<input
v-model="recordApp"
type="text"
placeholder="live"
/>
</div>
<div class="form-group">
<label>流ID (stream):</label>
<input
v-model="recordStream"
type="text"
placeholder="prod"
/>
</div>
</div>
<div class="form-group">
<label>最大录制时长 ():</label>
<input
v-model.number="maxSecond"
type="number"
min="1"
placeholder="86400"
/>
</div>
<div class="button-group">
<button @click="startRecord" :disabled="recordLoading">
{{ recordLoading ? '录制中...' : '开始录制' }}
</button>
</div>
<div v-if="recordError" class="error">
错误: {{ recordError }}
</div>
<div v-if="recordResult" class="result-preview">
<h4>响应结果:</h4>
<pre>{{ recordResult }}</pre>
</div>
</div>
<div class="api-section">
<h3>3. 检查录制状态 (isRecording)</h3>
<div class="api-description">
<p>通过网关调用: <code>GET /zlm/index/api/isRecording</code></p>
<p>实际转发到: <code>http://114.67.89.4:8778/index/api/isRecording</code></p>
</div>
<div class="form-row">
<div class="form-group">
<label>类型 (type):</label>
<input
v-model.number="checkType"
type="number"
min="0"
max="1"
/>
</div>
<div class="form-group">
<label>虚拟主机 (vhost):</label>
<input
v-model="checkVhost"
type="text"
placeholder="__defaultVhost__"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>应用名 (app):</label>
<input
v-model="checkApp"
type="text"
placeholder="live"
/>
</div>
<div class="form-group">
<label>流ID (stream):</label>
<input
v-model="checkStream"
type="text"
placeholder="prod"
/>
</div>
</div>
<div class="button-group">
<button @click="checkRecording" :disabled="checkLoading">
{{ checkLoading ? '检查中...' : '检查录制状态' }}
</button>
</div>
<div v-if="checkError" class="error">
错误: {{ checkError }}
</div>
<div v-if="checkResult" class="result-preview">
<h4>响应结果:</h4>
<pre>{{ checkResult }}</pre>
</div>
</div>
<div class="api-section">
<h3>4. 获取MP4录制文件列表 (getMp4RecordFile)</h3>
<div class="api-description">
<p>通过网关调用: <code>GET /zlm/index/api/getMp4RecordFile</code></p>
<p>实际转发到: <code>http://114.67.89.4:8778/index/api/getMp4RecordFile</code></p>
</div>
<div class="form-row">
<div class="form-group">
<label>虚拟主机 (vhost):</label>
<input
v-model="mp4Vhost"
type="text"
placeholder="__defaultVhost__"
/>
</div>
<div class="form-group">
<label>应用名 (app):</label>
<input
v-model="mp4App"
type="text"
placeholder="live"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>流ID (stream):</label>
<input
v-model="mp4Stream"
type="text"
placeholder="prod"
/>
</div>
<div class="form-group">
<label>日期 (period):</label>
<input
v-model="mp4Period"
type="date"
/>
</div>
</div>
<div class="button-group">
<button @click="getMp4RecordFile" :disabled="mp4Loading">
{{ mp4Loading ? '查询中...' : '获取录制文件' }}
</button>
</div>
<div v-if="mp4Error" class="error">
错误: {{ mp4Error }}
</div>
<div v-if="mp4Result" class="result-preview">
<h4>响应结果:</h4>
<pre>{{ mp4Result }}</pre>
</div>
</div>
<div class="api-section">
<h3>5. 停止录制 (stopRecord)</h3>
<div class="api-description">
<p>通过网关调用: <code>GET /zlm/index/api/stopRecord</code></p>
<p>实际转发到: <code>http://114.67.89.4:8778/index/api/stopRecord</code></p>
</div>
<div class="form-row">
<div class="form-group">
<label>类型 (type):</label>
<input
v-model.number="stopType"
type="number"
min="0"
max="1"
/>
</div>
<div class="form-group">
<label>虚拟主机 (vhost):</label>
<input
v-model="stopVhost"
type="text"
placeholder="__defaultVhost__"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>应用名 (app):</label>
<input
v-model="stopApp"
type="text"
placeholder="live"
/>
</div>
<div class="form-group">
<label>流ID (stream):</label>
<input
v-model="stopStream"
type="text"
placeholder="prod"
/>
</div>
</div>
<div class="button-group">
<button @click="stopRecord" :disabled="stopLoading" class="danger">
{{ stopLoading ? '停止中...' : '停止录制' }}
</button>
</div>
<div v-if="stopError" class="error">
错误: {{ stopError }}
</div>
<div v-if="stopResult" class="result-preview">
<h4>响应结果:</h4>
<pre>{{ stopResult }}</pre>
</div>
</div>
<div class="api-section">
<h3>6. 下载录制文件 (downloadFile)</h3>
<div class="api-description">
<p>通过网关调用: <code>GET /zlm/index/api/downloadFile</code></p>
<p>实际转发到: <code>http://114.67.89.4:8778/index/api/downloadFile</code></p>
</div>
<div class="form-group">
<label>文件路径 (file_path):</label>
<input
v-model="downloadFilePath"
type="text"
placeholder="/opt/media/bin/www/record/live/prod/2025-12-09/2025-12-09-16-49-44-0.mp4"
/>
</div>
<div class="button-group">
<button @click="downloadFile" :disabled="downloadLoading" class="secondary">
{{ downloadLoading ? '下载中...' : '下载文件' }}
</button>
</div>
<div v-if="downloadError" class="error">
错误: {{ downloadError }}
</div>
<div v-if="downloadSuccess" class="success">
{{ downloadSuccess }}
</div>
</div>
<div class="api-section">
<h3>7. 删除录制目录 (deleteRecordDirectory)</h3>
<div class="api-description">
<p>通过网关调用: <code>GET /zlm/index/api/deleteRecordDirectory</code></p>
<p>实际转发到: <code>http://114.67.89.4:8778/index/api/deleteRecordDirectory</code></p>
</div>
<div class="form-row">
<div class="form-group">
<label>虚拟主机 (vhost):</label>
<input
v-model="deleteVhost"
type="text"
placeholder="__defaultVhost__"
/>
</div>
<div class="form-group">
<label>应用名 (app):</label>
<input
v-model="deleteApp"
type="text"
placeholder="live"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>流ID (stream):</label>
<input
v-model="deleteStream"
type="text"
placeholder="prod"
/>
</div>
<div class="form-group">
<label>日期 (period):</label>
<input
v-model="deletePeriod"
type="date"
/>
</div>
</div>
<div class="button-group">
<button @click="deleteRecordDirectory" :disabled="deleteLoading" class="danger">
{{ deleteLoading ? '删除中...' : '删除录制目录' }}
</button>
</div>
<div v-if="deleteError" class="error">
错误: {{ deleteError }}
</div>
<div v-if="deleteResult" class="result-preview">
<h4>响应结果:</h4>
<pre>{{ deleteResult }}</pre>
</div>
</div>
</div>
</template>
<style scoped>
.zlm-container {
max-width: 1000px;
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 {
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;
}
button.secondary {
background-color: #4caf50;
}
button.secondary:hover:not(:disabled) {
background-color: #45a049;
}
button.danger {
background-color: #f44336;
}
button.danger:hover:not(:disabled) {
background-color: #d32f2f;
}
.error {
margin-top: 15px;
padding: 12px;
background-color: #ffebee;
color: #c62828;
border-radius: 4px;
font-size: 14px;
}
.success {
margin-top: 15px;
padding: 12px;
background-color: #e8f5e9;
color: #2e7d32;
border-radius: 4px;
font-size: 14px;
}
.snapshot-preview {
margin-top: 20px;
padding: 15px;
background-color: #f5f5f5;
border-radius: 4px;
}
.snapshot-preview h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.snapshot-preview img {
max-width: 100%;
height: auto;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.result-preview {
margin-top: 20px;
padding: 15px;
background-color: #f5f5f5;
border-radius: 4px;
}
.result-preview h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.result-preview pre {
background-color: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
margin: 0;
}
</style>