mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-29 22:56:45 +08:00
feat(web): 优化视频格式与虚拟键盘显示
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
|||||||
type BitratePreset,
|
type BitratePreset,
|
||||||
type StreamConstraintsResponse,
|
type StreamConstraintsResponse,
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
|
import { getVideoFormatState, isVideoFormatSelectable } from '@/lib/video-format-support'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -167,6 +168,12 @@ const isBrowserSupported = (codecId: string): boolean => {
|
|||||||
return browserSupportedCodecs.value.has(codecId)
|
return browserSupportedCodecs.value.has(codecId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFormatState = (formatName: string) =>
|
||||||
|
getVideoFormatState(formatName, props.videoMode, currentEncoderBackend.value)
|
||||||
|
|
||||||
|
const isFormatUnsupported = (formatName: string): boolean =>
|
||||||
|
getFormatState(formatName) === 'unsupported'
|
||||||
|
|
||||||
// Translate backend name for display
|
// Translate backend name for display
|
||||||
const translateBackendName = (backend: string | undefined): string => {
|
const translateBackendName = (backend: string | undefined): string => {
|
||||||
if (!backend) return ''
|
if (!backend) return ''
|
||||||
@@ -189,6 +196,10 @@ const hasHighFps = (format: { resolutions: { fps: number[] }[] }): boolean => {
|
|||||||
|
|
||||||
// Check if a format is recommended based on video mode
|
// Check if a format is recommended based on video mode
|
||||||
const isFormatRecommended = (formatName: string): boolean => {
|
const isFormatRecommended = (formatName: string): boolean => {
|
||||||
|
if (!isVideoFormatSelectable(formatName, props.videoMode, currentEncoderBackend.value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const formats = availableFormats.value
|
const formats = availableFormats.value
|
||||||
const upperFormat = formatName.toUpperCase()
|
const upperFormat = formatName.toUpperCase()
|
||||||
|
|
||||||
@@ -225,12 +236,7 @@ const isFormatRecommended = (formatName: string): boolean => {
|
|||||||
// Check if a format is not recommended for current video mode
|
// Check if a format is not recommended for current video mode
|
||||||
// In WebRTC mode, compressed formats (MJPEG/JPEG) are not recommended
|
// In WebRTC mode, compressed formats (MJPEG/JPEG) are not recommended
|
||||||
const isFormatNotRecommended = (formatName: string): boolean => {
|
const isFormatNotRecommended = (formatName: string): boolean => {
|
||||||
const upperFormat = formatName.toUpperCase()
|
return getFormatState(formatName) === 'not_recommended'
|
||||||
// WebRTC mode: MJPEG/JPEG are not recommended (require decoding before encoding)
|
|
||||||
if (props.videoMode !== 'mjpeg') {
|
|
||||||
return upperFormat === 'MJPEG' || upperFormat === 'JPEG'
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selected values (mode comes from props)
|
// Selected values (mode comes from props)
|
||||||
@@ -303,6 +309,14 @@ const availableFormats = computed(() => {
|
|||||||
return device?.formats || []
|
return device?.formats || []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const availableFormatOptions = computed(() => {
|
||||||
|
return availableFormats.value.map(format => ({
|
||||||
|
...format,
|
||||||
|
state: getFormatState(format.format),
|
||||||
|
disabled: isFormatUnsupported(format.format),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
const availableResolutions = computed(() => {
|
const availableResolutions = computed(() => {
|
||||||
const format = availableFormats.value.find(f => f.format === selectedFormat.value)
|
const format = availableFormats.value.find(f => f.format === selectedFormat.value)
|
||||||
return format?.resolutions || []
|
return format?.resolutions || []
|
||||||
@@ -317,8 +331,8 @@ const availableFps = computed(() => {
|
|||||||
|
|
||||||
// Get selected format description for display in trigger
|
// Get selected format description for display in trigger
|
||||||
const selectedFormatInfo = computed(() => {
|
const selectedFormatInfo = computed(() => {
|
||||||
const format = availableFormats.value.find(f => f.format === selectedFormat.value)
|
const format = availableFormatOptions.value.find(f => f.format === selectedFormat.value)
|
||||||
return format ? { description: format.description, format: format.format } : null
|
return format
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get selected codec info for display in trigger
|
// Get selected codec info for display in trigger
|
||||||
@@ -423,6 +437,37 @@ function handleVideoModeChange(mode: unknown) {
|
|||||||
emit('update:videoMode', mode as VideoMode)
|
emit('update:videoMode', mode as VideoMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findFirstSelectableFormat(
|
||||||
|
formats: VideoDevice['formats'],
|
||||||
|
): VideoDevice['formats'][number] | undefined {
|
||||||
|
return formats.find(format =>
|
||||||
|
isVideoFormatSelectable(format.format, props.videoMode, currentEncoderBackend.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFormatSelection() {
|
||||||
|
selectedFormat.value = ''
|
||||||
|
selectedResolution.value = ''
|
||||||
|
selectedFps.value = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFormatWithDefaults(format: string) {
|
||||||
|
if (isFormatUnsupported(format)) return
|
||||||
|
|
||||||
|
selectedFormat.value = format
|
||||||
|
|
||||||
|
const formatData = availableFormats.value.find(f => f.format === format)
|
||||||
|
const resolution = formatData?.resolutions[0]
|
||||||
|
if (!resolution) {
|
||||||
|
selectedResolution.value = ''
|
||||||
|
selectedFps.value = 30
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedResolution.value = `${resolution.width}x${resolution.height}`
|
||||||
|
selectedFps.value = resolution.fps[0] || 30
|
||||||
|
}
|
||||||
|
|
||||||
// Handle device change
|
// Handle device change
|
||||||
function handleDeviceChange(devicePath: unknown) {
|
function handleDeviceChange(devicePath: unknown) {
|
||||||
if (typeof devicePath !== 'string') return
|
if (typeof devicePath !== 'string') return
|
||||||
@@ -431,31 +476,22 @@ function handleDeviceChange(devicePath: unknown) {
|
|||||||
|
|
||||||
// Auto-select first format
|
// Auto-select first format
|
||||||
const device = devices.value.find(d => d.path === devicePath)
|
const device = devices.value.find(d => d.path === devicePath)
|
||||||
if (device?.formats[0]) {
|
const format = device ? findFirstSelectableFormat(device.formats) : undefined
|
||||||
selectedFormat.value = device.formats[0].format
|
if (!format) {
|
||||||
|
clearFormatSelection()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-select first resolution
|
selectFormatWithDefaults(format.format)
|
||||||
const resolution = device.formats[0].resolutions[0]
|
|
||||||
if (resolution) {
|
|
||||||
selectedResolution.value = `${resolution.width}x${resolution.height}`
|
|
||||||
selectedFps.value = resolution.fps[0] || 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle format change
|
// Handle format change
|
||||||
function handleFormatChange(format: unknown) {
|
function handleFormatChange(format: unknown) {
|
||||||
if (typeof format !== 'string') return
|
if (typeof format !== 'string') return
|
||||||
selectedFormat.value = format
|
if (isFormatUnsupported(format)) return
|
||||||
isDirty.value = true
|
|
||||||
|
|
||||||
// Auto-select first resolution for this format
|
selectFormatWithDefaults(format)
|
||||||
const formatData = availableFormats.value.find(f => f.format === format)
|
isDirty.value = true
|
||||||
if (formatData?.resolutions[0]) {
|
|
||||||
const resolution = formatData.resolutions[0]
|
|
||||||
selectedResolution.value = `${resolution.width}x${resolution.height}`
|
|
||||||
selectedFps.value = resolution.fps[0] || 30
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle resolution change
|
// Handle resolution change
|
||||||
@@ -567,6 +603,29 @@ watch(currentConfig, () => {
|
|||||||
if (props.open && isDirty.value) return
|
if (props.open && isDirty.value) return
|
||||||
syncFromCurrentIfChanged()
|
syncFromCurrentIfChanged()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[availableFormatOptions, () => props.videoMode, currentEncoderBackend],
|
||||||
|
() => {
|
||||||
|
if (!selectedDevice.value) return
|
||||||
|
|
||||||
|
const currentFormat = availableFormatOptions.value.find(
|
||||||
|
format => format.format === selectedFormat.value,
|
||||||
|
)
|
||||||
|
if (currentFormat && !currentFormat.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = availableFormatOptions.value.find(format => !format.disabled)
|
||||||
|
if (!fallback) {
|
||||||
|
clearFormatSelection()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFormatWithDefaults(fallback.format)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -770,6 +829,12 @@ watch(currentConfig, () => {
|
|||||||
<SelectTrigger class="h-8 text-xs">
|
<SelectTrigger class="h-8 text-xs">
|
||||||
<div v-if="selectedFormatInfo" class="flex items-center gap-1.5 truncate">
|
<div v-if="selectedFormatInfo" class="flex items-center gap-1.5 truncate">
|
||||||
<span class="truncate">{{ selectedFormatInfo.description }}</span>
|
<span class="truncate">{{ selectedFormatInfo.description }}</span>
|
||||||
|
<span
|
||||||
|
v-if="selectedFormatInfo.state === 'unsupported'"
|
||||||
|
class="shrink-0 text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ t('common.notSupportedYet') }}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="isFormatRecommended(selectedFormatInfo.format)"
|
v-if="isFormatRecommended(selectedFormatInfo.format)"
|
||||||
class="text-[10px] px-1 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 shrink-0"
|
class="text-[10px] px-1 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 shrink-0"
|
||||||
@@ -787,13 +852,20 @@ watch(currentConfig, () => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
v-for="format in availableFormats"
|
v-for="format in availableFormatOptions"
|
||||||
:key="format.format"
|
:key="format.format"
|
||||||
:value="format.format"
|
:value="format.format"
|
||||||
class="text-xs"
|
:disabled="format.disabled"
|
||||||
|
:class="['text-xs', { 'opacity-50': format.disabled }]"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>{{ format.description }}</span>
|
<span>{{ format.description }}</span>
|
||||||
|
<span
|
||||||
|
v-if="format.state === 'unsupported'"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ t('common.notSupportedYet') }}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="isFormatRecommended(format.format)"
|
v-if="isFormatRecommended(format.format)"
|
||||||
class="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"
|
class="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const props = defineProps<{
|
|||||||
attached?: boolean
|
attached?: boolean
|
||||||
capsLock?: boolean
|
capsLock?: boolean
|
||||||
pressedKeys?: CanonicalKey[]
|
pressedKeys?: CanonicalKey[]
|
||||||
|
consumerEnabled?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -718,7 +719,7 @@ onUnmounted(() => {
|
|||||||
<!-- Keyboard body -->
|
<!-- Keyboard body -->
|
||||||
<div class="vkb-body">
|
<div class="vkb-body">
|
||||||
<!-- Media keys row -->
|
<!-- Media keys row -->
|
||||||
<div class="vkb-media-row">
|
<div v-if="props.consumerEnabled !== false" class="vkb-media-row">
|
||||||
<button
|
<button
|
||||||
v-for="key in mediaKeys"
|
v-for="key in mediaKeys"
|
||||||
:key="key"
|
:key="key"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
menu: 'Menu',
|
menu: 'Menu',
|
||||||
optional: 'Optional',
|
optional: 'Optional',
|
||||||
recommended: 'Recommended',
|
recommended: 'Recommended',
|
||||||
|
notSupportedYet: ' (Not Yet Supported)',
|
||||||
create: 'Create',
|
create: 'Create',
|
||||||
creating: 'Creating...',
|
creating: 'Creating...',
|
||||||
deleting: 'Deleting...',
|
deleting: 'Deleting...',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
menu: '菜单',
|
menu: '菜单',
|
||||||
optional: '可选',
|
optional: '可选',
|
||||||
recommended: '推荐',
|
recommended: '推荐',
|
||||||
|
notSupportedYet: '(尚未支持)',
|
||||||
create: '创建',
|
create: '创建',
|
||||||
creating: '创建中...',
|
creating: '创建中...',
|
||||||
deleting: '删除中...',
|
deleting: '删除中...',
|
||||||
|
|||||||
97
web/src/lib/video-format-support.ts
Normal file
97
web/src/lib/video-format-support.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
export type VideoFormatSupportContext = 'config' | 'mjpeg' | 'h264' | 'h265' | 'vp8' | 'vp9'
|
||||||
|
|
||||||
|
export type VideoFormatState = 'supported' | 'not_recommended' | 'unsupported'
|
||||||
|
|
||||||
|
const MJPEG_MODE_SUPPORTED_FORMATS = new Set([
|
||||||
|
'MJPEG',
|
||||||
|
'JPEG',
|
||||||
|
'YUYV',
|
||||||
|
'YVYU',
|
||||||
|
'NV12',
|
||||||
|
'RGB24',
|
||||||
|
'BGR24',
|
||||||
|
])
|
||||||
|
|
||||||
|
const CONFIG_SUPPORTED_FORMATS = new Set([
|
||||||
|
'MJPEG',
|
||||||
|
'JPEG',
|
||||||
|
'YUYV',
|
||||||
|
'YVYU',
|
||||||
|
'NV12',
|
||||||
|
'NV21',
|
||||||
|
'NV16',
|
||||||
|
'YUV420',
|
||||||
|
'RGB24',
|
||||||
|
'BGR24',
|
||||||
|
])
|
||||||
|
|
||||||
|
const WEBRTC_SUPPORTED_FORMATS = new Set([
|
||||||
|
'MJPEG',
|
||||||
|
'JPEG',
|
||||||
|
'YUYV',
|
||||||
|
'NV12',
|
||||||
|
'NV21',
|
||||||
|
'NV16',
|
||||||
|
'YUV420',
|
||||||
|
'RGB24',
|
||||||
|
'BGR24',
|
||||||
|
])
|
||||||
|
|
||||||
|
function normalizeFormat(formatName: string): string {
|
||||||
|
return formatName.trim().toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCompressedFormat(formatName: string): boolean {
|
||||||
|
return formatName === 'MJPEG' || formatName === 'JPEG'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRkmppBackend(backendId?: string): boolean {
|
||||||
|
return backendId?.toLowerCase() === 'rkmpp'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVideoFormatState(
|
||||||
|
formatName: string,
|
||||||
|
context: VideoFormatSupportContext,
|
||||||
|
encoderBackend = 'auto',
|
||||||
|
): VideoFormatState {
|
||||||
|
const normalizedFormat = normalizeFormat(formatName)
|
||||||
|
|
||||||
|
if (context === 'mjpeg') {
|
||||||
|
return MJPEG_MODE_SUPPORTED_FORMATS.has(normalizedFormat) ? 'supported' : 'unsupported'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context === 'config') {
|
||||||
|
if (CONFIG_SUPPORTED_FORMATS.has(normalizedFormat)) {
|
||||||
|
return 'supported'
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalizedFormat === 'NV24'
|
||||||
|
&& isRkmppBackend(encoderBackend)
|
||||||
|
) {
|
||||||
|
return 'supported'
|
||||||
|
}
|
||||||
|
return 'unsupported'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WEBRTC_SUPPORTED_FORMATS.has(normalizedFormat)) {
|
||||||
|
return isCompressedFormat(normalizedFormat) ? 'not_recommended' : 'supported'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedFormat === 'NV24'
|
||||||
|
&& isRkmppBackend(encoderBackend)
|
||||||
|
&& (context === 'h264' || context === 'h265')
|
||||||
|
) {
|
||||||
|
return 'supported'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unsupported'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVideoFormatSelectable(
|
||||||
|
formatName: string,
|
||||||
|
context: VideoFormatSupportContext,
|
||||||
|
encoderBackend = 'auto',
|
||||||
|
): boolean {
|
||||||
|
return getVideoFormatState(formatName, context, encoderBackend) !== 'unsupported'
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import { useWebRTC } from '@/composables/useWebRTC'
|
|||||||
import { useVideoSession } from '@/composables/useVideoSession'
|
import { useVideoSession } from '@/composables/useVideoSession'
|
||||||
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
|
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
|
||||||
import { streamApi, hidApi, atxApi, atxConfigApi, authApi } from '@/api'
|
import { streamApi, hidApi, atxApi, atxConfigApi, authApi } from '@/api'
|
||||||
import { CanonicalKey } from '@/types/generated'
|
import { CanonicalKey, HidBackend } from '@/types/generated'
|
||||||
import type { HidKeyboardEvent, HidMouseEvent } from '@/types/hid'
|
import type { HidKeyboardEvent, HidMouseEvent } from '@/types/hid'
|
||||||
import { keyboardEventToCanonicalKey, updateModifierMaskForKey } from '@/lib/keyboardMappings'
|
import { keyboardEventToCanonicalKey, updateModifierMaskForKey } from '@/lib/keyboardMappings'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
@@ -156,6 +156,12 @@ function syncMouseModeFromConfig() {
|
|||||||
const virtualKeyboardVisible = ref(false)
|
const virtualKeyboardVisible = ref(false)
|
||||||
const virtualKeyboardAttached = ref(true)
|
const virtualKeyboardAttached = ref(true)
|
||||||
const statsSheetOpen = ref(false)
|
const statsSheetOpen = ref(false)
|
||||||
|
const virtualKeyboardConsumerEnabled = computed(() => {
|
||||||
|
const hid = configStore.hid
|
||||||
|
if (!hid) return true
|
||||||
|
if (hid.backend !== HidBackend.Otg) return true
|
||||||
|
return hid.otg_functions?.consumer !== false
|
||||||
|
})
|
||||||
|
|
||||||
// Change password dialog state
|
// Change password dialog state
|
||||||
const changePasswordDialogOpen = ref(false)
|
const changePasswordDialogOpen = ref(false)
|
||||||
@@ -2528,6 +2534,7 @@ onUnmounted(() => {
|
|||||||
v-model:attached="virtualKeyboardAttached"
|
v-model:attached="virtualKeyboardAttached"
|
||||||
:caps-lock="keyboardLed.capsLock"
|
:caps-lock="keyboardLed.capsLock"
|
||||||
:pressed-keys="pressedKeys"
|
:pressed-keys="pressedKeys"
|
||||||
|
:consumer-enabled="virtualKeyboardConsumerEnabled"
|
||||||
@key-down="handleVirtualKeyDown"
|
@key-down="handleVirtualKeyDown"
|
||||||
@key-up="handleVirtualKeyUp"
|
@key-up="handleVirtualKeyUp"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import type {
|
|||||||
} from '@/types/generated'
|
} from '@/types/generated'
|
||||||
import { setLanguage } from '@/i18n'
|
import { setLanguage } from '@/i18n'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
|
import { getVideoFormatState } from '@/lib/video-format-support'
|
||||||
import AppLayout from '@/components/AppLayout.vue'
|
import AppLayout from '@/components/AppLayout.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
@@ -780,6 +781,21 @@ const availableFormats = computed(() => {
|
|||||||
return selectedDevice.value.formats
|
return selectedDevice.value.formats
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const availableFormatOptions = computed(() => {
|
||||||
|
return availableFormats.value.map(format => {
|
||||||
|
const state = getVideoFormatState(format.format, 'config', config.value.encoder_backend)
|
||||||
|
return {
|
||||||
|
...format,
|
||||||
|
state,
|
||||||
|
disabled: state === 'unsupported',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectableFormats = computed(() => {
|
||||||
|
return availableFormatOptions.value.filter(format => !format.disabled)
|
||||||
|
})
|
||||||
|
|
||||||
const selectedFormat = computed(() => {
|
const selectedFormat = computed(() => {
|
||||||
if (!selectedDevice.value || !config.value.video_format) return null
|
if (!selectedDevice.value || !config.value.video_format) return null
|
||||||
return selectedDevice.value.formats.find(f => f.format === config.value.video_format)
|
return selectedDevice.value.formats.find(f => f.format === config.value.video_format)
|
||||||
@@ -810,17 +826,22 @@ const availableFps = computed(() => {
|
|||||||
return currentRes ? currentRes.fps : []
|
return currentRes ? currentRes.fps : []
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for device change to set default format
|
// Keep the selected format aligned with currently selectable formats.
|
||||||
watch(() => config.value.video_device, () => {
|
watch(
|
||||||
if (availableFormats.value.length > 0) {
|
selectableFormats,
|
||||||
const isValid = availableFormats.value.some(f => f.format === config.value.video_format)
|
() => {
|
||||||
if (!isValid) {
|
if (selectableFormats.value.length === 0) {
|
||||||
config.value.video_format = availableFormats.value[0]?.format || ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.value.video_format = ''
|
config.value.video_format = ''
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
const isValid = selectableFormats.value.some(f => f.format === config.value.video_format)
|
||||||
|
if (!isValid) {
|
||||||
|
config.value.video_format = selectableFormats.value[0]?.format || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
// Watch for format change to set default resolution
|
// Watch for format change to set default resolution
|
||||||
watch(() => config.value.video_format, () => {
|
watch(() => config.value.video_format, () => {
|
||||||
@@ -2091,7 +2112,14 @@ watch(() => route.query.tab, (tab) => {
|
|||||||
<Label for="video-format">{{ t('settings.videoFormat') }}</Label>
|
<Label for="video-format">{{ t('settings.videoFormat') }}</Label>
|
||||||
<select id="video-format" v-model="config.video_format" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!config.video_device">
|
<select id="video-format" v-model="config.video_format" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="!config.video_device">
|
||||||
<option value="">{{ t('settings.selectFormat') }}</option>
|
<option value="">{{ t('settings.selectFormat') }}</option>
|
||||||
<option v-for="fmt in availableFormats" :key="fmt.format" :value="fmt.format">{{ fmt.format }} - {{ fmt.description }}</option>
|
<option
|
||||||
|
v-for="fmt in availableFormatOptions"
|
||||||
|
:key="fmt.format"
|
||||||
|
:value="fmt.format"
|
||||||
|
:disabled="fmt.disabled"
|
||||||
|
>
|
||||||
|
{{ fmt.format }} - {{ fmt.description }}{{ fmt.disabled ? t('common.notSupportedYet') : '' }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user