mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +08:00
feat: 新增 RTSP 设置菜单与配置面板
This commit is contained in:
@@ -800,6 +800,25 @@ export default {
|
|||||||
keypairGenerated: 'Keypair Generated',
|
keypairGenerated: 'Keypair Generated',
|
||||||
noKeypair: 'No Keypair',
|
noKeypair: 'No Keypair',
|
||||||
},
|
},
|
||||||
|
rtsp: {
|
||||||
|
title: 'RTSP Streaming',
|
||||||
|
desc: 'Configure RTSP video output service (H.264/H.265)',
|
||||||
|
bind: 'Bind Address',
|
||||||
|
port: 'Port',
|
||||||
|
path: 'Stream Path',
|
||||||
|
pathPlaceholder: 'live',
|
||||||
|
pathHint: 'Example: rtsp://device-ip:8554/live',
|
||||||
|
codec: 'Codec',
|
||||||
|
codecHint: 'Enabling RTSP locks codec to selected value and disables MJPEG.',
|
||||||
|
allowOneClient: 'Allow One Client Only',
|
||||||
|
username: 'Username',
|
||||||
|
usernamePlaceholder: 'Empty means no authentication',
|
||||||
|
password: 'Password',
|
||||||
|
passwordPlaceholder: 'Enter new password',
|
||||||
|
passwordSet: '••••••••',
|
||||||
|
passwordHint: 'Leave empty to keep current password; enter a new value to update it.',
|
||||||
|
urlPreview: 'RTSP URL Preview',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
title: 'Connection Stats',
|
title: 'Connection Stats',
|
||||||
|
|||||||
@@ -800,6 +800,25 @@ export default {
|
|||||||
keypairGenerated: '密钥对已生成',
|
keypairGenerated: '密钥对已生成',
|
||||||
noKeypair: '密钥对未生成',
|
noKeypair: '密钥对未生成',
|
||||||
},
|
},
|
||||||
|
rtsp: {
|
||||||
|
title: 'RTSP 视频流',
|
||||||
|
desc: '配置 RTSP 推流服务(H.264/H.265)',
|
||||||
|
bind: '监听地址',
|
||||||
|
port: '端口',
|
||||||
|
path: '流路径',
|
||||||
|
pathPlaceholder: 'live',
|
||||||
|
pathHint: '访问路径,例如 rtsp://设备IP:8554/live',
|
||||||
|
codec: '编码格式',
|
||||||
|
codecHint: '启用 RTSP 后将锁定编码为所选项,并禁用 MJPEG。',
|
||||||
|
allowOneClient: '仅允许单客户端',
|
||||||
|
username: '用户名',
|
||||||
|
usernamePlaceholder: '留空表示无需认证',
|
||||||
|
password: '密码',
|
||||||
|
passwordPlaceholder: '输入新密码',
|
||||||
|
passwordSet: '••••••••',
|
||||||
|
passwordHint: '留空表示保持当前密码;如需修改请输入新密码。',
|
||||||
|
urlPreview: 'RTSP 地址预览',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
title: '连接统计',
|
title: '连接统计',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
audioConfigApi,
|
audioConfigApi,
|
||||||
hidConfigApi,
|
hidConfigApi,
|
||||||
msdConfigApi,
|
msdConfigApi,
|
||||||
|
rtspConfigApi,
|
||||||
rustdeskConfigApi,
|
rustdeskConfigApi,
|
||||||
streamConfigApi,
|
streamConfigApi,
|
||||||
videoConfigApi,
|
videoConfigApi,
|
||||||
@@ -30,6 +31,9 @@ import type {
|
|||||||
WebConfigUpdate,
|
WebConfigUpdate,
|
||||||
} from '@/types/generated'
|
} from '@/types/generated'
|
||||||
import type {
|
import type {
|
||||||
|
RtspConfigResponse as ApiRtspConfigResponse,
|
||||||
|
RtspConfigUpdate as ApiRtspConfigUpdate,
|
||||||
|
RtspStatusResponse as ApiRtspStatusResponse,
|
||||||
RustDeskConfigResponse as ApiRustDeskConfigResponse,
|
RustDeskConfigResponse as ApiRustDeskConfigResponse,
|
||||||
RustDeskConfigUpdate as ApiRustDeskConfigUpdate,
|
RustDeskConfigUpdate as ApiRustDeskConfigUpdate,
|
||||||
RustDeskStatusResponse as ApiRustDeskStatusResponse,
|
RustDeskStatusResponse as ApiRustDeskStatusResponse,
|
||||||
@@ -51,6 +55,8 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
const stream = ref<StreamConfigResponse | null>(null)
|
const stream = ref<StreamConfigResponse | null>(null)
|
||||||
const web = ref<WebConfig | null>(null)
|
const web = ref<WebConfig | null>(null)
|
||||||
const atx = ref<AtxConfig | null>(null)
|
const atx = ref<AtxConfig | null>(null)
|
||||||
|
const rtspConfig = ref<ApiRtspConfigResponse | null>(null)
|
||||||
|
const rtspStatus = ref<ApiRtspStatusResponse | null>(null)
|
||||||
const rustdeskConfig = ref<ApiRustDeskConfigResponse | null>(null)
|
const rustdeskConfig = ref<ApiRustDeskConfigResponse | null>(null)
|
||||||
const rustdeskStatus = ref<ApiRustDeskStatusResponse | null>(null)
|
const rustdeskStatus = ref<ApiRustDeskStatusResponse | null>(null)
|
||||||
const rustdeskPassword = ref<ApiRustDeskPasswordResponse | null>(null)
|
const rustdeskPassword = ref<ApiRustDeskPasswordResponse | null>(null)
|
||||||
@@ -63,6 +69,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
const streamLoading = ref(false)
|
const streamLoading = ref(false)
|
||||||
const webLoading = ref(false)
|
const webLoading = ref(false)
|
||||||
const atxLoading = ref(false)
|
const atxLoading = ref(false)
|
||||||
|
const rtspLoading = ref(false)
|
||||||
const rustdeskLoading = ref(false)
|
const rustdeskLoading = ref(false)
|
||||||
|
|
||||||
const authError = ref<string | null>(null)
|
const authError = ref<string | null>(null)
|
||||||
@@ -73,6 +80,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
const streamError = ref<string | null>(null)
|
const streamError = ref<string | null>(null)
|
||||||
const webError = ref<string | null>(null)
|
const webError = ref<string | null>(null)
|
||||||
const atxError = ref<string | null>(null)
|
const atxError = ref<string | null>(null)
|
||||||
|
const rtspError = ref<string | null>(null)
|
||||||
const rustdeskError = ref<string | null>(null)
|
const rustdeskError = ref<string | null>(null)
|
||||||
|
|
||||||
let authPromise: Promise<AuthConfig> | null = null
|
let authPromise: Promise<AuthConfig> | null = null
|
||||||
@@ -83,6 +91,8 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
let streamPromise: Promise<StreamConfigResponse> | null = null
|
let streamPromise: Promise<StreamConfigResponse> | null = null
|
||||||
let webPromise: Promise<WebConfig> | null = null
|
let webPromise: Promise<WebConfig> | null = null
|
||||||
let atxPromise: Promise<AtxConfig> | null = null
|
let atxPromise: Promise<AtxConfig> | null = null
|
||||||
|
let rtspPromise: Promise<ApiRtspConfigResponse> | null = null
|
||||||
|
let rtspStatusPromise: Promise<ApiRtspStatusResponse> | null = null
|
||||||
let rustdeskPromise: Promise<ApiRustDeskConfigResponse> | null = null
|
let rustdeskPromise: Promise<ApiRustDeskConfigResponse> | null = null
|
||||||
let rustdeskStatusPromise: Promise<ApiRustDeskStatusResponse> | null = null
|
let rustdeskStatusPromise: Promise<ApiRustDeskStatusResponse> | null = null
|
||||||
let rustdeskPasswordPromise: Promise<ApiRustDeskPasswordResponse> | null = null
|
let rustdeskPasswordPromise: Promise<ApiRustDeskPasswordResponse> | null = null
|
||||||
@@ -263,6 +273,51 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshRtspConfig() {
|
||||||
|
if (rtspLoading.value && rtspPromise) return rtspPromise
|
||||||
|
rtspLoading.value = true
|
||||||
|
rtspError.value = null
|
||||||
|
const request = rtspConfigApi.get()
|
||||||
|
.then((response) => {
|
||||||
|
rtspConfig.value = response
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
rtspError.value = normalizeErrorMessage(error)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
rtspLoading.value = false
|
||||||
|
rtspPromise = null
|
||||||
|
})
|
||||||
|
|
||||||
|
rtspPromise = request
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshRtspStatus() {
|
||||||
|
if (rtspLoading.value && rtspStatusPromise) return rtspStatusPromise
|
||||||
|
rtspLoading.value = true
|
||||||
|
rtspError.value = null
|
||||||
|
const request = rtspConfigApi.getStatus()
|
||||||
|
.then((response) => {
|
||||||
|
rtspStatus.value = response
|
||||||
|
rtspConfig.value = response.config
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
rtspError.value = normalizeErrorMessage(error)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
rtspLoading.value = false
|
||||||
|
rtspStatusPromise = null
|
||||||
|
})
|
||||||
|
|
||||||
|
rtspStatusPromise = request
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshRustdeskConfig() {
|
async function refreshRustdeskConfig() {
|
||||||
if (rustdeskLoading.value && rustdeskPromise) return rustdeskPromise
|
if (rustdeskLoading.value && rustdeskPromise) return rustdeskPromise
|
||||||
rustdeskLoading.value = true
|
rustdeskLoading.value = true
|
||||||
@@ -370,6 +425,11 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
return refreshAtx()
|
return refreshAtx()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureRtspConfig() {
|
||||||
|
if (rtspConfig.value) return Promise.resolve(rtspConfig.value)
|
||||||
|
return refreshRtspConfig()
|
||||||
|
}
|
||||||
|
|
||||||
function ensureRustdeskConfig() {
|
function ensureRustdeskConfig() {
|
||||||
if (rustdeskConfig.value) return Promise.resolve(rustdeskConfig.value)
|
if (rustdeskConfig.value) return Promise.resolve(rustdeskConfig.value)
|
||||||
return refreshRustdeskConfig()
|
return refreshRustdeskConfig()
|
||||||
@@ -423,6 +483,12 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateRtsp(update: ApiRtspConfigUpdate) {
|
||||||
|
const response = await rtspConfigApi.update(update)
|
||||||
|
rtspConfig.value = response
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
async function updateRustdesk(update: ApiRustDeskConfigUpdate) {
|
async function updateRustdesk(update: ApiRustDeskConfigUpdate) {
|
||||||
const response = await rustdeskConfigApi.update(update)
|
const response = await rustdeskConfigApi.update(update)
|
||||||
rustdeskConfig.value = response
|
rustdeskConfig.value = response
|
||||||
@@ -450,6 +516,8 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
stream,
|
stream,
|
||||||
web,
|
web,
|
||||||
atx,
|
atx,
|
||||||
|
rtspConfig,
|
||||||
|
rtspStatus,
|
||||||
rustdeskConfig,
|
rustdeskConfig,
|
||||||
rustdeskStatus,
|
rustdeskStatus,
|
||||||
rustdeskPassword,
|
rustdeskPassword,
|
||||||
@@ -461,6 +529,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
streamLoading,
|
streamLoading,
|
||||||
webLoading,
|
webLoading,
|
||||||
atxLoading,
|
atxLoading,
|
||||||
|
rtspLoading,
|
||||||
rustdeskLoading,
|
rustdeskLoading,
|
||||||
authError,
|
authError,
|
||||||
videoError,
|
videoError,
|
||||||
@@ -470,6 +539,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
streamError,
|
streamError,
|
||||||
webError,
|
webError,
|
||||||
atxError,
|
atxError,
|
||||||
|
rtspError,
|
||||||
rustdeskError,
|
rustdeskError,
|
||||||
refreshAuth,
|
refreshAuth,
|
||||||
refreshVideo,
|
refreshVideo,
|
||||||
@@ -479,6 +549,8 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
refreshStream,
|
refreshStream,
|
||||||
refreshWeb,
|
refreshWeb,
|
||||||
refreshAtx,
|
refreshAtx,
|
||||||
|
refreshRtspConfig,
|
||||||
|
refreshRtspStatus,
|
||||||
refreshRustdeskConfig,
|
refreshRustdeskConfig,
|
||||||
refreshRustdeskStatus,
|
refreshRustdeskStatus,
|
||||||
refreshRustdeskPassword,
|
refreshRustdeskPassword,
|
||||||
@@ -490,6 +562,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
ensureStream,
|
ensureStream,
|
||||||
ensureWeb,
|
ensureWeb,
|
||||||
ensureAtx,
|
ensureAtx,
|
||||||
|
ensureRtspConfig,
|
||||||
ensureRustdeskConfig,
|
ensureRustdeskConfig,
|
||||||
updateAuth,
|
updateAuth,
|
||||||
updateVideo,
|
updateVideo,
|
||||||
@@ -499,6 +572,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
updateStream,
|
updateStream,
|
||||||
updateWeb,
|
updateWeb,
|
||||||
updateAtx,
|
updateAtx,
|
||||||
|
updateRtsp,
|
||||||
updateRustdesk,
|
updateRustdesk,
|
||||||
regenerateRustdeskId,
|
regenerateRustdeskId,
|
||||||
regenerateRustdeskPassword,
|
regenerateRustdeskPassword,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
type RustDeskConfigResponse,
|
type RustDeskConfigResponse,
|
||||||
type RustDeskStatusResponse,
|
type RustDeskStatusResponse,
|
||||||
type RustDeskPasswordResponse,
|
type RustDeskPasswordResponse,
|
||||||
|
type RtspStatusResponse,
|
||||||
|
type RtspConfigUpdate,
|
||||||
type WebConfig,
|
type WebConfig,
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
import type {
|
import type {
|
||||||
@@ -70,6 +72,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Copy,
|
Copy,
|
||||||
ScreenShare,
|
ScreenShare,
|
||||||
|
Radio,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
@@ -106,6 +109,7 @@ const navGroups = computed(() => [
|
|||||||
title: t('settings.extensions'),
|
title: t('settings.extensions'),
|
||||||
items: [
|
items: [
|
||||||
{ id: 'ext-rustdesk', label: t('extensions.rustdesk.title'), icon: ScreenShare },
|
{ id: 'ext-rustdesk', label: t('extensions.rustdesk.title'), icon: ScreenShare },
|
||||||
|
{ id: 'ext-rtsp', label: t('extensions.rtsp.title'), icon: Radio },
|
||||||
{ id: 'ext-remote-access', label: t('extensions.remoteAccess.title'), icon: ExternalLink },
|
{ id: 'ext-remote-access', label: t('extensions.remoteAccess.title'), icon: ExternalLink },
|
||||||
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
|
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
|
||||||
]
|
]
|
||||||
@@ -187,6 +191,26 @@ const rustdeskLocalConfig = ref({
|
|||||||
relay_key: '',
|
relay_key: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// RTSP config state
|
||||||
|
const rtspStatus = ref<RtspStatusResponse | null>(null)
|
||||||
|
const rtspLoading = ref(false)
|
||||||
|
const rtspLocalConfig = ref<RtspConfigUpdate & { password?: string }>({
|
||||||
|
enabled: false,
|
||||||
|
bind: '0.0.0.0',
|
||||||
|
port: 8554,
|
||||||
|
path: 'live',
|
||||||
|
allow_one_client: true,
|
||||||
|
codec: 'h264',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
const rtspStreamUrl = computed(() => {
|
||||||
|
const host = window.location.hostname || '127.0.0.1'
|
||||||
|
const path = (rtspLocalConfig.value.path || 'live').trim().replace(/^\/+|\/+$/g, '') || 'live'
|
||||||
|
const port = Number(rtspLocalConfig.value.port) || 8554
|
||||||
|
return `rtsp://${host}:${port}/${path}`
|
||||||
|
})
|
||||||
|
|
||||||
// Web server config state
|
// Web server config state
|
||||||
const webServerConfig = ref<WebConfig>({
|
const webServerConfig = ref<WebConfig>({
|
||||||
http_port: 8080,
|
http_port: 8080,
|
||||||
@@ -996,6 +1020,10 @@ function normalizeRustdeskServer(value: string, defaultPort: number): string | u
|
|||||||
return `${trimmed}:${defaultPort}`
|
return `${trimmed}:${defaultPort}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRtspPath(path: string): string {
|
||||||
|
return path.trim().replace(/^\/+|\/+$/g, '') || 'live'
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeBindAddresses(addresses: string[]): string[] {
|
function normalizeBindAddresses(addresses: string[]): string[] {
|
||||||
return addresses.map(addr => addr.trim()).filter(Boolean)
|
return addresses.map(addr => addr.trim()).filter(Boolean)
|
||||||
}
|
}
|
||||||
@@ -1225,6 +1253,108 @@ function getRustdeskStatusClass(status: string | null | undefined): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadRtspConfig() {
|
||||||
|
rtspLoading.value = true
|
||||||
|
try {
|
||||||
|
const status = await configStore.refreshRtspStatus()
|
||||||
|
rtspStatus.value = status
|
||||||
|
rtspLocalConfig.value = {
|
||||||
|
enabled: status.config.enabled,
|
||||||
|
bind: status.config.bind,
|
||||||
|
port: status.config.port,
|
||||||
|
path: status.config.path,
|
||||||
|
allow_one_client: status.config.allow_one_client,
|
||||||
|
codec: status.config.codec,
|
||||||
|
username: status.config.username || '',
|
||||||
|
password: '',
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load RTSP config:', e)
|
||||||
|
} finally {
|
||||||
|
rtspLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRtspConfig() {
|
||||||
|
loading.value = true
|
||||||
|
saved.value = false
|
||||||
|
try {
|
||||||
|
const update: RtspConfigUpdate = {
|
||||||
|
enabled: !!rtspLocalConfig.value.enabled,
|
||||||
|
bind: rtspLocalConfig.value.bind?.trim() || '0.0.0.0',
|
||||||
|
port: Number(rtspLocalConfig.value.port) || 8554,
|
||||||
|
path: normalizeRtspPath(rtspLocalConfig.value.path || 'live'),
|
||||||
|
allow_one_client: !!rtspLocalConfig.value.allow_one_client,
|
||||||
|
codec: rtspLocalConfig.value.codec || 'h264',
|
||||||
|
username: (rtspLocalConfig.value.username || '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPassword = (rtspLocalConfig.value.password || '').trim()
|
||||||
|
if (nextPassword) {
|
||||||
|
update.password = nextPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
await configStore.updateRtsp(update)
|
||||||
|
await loadRtspConfig()
|
||||||
|
rtspLocalConfig.value.password = ''
|
||||||
|
saved.value = true
|
||||||
|
setTimeout(() => (saved.value = false), 2000)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save RTSP config:', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startRtsp() {
|
||||||
|
rtspLoading.value = true
|
||||||
|
try {
|
||||||
|
await configStore.updateRtsp({ enabled: true })
|
||||||
|
rtspLocalConfig.value.enabled = true
|
||||||
|
await loadRtspConfig()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to start RTSP:', e)
|
||||||
|
} finally {
|
||||||
|
rtspLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopRtsp() {
|
||||||
|
rtspLoading.value = true
|
||||||
|
try {
|
||||||
|
await configStore.updateRtsp({ enabled: false })
|
||||||
|
rtspLocalConfig.value.enabled = false
|
||||||
|
await loadRtspConfig()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to stop RTSP:', e)
|
||||||
|
} finally {
|
||||||
|
rtspLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRtspServiceStatusText(status: string | undefined): string {
|
||||||
|
if (!status) return t('extensions.stopped')
|
||||||
|
switch (status) {
|
||||||
|
case 'running': return t('extensions.running')
|
||||||
|
case 'starting': return t('extensions.starting')
|
||||||
|
case 'stopped': return t('extensions.stopped')
|
||||||
|
default:
|
||||||
|
if (status.startsWith('error:')) return t('extensions.failed')
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRtspStatusClass(status: string | undefined): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'running': return 'bg-green-500'
|
||||||
|
case 'starting': return 'bg-yellow-500'
|
||||||
|
case 'stopped': return 'bg-gray-400'
|
||||||
|
default:
|
||||||
|
if (status?.startsWith('error:')) return 'bg-red-500'
|
||||||
|
return 'bg-gray-400'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Load theme preference
|
// Load theme preference
|
||||||
@@ -1244,6 +1374,7 @@ onMounted(async () => {
|
|||||||
loadAtxDevices(),
|
loadAtxDevices(),
|
||||||
loadRustdeskConfig(),
|
loadRustdeskConfig(),
|
||||||
loadRustdeskPassword(),
|
loadRustdeskPassword(),
|
||||||
|
loadRtspConfig(),
|
||||||
loadWebServerConfig(),
|
loadWebServerConfig(),
|
||||||
])
|
])
|
||||||
usernameInput.value = authStore.user || ''
|
usernameInput.value = authStore.user || ''
|
||||||
@@ -2341,6 +2472,121 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- RTSP Section -->
|
||||||
|
<div v-show="activeSection === 'ext-rtsp'" class="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<CardTitle>{{ t('extensions.rtsp.title') }}</CardTitle>
|
||||||
|
<CardDescription>{{ t('extensions.rtsp.desc') }}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Badge :variant="rtspStatus?.service_status === 'running' ? 'default' : 'secondary'">
|
||||||
|
{{ getRtspServiceStatusText(rtspStatus?.service_status) }}
|
||||||
|
</Badge>
|
||||||
|
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadRtspConfig" :disabled="rtspLoading">
|
||||||
|
<RefreshCw :class="['h-4 w-4', rtspLoading ? 'animate-spin' : '']" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div :class="['w-2 h-2 rounded-full', getRtspStatusClass(rtspStatus?.service_status)]" />
|
||||||
|
<span class="text-sm">{{ getRtspServiceStatusText(rtspStatus?.service_status) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
v-if="rtspStatus?.service_status !== 'running'"
|
||||||
|
size="sm"
|
||||||
|
@click="startRtsp"
|
||||||
|
:disabled="rtspLoading"
|
||||||
|
>
|
||||||
|
<Play class="h-4 w-4 mr-1" />
|
||||||
|
{{ t('extensions.start') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
@click="stopRtsp"
|
||||||
|
:disabled="rtspLoading"
|
||||||
|
>
|
||||||
|
<Square class="h-4 w-4 mr-1" />
|
||||||
|
{{ t('extensions.stop') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||||
|
<Switch v-model="rtspLocalConfig.enabled" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
|
<Label class="sm:text-right">{{ t('extensions.rtsp.bind') }}</Label>
|
||||||
|
<Input v-model="rtspLocalConfig.bind" class="sm:col-span-3" placeholder="0.0.0.0" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
|
<Label class="sm:text-right">{{ t('extensions.rtsp.port') }}</Label>
|
||||||
|
<Input v-model.number="rtspLocalConfig.port" class="sm:col-span-3" type="number" min="1" max="65535" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
|
<Label class="sm:text-right">{{ t('extensions.rtsp.path') }}</Label>
|
||||||
|
<div class="sm:col-span-3 space-y-1">
|
||||||
|
<Input v-model="rtspLocalConfig.path" :placeholder="t('extensions.rtsp.pathPlaceholder')" />
|
||||||
|
<p class="text-xs text-muted-foreground">{{ t('extensions.rtsp.pathHint') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
|
<Label class="sm:text-right">{{ t('extensions.rtsp.codec') }}</Label>
|
||||||
|
<div class="sm:col-span-3 space-y-1">
|
||||||
|
<select v-model="rtspLocalConfig.codec" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||||
|
<option value="h264">H.264</option>
|
||||||
|
<option value="h265">H.265</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ t('extensions.rtsp.codecHint') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label>{{ t('extensions.rtsp.allowOneClient') }}</Label>
|
||||||
|
<Switch v-model="rtspLocalConfig.allow_one_client" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
|
<Label class="sm:text-right">{{ t('extensions.rtsp.username') }}</Label>
|
||||||
|
<Input v-model="rtspLocalConfig.username" class="sm:col-span-3" :placeholder="t('extensions.rtsp.usernamePlaceholder')" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
|
<Label class="sm:text-right">{{ t('extensions.rtsp.password') }}</Label>
|
||||||
|
<div class="sm:col-span-3 space-y-1">
|
||||||
|
<Input
|
||||||
|
v-model="rtspLocalConfig.password"
|
||||||
|
type="password"
|
||||||
|
:placeholder="rtspStatus?.config?.has_password ? t('extensions.rtsp.passwordSet') : t('extensions.rtsp.passwordPlaceholder')"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ t('extensions.rtsp.passwordHint') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div class="rounded-md border p-3 bg-muted/20 space-y-1">
|
||||||
|
<p class="text-sm font-medium">{{ t('extensions.rtsp.urlPreview') }}</p>
|
||||||
|
<code class="font-mono text-sm break-all">{{ rtspStreamUrl }}</code>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button :disabled="loading || rtspLoading" @click="saveRtspConfig">
|
||||||
|
<Check v-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ saved ? t('common.success') : t('common.save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- RustDesk Section -->
|
<!-- RustDesk Section -->
|
||||||
<div v-show="activeSection === 'ext-rustdesk'" class="space-y-6">
|
<div v-show="activeSection === 'ext-rustdesk'" class="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user