Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 120 additions & 19 deletions src_assets/common/assets/web/configs/tabs/Advanced.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,57 @@ watch(isWGCSelected, (newValue) => {
checkSunshineMode()
}
})

// === 编解码器策略(HEVC + AV1 整合 UI) ===
// 底层仍写入 hevc_mode / av1_mode(保持 sunshine.conf 兼容),UI 层用一个策略 +
// 一个 HDR 复选框推算两者的值。
const showCodecAdvanced = ref(false)

// 把 config.value.hevc_mode / av1_mode 转成 number,便于比较(旧值可能是 string)
const hevcModeNum = computed(() => Number(config.value.hevc_mode ?? 0))
const av1ModeNum = computed(() => Number(config.value.av1_mode ?? 0))

const codecStrategy = computed({
get() {
const h = hevcModeNum.value
const a = av1ModeNum.value
if (h === 0 && a === 0) return 'auto'
if (h === 1 && a === 1) return 'h264_only'
// modern: 都通告(值为 2 或 3 都算 modern;HDR 由 enableHdr 单独决定)
if ((h === 2 || h === 3) && (a === 2 || a === 3)) return 'modern'
return 'custom'
},
set(v) {
if (v === 'auto') {
config.value.hevc_mode = 0
config.value.av1_mode = 0
} else if (v === 'h264_only') {
config.value.hevc_mode = 1
config.value.av1_mode = 1
} else if (v === 'modern') {
const hdr = enableHdr.value
config.value.hevc_mode = hdr ? 3 : 2
config.value.av1_mode = hdr ? 3 : 2
}
// 'custom' → 不修改值,由用户在展开区编辑
},
})

// 当任意 codec 选择 mode 3(含 10-bit/HDR),即视为开启 HDR 通告
const enableHdr = computed({
get() {
return hevcModeNum.value === 3 || av1ModeNum.value === 3
},
set(v) {
// 仅在"现代编码器"策略下生效
if (codecStrategy.value !== 'modern') return
config.value.hevc_mode = v ? 3 : 2
config.value.av1_mode = v ? 3 : 2
},
})

// HDR 复选框是否可用(只有 modern 策略下才有意义)
const hdrToggleDisabled = computed(() => codecStrategy.value !== 'modern')
</script>

<template>
Expand All @@ -129,28 +180,78 @@ watch(isWGCSelected, (newValue) => {
<div class="form-text">{{ $t('config.min_threads_desc') }}</div>
</div>

<!-- HEVC Support -->
<!-- Codec Strategy (整合 HEVC + AV1) -->
<div class="mb-3">
<label for="hevc_mode" class="form-label">{{ $t('config.hevc_mode') }}</label>
<select id="hevc_mode" class="form-select" v-model="config.hevc_mode">
<option value="0">{{ $t('config.hevc_mode_0') }}</option>
<option value="1">{{ $t('config.hevc_mode_1') }}</option>
<option value="2">{{ $t('config.hevc_mode_2') }}</option>
<option value="3">{{ $t('config.hevc_mode_3') }}</option>
<label for="codec_strategy" class="form-label">{{ $t('config.codec_strategy') }}</label>
<select id="codec_strategy" class="form-select" v-model="codecStrategy">
<option value="auto">{{ $t('config.codec_strategy_auto') }}</option>
<option value="modern">{{ $t('config.codec_strategy_modern') }}</option>
<option value="h264_only">{{ $t('config.codec_strategy_h264') }}</option>
<option value="custom" disabled v-if="codecStrategy !== 'custom'">
{{ $t('config.codec_strategy_custom_locked') }}
</option>
<option value="custom" v-else>{{ $t('config.codec_strategy_custom') }}</option>
</select>
<div class="form-text">{{ $t('config.hevc_mode_desc') }}</div>
</div>

<!-- AV1 Support -->
<div class="mb-3">
<label for="av1_mode" class="form-label">{{ $t('config.av1_mode') }}</label>
<select id="av1_mode" class="form-select" v-model="config.av1_mode">
<option value="0">{{ $t('config.av1_mode_0') }}</option>
<option value="1">{{ $t('config.av1_mode_1') }}</option>
<option value="2">{{ $t('config.av1_mode_2') }}</option>
<option value="3">{{ $t('config.av1_mode_3') }}</option>
</select>
<div class="form-text">{{ $t('config.av1_mode_desc') }}</div>
<div class="form-check mt-2">
<input
class="form-check-input"
type="checkbox"
id="codec_enable_hdr"
v-model="enableHdr"
:disabled="hdrToggleDisabled"
/>
<label class="form-check-label" for="codec_enable_hdr">
{{ $t('config.codec_enable_hdr') }}
</label>
<div class="form-text" v-if="hdrToggleDisabled">
{{ $t('config.codec_enable_hdr_disabled_hint') }}
</div>
</div>

<div class="form-text">{{ $t('config.codec_strategy_desc') }}</div>

<!-- 偏离推荐值时给出温和提示 -->
<div class="alert alert-warning py-2 mt-2 mb-0" v-if="codecStrategy !== 'auto'">
<small>{{ $t('config.codec_strategy_non_default_warning') }}</small>
</div>

<!-- 高级(专家模式):原 HEVC / AV1 dropdown -->
<div class="mt-2">
<button
type="button"
class="btn btn-sm btn-outline-secondary"
:aria-expanded="showCodecAdvanced"
aria-controls="codec-advanced-panel"
@click="showCodecAdvanced = !showCodecAdvanced"
>
{{ showCodecAdvanced ? $t('config.codec_advanced_hide') : $t('config.codec_advanced_show') }}
</button>
</div>

<div v-if="showCodecAdvanced" id="codec-advanced-panel" class="mt-3 ps-3 border-start">
<div class="mb-3">
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<label for="hevc_mode" class="form-label">{{ $t('config.hevc_mode') }}</label>
<select id="hevc_mode" class="form-select" v-model="config.hevc_mode">
<option value="0">{{ $t('config.hevc_mode_0') }}</option>
<option value="1">{{ $t('config.hevc_mode_1') }}</option>
<option value="2">{{ $t('config.hevc_mode_2') }}</option>
<option value="3">{{ $t('config.hevc_mode_3') }}</option>
</select>
<div class="form-text">{{ $t('config.hevc_mode_desc') }}</div>
</div>

<div class="mb-0">
<label for="av1_mode" class="form-label">{{ $t('config.av1_mode') }}</label>
<select id="av1_mode" class="form-select" v-model="config.av1_mode">
<option value="0">{{ $t('config.av1_mode_0') }}</option>
<option value="1">{{ $t('config.av1_mode_1') }}</option>
<option value="2">{{ $t('config.av1_mode_2') }}</option>
<option value="3">{{ $t('config.av1_mode_3') }}</option>
</select>
<div class="form-text">{{ $t('config.av1_mode_desc') }}</div>
</div>
</div>
</div>

<!-- Capture -->
Expand Down
162 changes: 66 additions & 96 deletions src_assets/common/assets/web/configs/tabs/Inputs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,119 +12,86 @@ const props = defineProps([

const config = ref(props.config)

// Tauri 环境下的 vmouse 驱动管理
const hasVmouseDriver = ref(false)
const vmouseStatus = ref({ installed: false, running: false, status_text: '' })
const vmouseLoading = ref(false)
const vmouseOperating = ref(false)

// Tauri 环境下的 ViGEmBus 虚拟手柄驱动管理
const hasVigemDriver = ref(false)
const vigemStatus = ref({ installed: false, running: false, version: '', version_ok: false, status_text: '' })
const vigemLoading = ref(false)
const vigemOperating = ref(false)

onMounted(async () => {
if (!window.isTauri) return
hasVmouseDriver.value = !!window.vmouseDriver
hasVigemDriver.value = !!window.vigemDriver
if (hasVmouseDriver.value) await refreshVmouseStatus()
if (hasVigemDriver.value) await refreshVigemStatus()
})

async function refreshVmouseStatus() {
vmouseLoading.value = true
try {
vmouseStatus.value = await window.vmouseDriver.getStatus()
} catch { /* ignore */ }
vmouseLoading.value = false
}

async function installVmouse() {
if (!confirm(t('config.vmouse_confirm_install'))) return
vmouseOperating.value = true
try {
await window.vmouseDriver.install()
setTimeout(() => refreshVmouseStatus(), 2000)
} catch (e) {
alert(String(e))
/**
* 创建一个驱动管理器,统一封装状态/加载/操作逻辑,避免 vmouse / vigem 的重复代码。
* @param {string} driverKey - window 上挂载的驱动对象属性名(如 'vmouseDriver')
* @param {object} initialStatus - 初始状态对象
*/
function createDriverManager(driverKey, initialStatus) {
const available = ref(false)
const status = ref({ ...initialStatus })
const loading = ref(false)
const operating = ref(false)

const driver = () => window[driverKey]

async function refresh() {
loading.value = true
try {
status.value = await driver().getStatus()
} catch { /* ignore */ }
loading.value = false
}
vmouseOperating.value = false
}

async function uninstallVmouse() {
if (!confirm(t('config.vmouse_confirm_uninstall'))) return
vmouseOperating.value = true
try {
await window.vmouseDriver.uninstall()
setTimeout(() => refreshVmouseStatus(), 2000)
} catch (e) {
alert(String(e))
async function runOp(confirmKey, op) {
if (!confirm(t(confirmKey))) return
operating.value = true
try {
await op(driver())
setTimeout(refresh, 2000)
} catch (e) {
alert(String(e))
}
operating.value = false
}
vmouseOperating.value = false
}

async function refreshVigemStatus() {
vigemLoading.value = true
try {
vigemStatus.value = await window.vigemDriver.getStatus()
} catch { /* ignore */ }
vigemLoading.value = false
}

async function installVigem(force = false) {
const key = force ? 'config.vigem_confirm_reinstall' : 'config.vigem_confirm_install'
if (!confirm(t(key))) return
vigemOperating.value = true
try {
await window.vigemDriver.install(force)
setTimeout(() => refreshVigemStatus(), 2000)
} catch (e) {
alert(String(e))
}
vigemOperating.value = false
}
const dotClass = computed(() => {
if (status.value.running) return 'dot-active'
if (status.value.installed) return 'dot-warning'
return 'dot-inactive'
})

async function uninstallVigem() {
if (!confirm(t('config.vigem_confirm_uninstall'))) return
vigemOperating.value = true
try {
await window.vigemDriver.uninstall()
setTimeout(() => refreshVigemStatus(), 2000)
} catch (e) {
alert(String(e))
}
vigemOperating.value = false
return { available, status, loading, operating, refresh, runOp, dotClass }
}

const vmouseDotClass = computed(() => {
if (vmouseStatus.value.running) return 'dot-active'
if (vmouseStatus.value.installed) return 'dot-warning'
return 'dot-inactive'
})
// vmouse 驱动管理
const vmouse = createDriverManager('vmouseDriver', { installed: false, running: false, status_text: '' })
const installVmouse = () => vmouse.runOp('config.vmouse_confirm_install', d => d.install())
const uninstallVmouse = () => vmouse.runOp('config.vmouse_confirm_uninstall', d => d.uninstall())

const vmouseStatusLabel = computed(() => {
if (vmouseStatus.value.running) return t('config.vmouse_status_running')
if (vmouseStatus.value.installed) return t('config.vmouse_status_installed')
if (vmouse.status.value.running) return t('config.vmouse_status_running')
if (vmouse.status.value.installed) return t('config.vmouse_status_installed')
return t('config.vmouse_status_not_installed')
})

const vigemDotClass = computed(() => {
if (vigemStatus.value.running) return 'dot-active'
if (vigemStatus.value.installed) return 'dot-warning'
return 'dot-inactive'
})
// ViGEmBus 虚拟手柄驱动管理
const vigem = createDriverManager('vigemDriver',
{ installed: false, running: false, version: '', version_ok: false, status_text: '' })
const installVigem = (force = false) => vigem.runOp(
force ? 'config.vigem_confirm_reinstall' : 'config.vigem_confirm_install',
d => d.install(force)
)
const uninstallVigem = () => vigem.runOp('config.vigem_confirm_uninstall', d => d.uninstall())

const vigemStatusLabel = computed(() => {
if (!vigemStatus.value.installed) return t('config.vigem_status_not_installed')
if (!vigemStatus.value.version_ok) return t('config.vigem_status_outdated')
if (vigemStatus.value.running) {
return vigemStatus.value.version
? `${t('config.vigem_status_running')} (${vigemStatus.value.version})`
: t('config.vigem_status_running')
const s = vigem.status.value
if (!s.installed) return t('config.vigem_status_not_installed')
if (!s.version_ok) return t('config.vigem_status_outdated')
if (s.running) {
const running = t('config.vigem_status_running')
return s.version ? `${running} (${s.version})` : running
}
return t('config.vigem_status_installed')
})

onMounted(async () => {
if (!window.isTauri) return
vmouse.available.value = !!window.vmouseDriver
vigem.available.value = !!window.vigemDriver
if (vmouse.available.value) await vmouse.refresh()
if (vigem.available.value) await vigem.refresh()
})
</script>
Comment on lines +57 to 95
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

重构后缺少模板绑定别名,导致页面绑定失效。

createDriverManager 重构后,模板仍在使用旧标识符(如 Line 134 的 hasVigemDriver、Line 143 的 vigemDotClass、Line 367 的 hasVmouseDriver 等),但 script 未定义这些变量,会导致模板解析/运行时报错。

建议补充兼容别名(最小改动)
 // vmouse 驱动管理
 const vmouse = createDriverManager('vmouseDriver', { installed: false, running: false, status_text: '' })
 const installVmouse = () => vmouse.runOp('config.vmouse_confirm_install', d => d.install())
 const uninstallVmouse = () => vmouse.runOp('config.vmouse_confirm_uninstall', d => d.uninstall())
+const hasVmouseDriver = computed(() => vmouse.available.value)
+const vmouseStatus = computed(() => vmouse.status.value)
+const vmouseLoading = computed(() => vmouse.loading.value)
+const vmouseOperating = computed(() => vmouse.operating.value)
+const vmouseDotClass = computed(() => vmouse.dotClass.value)
+const refreshVmouseStatus = () => vmouse.refresh()

 // ViGEmBus 虚拟手柄驱动管理
 const vigem = createDriverManager('vigemDriver',
   { installed: false, running: false, version: '', version_ok: false, status_text: '' })
 const installVigem = (force = false) => vigem.runOp(
   force ? 'config.vigem_confirm_reinstall' : 'config.vigem_confirm_install',
   d => d.install(force)
 )
 const uninstallVigem = () => vigem.runOp('config.vigem_confirm_uninstall', d => d.uninstall())
+const hasVigemDriver = computed(() => vigem.available.value)
+const vigemStatus = computed(() => vigem.status.value)
+const vigemLoading = computed(() => vigem.loading.value)
+const vigemOperating = computed(() => vigem.operating.value)
+const vigemDotClass = computed(() => vigem.dotClass.value)
+const refreshVigemStatus = () => vigem.refresh()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src_assets/common/assets/web/configs/tabs/Inputs.vue` around lines 57 - 95,
The template still references legacy aliases (e.g., hasVigemDriver,
vigemDotClass, hasVmouseDriver) that were removed during the createDriverManager
refactor; add small compatibility computed aliases that map the old names to the
new driver manager state (use the existing vmouse and vigem objects and their
computed labels): define hasVigemDriver => vigem.available.value,
hasVmouseDriver => vmouse.available.value, and create vigemDotClass and
vmouseDotClass computed values that derive the same CSS/class semantics as the
template expects by consulting vigem.status.value (and vigemStatusLabel) and
vmouse.status.value (and vmouseStatusLabel); keep these minimal shim computed
bindings so the template can continue to use the original identifiers.


<template>
Expand Down Expand Up @@ -565,6 +532,9 @@ const vigemStatusLabel = computed(() => {

/* 面板操作区 */
.vmouse-panel-body {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.6rem 0.85rem;
}

Expand Down
14 changes: 12 additions & 2 deletions src_assets/common/assets/web/public/assets/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,18 @@
"av1_mode_1": "Sunshine will not advertise support for AV1",
"av1_mode_2": "Sunshine will advertise support for AV1 Main 8-bit profile",
"av1_mode_3": "Sunshine will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles",
"av1_mode_desc": "Allows the client to request AV1 Main 8-bit or 10-bit video streams. AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.",
"back_button_timeout": "Home/Guide Button Emulation Timeout",
"av1_mode_desc": "Allows the client to request AV1 Main 8-bit or 10-bit video streams. AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.", "codec_strategy": "Video codec strategy",
"codec_strategy_auto": "Automatic (recommended)",
"codec_strategy_modern": "Prefer modern codecs (HEVC + AV1)",
"codec_strategy_h264": "H.264 only (maximum compatibility)",
"codec_strategy_custom": "Custom (configure below)",
"codec_strategy_custom_locked": "Custom (click 'Show advanced options' to edit)",
"codec_strategy_desc": "Controls which video codecs Sunshine advertises to Moonlight. The default 'Automatic' picks based on encoder capability and is right for ~99% of users. Switch to 'H.264 only' if your client shows a black/green screen or HDR colors look wrong.",
"codec_strategy_non_default_warning": "You are using a non-default strategy. Some clients may fail to connect or fall back to a lower quality. Restore 'Automatic (recommended)' if you encounter issues.",
"codec_enable_hdr": "Also advertise HDR / 10-bit profiles",
"codec_enable_hdr_disabled_hint": "HDR is only adjustable under 'Prefer modern codecs'. 'Automatic' decides this for you based on encoder capability.",
"codec_advanced_show": "Show advanced options (configure HEVC / AV1 separately)",
"codec_advanced_hide": "Hide advanced options", "back_button_timeout": "Home/Guide Button Emulation Timeout",
"back_button_timeout_desc": "If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. If set to a value < 0 (default), holding the Back/Select button will not emulate the Home/Guide button.",
"bind_address": "Bind address (test feature)",
"bind_address_desc": "Set the specific IP address Sunshine will bind to. If left blank, Sunshine will bind to all available addresses.",
Expand Down
14 changes: 12 additions & 2 deletions src_assets/common/assets/web/public/assets/locale/en_GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,18 @@
"av1_mode_1": "Sunshine will not advertise support for AV1",
"av1_mode_2": "Sunshine will advertise support for AV1 Main 8-bit profile",
"av1_mode_3": "Sunshine will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles",
"av1_mode_desc": "Allows the client to request AV1 Main 8-bit or 10-bit video streams. AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.",
"back_button_timeout": "Home/Guide Button Emulation Timeout",
"av1_mode_desc": "Allows the client to request AV1 Main 8-bit or 10-bit video streams. AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.", "codec_strategy": "Video codec strategy",
"codec_strategy_auto": "Automatic (recommended)",
"codec_strategy_modern": "Prefer modern codecs (HEVC + AV1)",
"codec_strategy_h264": "H.264 only (maximum compatibility)",
"codec_strategy_custom": "Custom (configure below)",
"codec_strategy_custom_locked": "Custom (click 'Show advanced options' to edit)",
"codec_strategy_desc": "Controls which video codecs Sunshine advertises to Moonlight. The default 'Automatic' picks based on encoder capability and is right for ~99% of users. Switch to 'H.264 only' if your client shows a black/green screen or HDR colors look wrong.",
"codec_strategy_non_default_warning": "You are using a non-default strategy. Some clients may fail to connect or fall back to a lower quality. Restore 'Automatic (recommended)' if you encounter issues.",
"codec_enable_hdr": "Also advertise HDR / 10-bit profiles",
"codec_enable_hdr_disabled_hint": "HDR is only adjustable under 'Prefer modern codecs'. 'Automatic' decides this for you based on encoder capability.",
"codec_advanced_show": "Show advanced options (configure HEVC / AV1 separately)",
"codec_advanced_hide": "Hide advanced options", "back_button_timeout": "Home/Guide Button Emulation Timeout",
"back_button_timeout_desc": "If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. If set to a value < 0 (default), holding the Back/Select button will not emulate the Home/Guide button.",
"bind_address": "Bind address (test feature)",
"bind_address_desc": "Set the specific IP address Sunshine will bind to. If left blank, Sunshine will bind to all available addresses.",
Expand Down
Loading
Loading