* Isolated display - web and backend Isolated display - web and backend * Moved settings outside of the advanced display * Update virtual_display.cpp with isolated setting for better maintenance Update virtual_display.cpp with isolated setting for better maintenance. The isolated routine becomes an additional routine instead of a replacement for the original one. * Changed source formatting, removed English in non-English languages, removed test code Changed source formatting, removed English in non-English languages, removed test code * change blank/unused lines in config.cpp change blank/unused lines in config.cpp * Change blank lines in DisplayDeviceOptions.vue Change blank lines in DisplayDeviceOptions.vue * Change line spacing in config.h Change line spacing in config.h * Changed line spacing in config.cpp Changed line spacing in config.cpp * Changed lines/line spacing in virtual_display.cpp Changed lines/line spacing in virtual_display.cpp * Fix spaceing on virtual_display.cpp Fix spaceing on virtual_display.cpp * Added check to not do anything if the virtual display is not active. Added check to not do anything to the order of the displays if the isolated checkbox is set and if the virtual display is not active. * Minor formatting fix w/ add Chinese locale * Expose `matchDisplay` though not used for now --------- Co-authored-by: Yukino Song <nutosservice@gmail.com>
511 lines
15 KiB
HTML
511 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-bs-theme="auto">
|
|
|
|
<head>
|
|
<%- header %>
|
|
<style>
|
|
.config-page {
|
|
padding: 1em;
|
|
border: 1px solid #dee2e6;
|
|
border-top: none;
|
|
}
|
|
|
|
.pre-wrap {
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.buttons {
|
|
padding: 1em 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body id="app" v-cloak>
|
|
<Navbar></Navbar>
|
|
<div class="container">
|
|
<h1 class="my-4">{{ $t('config.configuration') }}</h1>
|
|
<div class="form" v-if="config">
|
|
<!-- Header -->
|
|
<ul class="nav nav-tabs">
|
|
<li class="nav-item" v-for="tab in tabs" :key="tab.id">
|
|
<a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
|
|
@click="currentTab = tab.id">{{tab.name}}</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- General Tab -->
|
|
<general
|
|
v-if="currentTab === 'general'"
|
|
:config="config"
|
|
:global-prep-cmd="global_prep_cmd"
|
|
:server-cmd="server_cmd"
|
|
:platform="platform">
|
|
</general>
|
|
|
|
<!-- Input Tab -->
|
|
<inputs
|
|
v-if="currentTab === 'input'"
|
|
:config="config"
|
|
:platform="platform">
|
|
</inputs>
|
|
|
|
<!-- Audio/Video Tab -->
|
|
<audio-video
|
|
v-if="currentTab === 'av'"
|
|
:config="config"
|
|
:platform="platform"
|
|
:vdisplay="vdisplayStatus"
|
|
>
|
|
</audio-video>
|
|
|
|
<!-- Network Tab -->
|
|
<network
|
|
v-if="currentTab === 'network'"
|
|
:config="config"
|
|
:platform="platform">
|
|
</network>
|
|
|
|
<!-- Files Tab -->
|
|
<files
|
|
v-if="currentTab === 'files'"
|
|
:config="config"
|
|
:platform="platform">
|
|
</files>
|
|
|
|
<!-- Advanced Tab -->
|
|
<advanced
|
|
v-if="currentTab === 'advanced'"
|
|
:config="config"
|
|
:platform="platform">
|
|
</advanced>
|
|
|
|
<container-encoders
|
|
:current-tab="currentTab"
|
|
:config="config"
|
|
:platform="platform">
|
|
</container-encoders>
|
|
</div>
|
|
|
|
<!-- Save and Apply buttons -->
|
|
<div class="alert alert-success my-4" v-if="saved && !restarted">
|
|
<b>{{ $t('_common.success') }}</b> {{ $t('config.apply_note') }}
|
|
</div>
|
|
<div class="alert alert-success my-4" v-if="restarted">
|
|
<b>{{ $t('_common.success') }}</b> {{ $t('config.restart_note') }}
|
|
</div>
|
|
<div class="mb-3 buttons">
|
|
<button class="btn btn-primary" @click="save">{{ $t('_common.save') }}</button>
|
|
<button class="btn btn-success mx-2" @click="apply" v-if="saved && !restarted">{{ $t('_common.apply') }}</button>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
|
|
|
|
<script type="module">
|
|
import { computed, createApp } from 'vue'
|
|
import { initApp } from './init'
|
|
import Navbar from './Navbar.vue'
|
|
import General from './configs/tabs/General.vue'
|
|
import Inputs from './configs/tabs/Inputs.vue'
|
|
import Network from './configs/tabs/Network.vue'
|
|
import Files from './configs/tabs/Files.vue'
|
|
import Advanced from './configs/tabs/Advanced.vue'
|
|
import AudioVideo from './configs/tabs/AudioVideo.vue'
|
|
import ContainerEncoders from './configs/tabs/ContainerEncoders.vue'
|
|
import {$tp, usePlatformI18n} from './platform-i18n'
|
|
|
|
let fallbackDisplayModeCache = "";
|
|
|
|
const app = createApp({
|
|
components: {
|
|
Navbar,
|
|
General,
|
|
Inputs,
|
|
Network,
|
|
Files,
|
|
Advanced,
|
|
// They will be accessible via audio-video, container-encoders only.
|
|
AudioVideo,
|
|
ContainerEncoders,
|
|
},
|
|
data() {
|
|
return {
|
|
platform: "",
|
|
saved: false,
|
|
restarted: false,
|
|
config: null,
|
|
currentTab: "general",
|
|
vdisplayStatus: "1",
|
|
global_prep_cmd: [],
|
|
server_cmd: [],
|
|
tabs: [ // TODO: Move the options to each Component instead, encapsulate.
|
|
{
|
|
id: "general",
|
|
name: "General",
|
|
options: {
|
|
"locale": "en",
|
|
"sunshine_name": "",
|
|
"min_log_level": 2,
|
|
"global_prep_cmd": [],
|
|
"server_cmd": [],
|
|
"notify_pre_releases": "disabled",
|
|
"hide_tray_controls": "disabled",
|
|
"enable_pairing": "enabled",
|
|
"enable_discovery": "enabled",
|
|
},
|
|
},
|
|
{
|
|
id: "input",
|
|
name: "Input",
|
|
options: {
|
|
"controller": "enabled",
|
|
"gamepad": "auto",
|
|
"ds4_back_as_touchpad_click": "enabled",
|
|
"motion_as_ds4": "enabled",
|
|
"touchpad_as_ds4": "enabled",
|
|
"back_button_timeout": -1,
|
|
"keyboard": "enabled",
|
|
"key_repeat_delay": 500,
|
|
"key_repeat_frequency": 24.9,
|
|
"always_send_scancodes": "enabled",
|
|
"key_rightalt_to_key_win": "disabled",
|
|
"mouse": "enabled",
|
|
"high_resolution_scrolling": "enabled",
|
|
"native_pen_touch": "enabled",
|
|
"enable_input_only_mode": "disabled",
|
|
"keybindings": "[0x10,0xA0,0x11,0xA2,0x12,0xA4]", // todo: add this to UI
|
|
},
|
|
},
|
|
{
|
|
id: "av",
|
|
name: "Audio/Video",
|
|
options: {
|
|
"audio_sink": "",
|
|
"virtual_sink": "",
|
|
"install_steam_audio_drivers": "enabled",
|
|
"keep_sink_default": "enabled",
|
|
"auto_capture_sink": "enabled",
|
|
"adapter_name": "",
|
|
"output_name": "",
|
|
"fallback_mode": "",
|
|
"dd_configuration_option": "disabled",
|
|
"dd_resolution_option": "auto",
|
|
"dd_manual_resolution": "",
|
|
"dd_refresh_rate_option": "auto",
|
|
"dd_manual_refresh_rate": "",
|
|
"dd_hdr_option": "auto",
|
|
"dd_config_revert_delay": 3000,
|
|
"dd_config_revert_on_disconnect": "disabled",
|
|
"dd_mode_remapping": {"mixed": [], "resolution_only": [], "refresh_rate_only": []},
|
|
"dd_wa_hdr_toggle": "disabled",
|
|
"fallback_mode": "",
|
|
"headless_mode": "disabled",
|
|
"double_refreshrate": "disabled",
|
|
"dd_wa_hdr_toggle_delay": 0,
|
|
"min_fps_factor": 1,
|
|
"max_bitrate": 0,
|
|
"isolated_virtual_display_option": "disabled",
|
|
},
|
|
},
|
|
{
|
|
id: "network",
|
|
name: "Network",
|
|
options: {
|
|
"upnp": "disabled",
|
|
"address_family": "ipv4",
|
|
"port": 47989,
|
|
"origin_web_ui_allowed": "lan",
|
|
"external_ip": "",
|
|
"lan_encryption_mode": 0,
|
|
"wan_encryption_mode": 1,
|
|
"ping_timeout": 10000,
|
|
},
|
|
},
|
|
{
|
|
id: "files",
|
|
name: "Config Files",
|
|
options: {
|
|
"file_apps": "",
|
|
"credentials_file": "",
|
|
"log_path": "",
|
|
"pkey": "",
|
|
"cert": "",
|
|
"file_state": "",
|
|
},
|
|
},
|
|
{
|
|
id: "advanced",
|
|
name: "Advanced",
|
|
options: {
|
|
"fec_percentage": 20,
|
|
"qp": 28,
|
|
"min_threads": 2,
|
|
"limit_framerate": "enabled",
|
|
"hevc_mode": 0,
|
|
"av1_mode": 0,
|
|
"capture": "",
|
|
"encoder": "",
|
|
},
|
|
},
|
|
{
|
|
id: "nv",
|
|
name: "NVIDIA NVENC Encoder",
|
|
options: {
|
|
"nvenc_preset": 1,
|
|
"nvenc_twopass": "quarter_res",
|
|
"nvenc_spatial_aq": "disabled",
|
|
"nvenc_vbv_increase": 0,
|
|
"nvenc_realtime_hags": "enabled",
|
|
"nvenc_latency_over_power": "enabled",
|
|
"nvenc_opengl_vulkan_on_dxgi": "enabled",
|
|
"nvenc_h264_cavlc": "disabled",
|
|
"nvenc_intra_refresh": "disabled"
|
|
},
|
|
},
|
|
{
|
|
id: "qsv",
|
|
name: "Intel QuickSync Encoder",
|
|
options: {
|
|
"qsv_preset": "medium",
|
|
"qsv_coder": "auto",
|
|
"qsv_slow_hevc": "disabled",
|
|
},
|
|
},
|
|
{
|
|
id: "amd",
|
|
name: "AMD AMF Encoder",
|
|
options: {
|
|
"amd_usage": "ultralowlatency",
|
|
"amd_rc": "vbr_latency",
|
|
"amd_enforce_hrd": "disabled",
|
|
"amd_quality": "balanced",
|
|
"amd_preanalysis": "disabled",
|
|
"amd_vbaq": "enabled",
|
|
"amd_coder": "auto",
|
|
},
|
|
},
|
|
{
|
|
id: "vt",
|
|
name: "VideoToolbox Encoder",
|
|
options: {
|
|
"vt_coder": "auto",
|
|
"vt_software": "auto",
|
|
"vt_realtime": "enabled",
|
|
},
|
|
},
|
|
{
|
|
id: "vaapi",
|
|
name: "VA-API Encoder",
|
|
options: {
|
|
"vaapi_strict_rc_buffer": "disabled",
|
|
},
|
|
},
|
|
{
|
|
id: "sw",
|
|
name: "Software Encoder",
|
|
options: {
|
|
"sw_preset": "superfast",
|
|
"sw_tune": "zerolatency",
|
|
},
|
|
},
|
|
],
|
|
};
|
|
},
|
|
provide() {
|
|
return {
|
|
platform: computed(() => this.platform)
|
|
}
|
|
},
|
|
created() {
|
|
fetch("./api/config", {
|
|
credentials: 'include'
|
|
})
|
|
.then((r) => r.json())
|
|
.then((r) => {
|
|
this.config = r;
|
|
this.platform = this.config.platform;
|
|
|
|
var app = document.getElementById("app");
|
|
if (this.platform === "windows") {
|
|
this.tabs = this.tabs.filter((el) => {
|
|
return el.id !== "vt" && el.id !== "vaapi";
|
|
});
|
|
}
|
|
if (this.platform === "linux") {
|
|
this.tabs = this.tabs.filter((el) => {
|
|
return el.id !== "amd" && el.id !== "qsv" && el.id !== "vt";
|
|
});
|
|
}
|
|
if (this.platform === "macos") {
|
|
this.tabs = this.tabs.filter((el) => {
|
|
return el.id !== "amd" && el.id !== "nv" && el.id !== "qsv" && el.id !== "vaapi";
|
|
});
|
|
}
|
|
|
|
// remove values we don't want in the config file
|
|
delete this.config.platform;
|
|
delete this.config.status;
|
|
delete this.config.version;
|
|
|
|
this.vdisplayStatus = this.config.vdisplayStatus;
|
|
delete this.config.vdisplayStatus;
|
|
|
|
fallbackDisplayModeCache = this.config.fallback_mode || "";
|
|
|
|
// TODO: let each tab's Component handle it's own data instead of doing it here
|
|
|
|
// Parse the special options before population if available
|
|
const specialOptions = ["dd_mode_remapping", "global_prep_cmd", "server_cmd"]
|
|
for (const optionKey of specialOptions) {
|
|
if (typeof this.config[optionKey] === "string") {
|
|
this.config[optionKey] = JSON.parse(this.config[optionKey]);
|
|
} else {
|
|
this.config[optionKey] = null;
|
|
}
|
|
}
|
|
|
|
this.config.dd_mode_remapping ??= {mixed: [], resolution_only: [], refresh_rate_only: []};
|
|
this.config.global_prep_cmd ??= [];
|
|
this.config.server_cmd ??= [];
|
|
|
|
// Populate default values from tabs options
|
|
this.tabs.forEach(tab => {
|
|
Object.keys(tab.options).forEach(optionKey => {
|
|
if (this.config[optionKey] === undefined) {
|
|
// Make sure to copy by value
|
|
this.config[optionKey] = JSON.parse(JSON.stringify(tab.options[optionKey]));
|
|
}
|
|
});
|
|
});
|
|
|
|
this.global_prep_cmd = this.config.global_prep_cmd;
|
|
this.server_cmd = this.config.server_cmd;
|
|
});
|
|
},
|
|
methods: {
|
|
forceUpdate() {
|
|
this.$forceUpdate()
|
|
},
|
|
serialize() {
|
|
// Validate fallback mode
|
|
if (this.config.fallback_mode && !this.config.fallback_mode.match(/^\d+x\d+x\d+$/)) {
|
|
this.config.fallback_mode = fallbackDisplayModeCache;
|
|
} else {
|
|
fallbackDisplayModeCache = this.config.fallback_mode;
|
|
}
|
|
let config = JSON.parse(JSON.stringify(this.config));
|
|
config.global_prep_cmd = this.global_prep_cmd.filter(cmd => cmd.do || cmd.undo);
|
|
config.dd_mode_remapping = config.dd_mode_remapping;
|
|
config.server_cmd = this.server_cmd.filter(cmd => cmd.name && cmd.cmd);
|
|
return config;
|
|
},
|
|
save() {
|
|
this.saved = false;
|
|
this.restarted = false;
|
|
|
|
// create a temp copy of this.config to use for the post request
|
|
let config = this.serialize();
|
|
|
|
// delete default values from this.config
|
|
this.tabs.forEach(tab => {
|
|
Object.keys(tab.options).forEach(optionKey => {
|
|
let delete_value = false
|
|
|
|
// todo: add proper type checking
|
|
if (JSON.stringify(config[optionKey]) === JSON.stringify(tab.options[optionKey])) {
|
|
delete_value = true
|
|
}
|
|
|
|
if (delete_value) {
|
|
delete config[optionKey]
|
|
}
|
|
});
|
|
});
|
|
|
|
return fetch("./api/config", {
|
|
credentials: 'include',
|
|
method: "POST",
|
|
body: JSON.stringify(config),
|
|
}).then((r) => {
|
|
if (r.status === 200) {
|
|
this.saved = true
|
|
return this.saved
|
|
}
|
|
else {
|
|
return false
|
|
}
|
|
});
|
|
},
|
|
apply() {
|
|
this.saved = this.restarted = false;
|
|
let saved = this.save();
|
|
|
|
saved.then((result) => {
|
|
if (result === true) {
|
|
this.restarted = true;
|
|
setTimeout(() => {
|
|
this.saved = this.restarted = false;
|
|
}, 5000);
|
|
fetch("./api/restart", {
|
|
method: "POST"
|
|
})
|
|
.then((resp) => {
|
|
if (resp.status !== 200) {
|
|
location.reload();
|
|
return
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
console.error(e);
|
|
setTimeout(() => {
|
|
location.reload();
|
|
}, 1000);
|
|
return;
|
|
});
|
|
}
|
|
});
|
|
},
|
|
},
|
|
mounted() {
|
|
// Handle hashchange events
|
|
const handleHash = () => {
|
|
let hash = window.location.hash;
|
|
if (hash) {
|
|
// remove the # from the hash
|
|
let stripped_hash = hash.substring(1);
|
|
|
|
this.tabs.forEach(tab => {
|
|
Object.keys(tab.options).forEach(key => {
|
|
if (tab.id === stripped_hash || key === stripped_hash) {
|
|
this.currentTab = tab.id;
|
|
}
|
|
if (key === stripped_hash) {
|
|
// sleep for 2 seconds to allow the page to load
|
|
setTimeout(() => {
|
|
let element = document.getElementById(stripped_hash);
|
|
if (element) {
|
|
window.location.hash = hash;
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
if (this.currentTab === tab.id) {
|
|
// stop looping
|
|
return true;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
// Call handleHash for the initial load
|
|
handleHash();
|
|
|
|
// Add hashchange event listener
|
|
window.addEventListener("hashchange", handleHash);
|
|
},
|
|
});
|
|
|
|
initApp(app);
|
|
</script>
|