Compare commits

...

4 Commits

Author SHA1 Message Date
Marc Brooks 13aff1109a
Merge 748155d815 into cc9ff74276 2025-10-15 03:35:16 +00:00
Marc Brooks 748155d815
Added setting to choose locale 2025-10-14 22:35:05 -05:00
Aveline cc9ff74276
feat: add HDMI sleep mode (#881) 2025-10-09 14:52:51 +02:00
Marc Brooks b144d9926f
Remove the temporary directory after extracting buildkit (#874) 2025-10-07 11:57:26 +02:00
21 changed files with 420 additions and 12 deletions

View File

@ -104,6 +104,7 @@ type Config struct {
UsbDevices *usbgadget.Devices `json:"usb_devices"`
NetworkConfig *network.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
}
func (c *Config) GetDisplayRotation() uint16 {

View File

@ -19,6 +19,7 @@ type Native struct {
onVideoFrameReceived func(frame []byte, duration time.Duration)
onIndevEvent func(event string)
onRpcEvent func(event string)
sleepModeSupported bool
videoLock sync.Mutex
screenLock sync.Mutex
}
@ -62,6 +63,8 @@ func NewNative(opts NativeOptions) *Native {
}
}
sleepModeSupported := isSleepModeSupported()
return &Native{
ready: make(chan struct{}),
l: nativeLogger,
@ -73,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
onVideoFrameReceived: onVideoFrameReceived,
onIndevEvent: onIndevEvent,
onRpcEvent: onRpcEvent,
sleepModeSupported: sleepModeSupported,
videoLock: sync.Mutex{},
screenLock: sync.Mutex{},
}

View File

@ -1,5 +1,12 @@
package native
import (
"os"
)
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
// VideoState is the state of the video stream.
type VideoState struct {
Ready bool `json:"ready"`
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
@ -8,6 +15,58 @@ type VideoState struct {
FramePerSecond float64 `json:"fps"`
}
func isSleepModeSupported() bool {
_, err := os.Stat(sleepModeFile)
return err == nil
}
func (n *Native) setSleepMode(enabled bool) error {
if !n.sleepModeSupported {
return nil
}
bEnabled := "0"
if enabled {
bEnabled = "1"
}
return os.WriteFile(sleepModeFile, []byte(bEnabled), 0644)
}
func (n *Native) getSleepMode() (bool, error) {
if !n.sleepModeSupported {
return false, nil
}
data, err := os.ReadFile(sleepModeFile)
if err == nil {
return string(data) == "1", nil
}
return false, nil
}
// VideoSetSleepMode sets the sleep mode for the video stream.
func (n *Native) VideoSetSleepMode(enabled bool) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
return n.setSleepMode(enabled)
}
// VideoGetSleepMode gets the sleep mode for the video stream.
func (n *Native) VideoGetSleepMode() (bool, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
return n.getSleepMode()
}
// VideoSleepModeSupported checks if the sleep mode is supported.
func (n *Native) VideoSleepModeSupported() bool {
return n.sleepModeSupported
}
// VideoSetQualityFactor sets the quality factor for the video stream.
func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -15,6 +74,7 @@ func (n *Native) VideoSetQualityFactor(factor float64) error {
return videoSetStreamQualityFactor(factor)
}
// VideoGetQualityFactor gets the quality factor for the video stream.
func (n *Native) VideoGetQualityFactor() (float64, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -22,6 +82,7 @@ func (n *Native) VideoGetQualityFactor() (float64, error) {
return videoGetStreamQualityFactor()
}
// VideoSetEDID sets the EDID for the video stream.
func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -29,6 +90,7 @@ func (n *Native) VideoSetEDID(edid string) error {
return videoSetEDID(edid)
}
// VideoGetEDID gets the EDID for the video stream.
func (n *Native) VideoGetEDID() (string, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -36,6 +98,7 @@ func (n *Native) VideoGetEDID() (string, error) {
return videoGetEDID()
}
// VideoLogStatus gets the log status for the video stream.
func (n *Native) VideoLogStatus() (string, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -43,6 +106,7 @@ func (n *Native) VideoLogStatus() (string, error) {
return videoLogStatus(), nil
}
// VideoStop stops the video stream.
func (n *Native) VideoStop() error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -51,10 +115,14 @@ func (n *Native) VideoStop() error {
return nil
}
// VideoStart starts the video stream.
func (n *Native) VideoStart() error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
// disable sleep mode before starting video
_ = n.setSleepMode(false)
videoStart()
return nil
}

View File

@ -1215,6 +1215,8 @@ var rpcHandlers = map[string]RPCHandler{
"getEDID": {Func: rpcGetEDID},
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
"getVideoLogStatus": {Func: rpcGetVideoLogStatus},
"getVideoSleepMode": {Func: rpcGetVideoSleepMode},
"setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}},
"getDevChannelState": {Func: rpcGetDevChannelState},
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
"getLocalVersion": {Func: rpcGetLocalVersion},

View File

@ -77,6 +77,9 @@ func Main() {
// initialize display
initDisplay()
// start video sleep mode timer
startVideoSleepModeTicker()
go func() {
time.Sleep(15 * time.Minute)
for {

View File

@ -31,9 +31,17 @@
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"plugin.inlang.mFunctionMatcher": {
"matchers": [
{
"type": "m-function",
"function": "plural",
"parameter": "count"
}
]
},
"strategy": [
"cookie",
"localStorage",
"preferredLanguage",
"baseLocale"
]

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "Du har ændret din adgangskode til beskyttelse af din lokale enhed. Husk din nye adgangskode til senere brug.",
"local_auth_success_password_updated_title": "Adgangskode opdateret",
"local_auth_update_password_button": "Opdater adgangskode",
"locale_auto": "Bil",
"locale_change_success": "Sproget er ændret til {locale}",
"locale_da": "Dansk",
"locale_de": "Tysk",
"locale_en": "Engelsk",
"locale_es": "Spansk",
"locale_fr": "Fransk",
"locale_it": "Italiensk",
"locale_nb": "Norsk (bokmål)",
"locale_sv": "Svensk",
"locale_zh": "中文 (简体)",
"log_in": "Log ind",
"log_out": "Log ud",
"logged_in_as": "Logget ind som",
@ -748,6 +759,8 @@
"usb_state_disconnected": "Afbrudt",
"usb_state_low_power_mode": "Lavstrømstilstand",
"usb": "USB",
"user_interface_language_description": "Vælg det sprog, der skal bruges i JetKVM-brugergrænsefladen",
"user_interface_language_title": "Grænsefladesprog",
"video_brightness_description": "Lysstyrkeniveau ( {value} x)",
"video_brightness_title": "Lysstyrke",
"video_contrast_description": "Kontrastniveau ( {value} x)",

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "Sie haben Ihr lokales Geräteschutzkennwort erfolgreich geändert. Merken Sie sich das neue Kennwort für zukünftige Zugriffe.",
"local_auth_success_password_updated_title": "Passwort erfolgreich aktualisiert",
"local_auth_update_password_button": "Kennwort aktualisieren",
"locale_auto": "Auto",
"locale_change_success": "Die Sprache wurde erfolgreich in {locale} geändert.",
"locale_da": "Dänisch",
"locale_de": "Deutsch",
"locale_en": "Englisch",
"locale_es": "Spanisch",
"locale_fr": "Deutsch",
"locale_it": "Italienisch",
"locale_nb": "Norwegisch (bokmål)",
"locale_sv": "Schwedisch",
"locale_zh": "中文 (简体)",
"log_in": "Einloggen",
"log_out": "Ausloggen",
"logged_in_as": "Angemeldet als",
@ -748,6 +759,8 @@
"usb_state_disconnected": "Getrennt",
"usb_state_low_power_mode": "Energiesparmodus",
"usb": "USB",
"user_interface_language_description": "Wählen Sie die Sprache aus, die in der JetKVM-Benutzeroberfläche verwendet werden soll",
"user_interface_language_title": "Schnittstellensprache",
"video_brightness_description": "Helligkeitsstufe ( {value} x)",
"video_brightness_title": "Helligkeit",
"video_contrast_description": "Kontraststufe ( {value} x)",

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "You've successfully changed your local device protection password. Make sure to remember your new password for future access.",
"local_auth_success_password_updated_title": "Password Updated Successfully",
"local_auth_update_password_button": "Update Password",
"locale_auto": "Auto",
"locale_change_success": "Language changed successfully to {locale}",
"locale_da": "Dansk",
"locale_de": "Deutsch",
"locale_en": "English",
"locale_es": "Español",
"locale_fr": "Français",
"locale_it": "Italiano",
"locale_nb": "Norsk (bokmål)",
"locale_sv": "Svenska",
"locale_zh": "中文 (简体)",
"log_in": "Log In",
"log_out": "Log out",
"logged_in_as": "Logged in as",
@ -748,6 +759,8 @@
"usb_state_disconnected": "Disconnected",
"usb_state_low_power_mode": "Low power mode",
"usb": "USB",
"user_interface_language_description": "Select the language to use in the JetKVM user interface",
"user_interface_language_title": "Interface Language",
"video_brightness_description": "Brightness level ({value}x)",
"video_brightness_title": "Brightness",
"video_contrast_description": "Contrast level ({value}x)",

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "Has cambiado correctamente la contraseña de protección de tu dispositivo local. Recuerda la nueva contraseña para acceder en el futuro.",
"local_auth_success_password_updated_title": "Contraseña actualizada exitosamente",
"local_auth_update_password_button": "Actualizar contraseña",
"locale_auto": "Auto",
"locale_change_success": "El idioma se cambió correctamente a {locale}",
"locale_da": "Danés",
"locale_de": "Alemán",
"locale_en": "Inglés",
"locale_es": "Español",
"locale_fr": "Francés",
"locale_it": "Italiano",
"locale_nb": "Noruego (bokmål)",
"locale_sv": "Sueco",
"locale_zh": "中文 (简体)",
"log_in": "Acceso",
"log_out": "Finalizar la sesión",
"logged_in_as": "Inició sesión como",
@ -748,6 +759,8 @@
"usb_state_disconnected": "Desconectado",
"usb_state_low_power_mode": "Modo de bajo consumo",
"usb": "USB",
"user_interface_language_description": "Seleccione el idioma que se utilizará en la interfaz de usuario de JetKVM",
"user_interface_language_title": "Lenguaje de interfaz",
"video_brightness_description": "Nivel de brillo ( {value} x)",
"video_brightness_title": "Brillo",
"video_contrast_description": "Nivel de contraste ( {value} x)",

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "Vous avez modifié avec succès le mot de passe de protection de votre appareil local. N'oubliez pas de le mémoriser pour y accéder ultérieurement.",
"local_auth_success_password_updated_title": "Mot de passe mis à jour avec succès",
"local_auth_update_password_button": "Mettre à jour le mot de passe",
"locale_auto": "Auto",
"locale_change_success": "La langue a été modifiée avec succès en {locale}",
"locale_da": "danois",
"locale_de": "Allemand",
"locale_en": "Anglais",
"locale_es": "Espagnol",
"locale_fr": "Français",
"locale_it": "italien",
"locale_nb": "Norvégien (bokmål)",
"locale_sv": "suédois",
"locale_zh": "中文 (简体)",
"log_in": "Se connecter",
"log_out": "Se déconnecter",
"logged_in_as": "Connecté en tant que",
@ -748,6 +759,8 @@
"usb_state_disconnected": "Déconnecté",
"usb_state_low_power_mode": "Mode basse consommation",
"usb": "USB",
"user_interface_language_description": "Sélectionnez la langue à utiliser dans l'interface utilisateur de JetKVM",
"user_interface_language_title": "Langue de l'interface",
"video_brightness_description": "Niveau de luminosité ( {value} x)",
"video_brightness_title": "Luminosité",
"video_contrast_description": "Niveau de contraste ( {value} x)",

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "Hai modificato correttamente la password di protezione del tuo dispositivo locale. Assicurati di ricordare la nuova password per gli accessi futuri.",
"local_auth_success_password_updated_title": "Password aggiornata con successo",
"local_auth_update_password_button": "Aggiorna password",
"locale_auto": "Auto",
"locale_change_success": "Lingua modificata correttamente in {locale}",
"locale_da": "Danese",
"locale_de": "Tedesco",
"locale_en": "Inglese",
"locale_es": "Spagnolo",
"locale_fr": "Francese",
"locale_it": "Italiano",
"locale_nb": "Norvegese (bokmål)",
"locale_sv": "Svedese",
"locale_zh": "中文 (简体)",
"log_in": "Login",
"log_out": "Disconnetti",
"logged_in_as": "Accedi come",
@ -748,6 +759,8 @@
"usb_state_disconnected": "Disconnesso",
"usb_state_low_power_mode": "Modalità a basso consumo",
"usb": "USB",
"user_interface_language_description": "Seleziona la lingua da utilizzare nell'interfaccia utente JetKVM",
"user_interface_language_title": "Lingua dell'interfaccia",
"video_brightness_description": "Livello di luminosità ( {value} x)",
"video_brightness_title": "Luminosità",
"video_contrast_description": "Livello di contrasto ( {value} x)",

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "Du har endret passordet for beskyttelse av den lokale enheten. Husk det nye passordet for fremtidig tilgang.",
"local_auth_success_password_updated_title": "Passord oppdatert",
"local_auth_update_password_button": "Oppdater passord",
"locale_auto": "Bil",
"locale_change_success": "Språket er endret til {locale}",
"locale_da": "Dansk",
"locale_de": "Tysk",
"locale_en": "Engelsk",
"locale_es": "Spansk",
"locale_fr": "Fransk",
"locale_it": "Italiensk",
"locale_nb": "Norsk (bokmål)",
"locale_sv": "Svensk",
"locale_zh": "中文 (简体)",
"log_in": "Logg inn",
"log_out": "Logg ut",
"logged_in_as": "Logget inn som",
@ -748,6 +759,8 @@
"usb_state_disconnected": "Frakoblet",
"usb_state_low_power_mode": "Lavstrømsmodus",
"usb": "USB",
"user_interface_language_description": "Velg språket som skal brukes i JetKVM-brukergrensesnittet",
"user_interface_language_title": "Grensesnittspråk",
"video_brightness_description": "Lysstyrkenivå ( {value} x)",
"video_brightness_title": "Lysstyrke",
"video_contrast_description": "Kontrastnivå ( {value} x)",

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "Du har ändrat ditt lösenord för lokal enhetsskydd. Se till att komma ihåg ditt nya lösenord för framtida åtkomst.",
"local_auth_success_password_updated_title": "Lösenordet har uppdaterats",
"local_auth_update_password_button": "Uppdatera lösenord",
"locale_auto": "Bil",
"locale_change_success": "Språket har ändrats till {locale}",
"locale_da": "Danska",
"locale_de": "Deutsch",
"locale_en": "Engelska",
"locale_es": "Spanska",
"locale_fr": "Franska",
"locale_it": "italiensk",
"locale_nb": "Norska (bokmål)",
"locale_sv": "Svenska",
"locale_zh": "中文 (简体)",
"log_in": "Logga in",
"log_out": "Logga ut",
"logged_in_as": "Inloggad som",
@ -748,6 +759,8 @@
"usb_state_disconnected": "Osammanhängande",
"usb_state_low_power_mode": "Lågströmsläge",
"usb": "USB",
"user_interface_language_description": "Välj språket som ska användas i JetKVM-användargränssnittet",
"user_interface_language_title": "Gränssnittsspråk",
"video_brightness_description": "Ljusstyrka ( {value} x)",
"video_brightness_title": "Ljusstyrka",
"video_contrast_description": "Kontrastnivå ( {value} x)",

View File

@ -395,6 +395,17 @@
"local_auth_success_password_updated_description": "您已成功更改本地设备保护密码。请务必记住新密码,以便日后访问。",
"local_auth_success_password_updated_title": "密码更新成功",
"local_auth_update_password_button": "更新密码",
"locale_auto": "汽车",
"locale_change_success": "语言已成功更改为{locale}",
"locale_da": "丹麦语",
"locale_de": "德语",
"locale_en": "英语",
"locale_es": "西班牙语",
"locale_fr": "法语",
"locale_it": "意大利语",
"locale_nb": "挪威语(博克马尔语)",
"locale_sv": "瑞典语",
"locale_zh": "中文 (简体)",
"log_in": "登录",
"log_out": "登出",
"logged_in_as": "登录身份",
@ -748,6 +759,8 @@
"usb_state_disconnected": "断开连接",
"usb_state_low_power_mode": "低功耗模式",
"usb": "USB",
"user_interface_language_description": "选择 JetKVM 用户界面使用的语言",
"user_interface_language_title": "界面语言",
"video_brightness_description": "亮度级别( {value} x",
"video_brightness_title": "亮度",
"video_contrast_description": "对比度级别( {value} x",

View File

@ -244,6 +244,7 @@ export interface MouseMove {
y: number;
buttons: number;
}
export interface MouseState {
mouseX: number;
mouseY: number;
@ -347,8 +348,10 @@ export interface SettingsState {
// Video enhancement settings
videoSaturation: number;
setVideoSaturation: (value: number) => void;
videoBrightness: number;
setVideoBrightness: (value: number) => void;
videoContrast: number;
setVideoContrast: (value: number) => void;
}
@ -392,8 +395,10 @@ export const useSettingsStore = create(
// Video enhancement settings with default values (1.0 = normal)
videoSaturation: 1.0,
setVideoSaturation: (value: number) => set({ videoSaturation: value }),
videoBrightness: 1.0,
setVideoBrightness: (value: number) => set({ videoBrightness: value }),
videoContrast: 1.0,
setVideoContrast: (value: number) => set({ videoContrast: value }),
}),

View File

@ -1,14 +1,17 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { useDeviceStore } from "@hooks/stores";
import { Button } from "@components/Button";
import Checkbox from "@components/Checkbox";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications";
import { getLocale, setLocale, locales, baseLocale } from '@localizations/runtime.js';
import { m } from "@localizations/messages.js";
import { deleteCookie, map_locale_code_to_name } from "@/utils";
export default function SettingsGeneralRoute() {
const { send } = useJsonRpc();
@ -40,6 +43,37 @@ export default function SettingsGeneralRoute() {
});
};
const [currentLocale, setCurrentLocale] = useState(getLocale());
const localeOptions = useMemo(() => {
return ["", ...locales]
.map((code) => {
const [localizedName, nativeName] = map_locale_code_to_name(code);
// don't repeat the name if it's the same in both locales (or blank)
const label = nativeName && nativeName !== localizedName ? `${localizedName} - ${nativeName}` : localizedName;
return { value: code, label: label }
});
}, []);
const handleLocaleChange = (newLocale: string) => {
if (newLocale === currentLocale) return;
let validLocale = newLocale as typeof locales[number];
if (newLocale !== "") {
if (!locales.includes(validLocale)) {
validLocale = baseLocale;
}
setLocale(validLocale); // tell the i18n system to change locale
} else {
deleteCookie("JETKVM_LOCALE", "", "/"); // delete the cookie that the i18n system uses to store the locale
}
setCurrentLocale(validLocale);
notifications.success(m.locale_change_success({ locale: validLocale || m.locale_auto() }));
};
return (
<div className="space-y-4">
<SettingsPageHeader
@ -49,6 +83,20 @@ export default function SettingsGeneralRoute() {
<div className="space-y-4">
<div className="space-y-4 pb-2">
<div className="space-y-4">
<SettingsItem
title={m.user_interface_language_title()}
description={m.user_interface_language_description()}
>
<SelectMenuBasic
size="SM"
label=""
value={currentLocale}
options={localeOptions}
onChange={e => { handleLocaleChange(e.target.value); }}
/>
</SettingsItem>
</div>
<div className="mt-2 flex items-center justify-between gap-x-2">
<SettingsItem
title={m.general_check_for_updates()}
@ -82,7 +130,6 @@ export default function SettingsGeneralRoute() {
/>
</SettingsItem>
</div>
<div className="mt-2 flex items-center justify-between gap-x-2">
<SettingsItem
title={m.general_reboot_device()}

View File

@ -1,5 +1,6 @@
import { KeySequence } from "@hooks/stores";
import { getLocale } from '@localizations/runtime.js';
import { m } from "@localizations/messages.js";
export const formatters = {
date: (date: Date, options?: Intl.DateTimeFormatOptions) =>
@ -254,3 +255,29 @@ export function normalizeSortOrders(macros: KeySequence[]): KeySequence[] {
sortOrder: index + 1,
}));
};
export function map_locale_code_to_name(locale: string): [string, string] {
// the first is the name in the current app locale (e.g. Inglese),
// the second is the name in the language of the locale itself (e.g. English)
switch (locale) {
case '': return [m.locale_auto(), ""];
case 'en': return [m.locale_en(), m.locale_en({}, { locale })];
case 'da': return [m.locale_da(), m.locale_da({}, { locale })];
case 'de': return [m.locale_de(), m.locale_de({}, { locale })];
case 'es': return [m.locale_es(), m.locale_es({}, { locale })];
case 'fr': return [m.locale_fr(), m.locale_fr({}, { locale })];
case 'it': return [m.locale_it(), m.locale_it({}, { locale })];
case 'nb': return [m.locale_nb(), m.locale_nb({}, { locale })];
case 'sv': return [m.locale_sv(), m.locale_sv({}, { locale })];
case 'zh': return [m.locale_zh(), m.locale_zh({}, { locale })];
default: return [locale, ""];
}
}
export function deleteCookie(name: string, domain?: string, path = "/") {
const domainPart = domain ? `; domain=${domain}` : "";
// max-age=0 removes the cookie immediately in modern browsers
document.cookie = `${name}=; path=${path}; max-age=0${domainPart}`;
// fallback: set an expires in the past for older agents
document.cookie = `${name}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:00 GMT${domainPart}`;
}

View File

@ -33,8 +33,7 @@ export default defineConfig(({ mode, command }) => {
outdir: "./localization/paraglide",
outputStructure: 'message-modules',
cookieName: 'JETKVM_LOCALE',
localStorageKey: 'JETKVM_LOCALE',
strategy: ['cookie', 'localStorage', 'preferredLanguage', 'baseLocale'],
strategy: ['cookie', 'preferredLanguage', 'baseLocale'],
}))
return {

103
video.go
View File

@ -1,10 +1,22 @@
package kvm
import (
"context"
"fmt"
"time"
"github.com/jetkvm/kvm/internal/native"
)
var lastVideoState native.VideoState
var (
lastVideoState native.VideoState
videoSleepModeCtx context.Context
videoSleepModeCancel context.CancelFunc
)
const (
defaultVideoSleepModeDuration = 1 * time.Minute
)
func triggerVideoStateUpdate() {
go func() {
@ -17,3 +29,92 @@ func triggerVideoStateUpdate() {
func rpcGetVideoState() (native.VideoState, error) {
return lastVideoState, nil
}
type rpcVideoSleepModeResponse struct {
Supported bool `json:"supported"`
Enabled bool `json:"enabled"`
Duration int `json:"duration"`
}
func rpcGetVideoSleepMode() rpcVideoSleepModeResponse {
sleepMode, _ := nativeInstance.VideoGetSleepMode()
return rpcVideoSleepModeResponse{
Supported: nativeInstance.VideoSleepModeSupported(),
Enabled: sleepMode,
Duration: config.VideoSleepAfterSec,
}
}
func rpcSetVideoSleepMode(duration int) error {
if duration < 0 {
duration = -1 // disable
}
config.VideoSleepAfterSec = duration
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
// we won't restart the ticker here,
// as the session can't be inactive when this function is called
return nil
}
func stopVideoSleepModeTicker() {
nativeLogger.Trace().Msg("stopping HDMI sleep mode ticker")
if videoSleepModeCancel != nil {
nativeLogger.Trace().Msg("canceling HDMI sleep mode ticker context")
videoSleepModeCancel()
videoSleepModeCancel = nil
videoSleepModeCtx = nil
}
}
func startVideoSleepModeTicker() {
if !nativeInstance.VideoSleepModeSupported() {
return
}
var duration time.Duration
if config.VideoSleepAfterSec == 0 {
duration = defaultVideoSleepModeDuration
} else if config.VideoSleepAfterSec > 0 {
duration = time.Duration(config.VideoSleepAfterSec) * time.Second
} else {
stopVideoSleepModeTicker()
return
}
// Stop any existing timer and goroutine
stopVideoSleepModeTicker()
// Create new context for this ticker
videoSleepModeCtx, videoSleepModeCancel = context.WithCancel(context.Background())
go doVideoSleepModeTicker(videoSleepModeCtx, duration)
}
func doVideoSleepModeTicker(ctx context.Context, duration time.Duration) {
timer := time.NewTimer(duration)
defer timer.Stop()
nativeLogger.Trace().Msg("HDMI sleep mode ticker started")
for {
select {
case <-timer.C:
if getActiveSessions() > 0 {
nativeLogger.Warn().Msg("not going to enter HDMI sleep mode because there are active sessions")
continue
}
nativeLogger.Trace().Msg("entering HDMI sleep mode")
_ = nativeInstance.VideoSetSleepMode(true)
case <-ctx.Done():
nativeLogger.Trace().Msg("HDMI sleep mode ticker stopped")
return
}
}
}

View File

@ -39,6 +39,34 @@ type Session struct {
keysDownStateQueue chan usbgadget.KeysDownState
}
var (
actionSessions int = 0
activeSessionsMutex = &sync.Mutex{}
)
func incrActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
actionSessions++
return actionSessions
}
func decrActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
actionSessions--
return actionSessions
}
func getActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
return actionSessions
}
func (s *Session) resetKeepAliveTime() {
s.keepAliveJitterLock.Lock()
defer s.keepAliveJitterLock.Unlock()
@ -312,9 +340,8 @@ func newSession(config SessionConfig) (*Session, error) {
if connectionState == webrtc.ICEConnectionStateConnected {
if !isConnected {
isConnected = true
actionSessions++
onActiveSessionsChanged()
if actionSessions == 1 {
if incrActiveSessions() == 1 {
onFirstSessionConnected()
}
}
@ -353,9 +380,8 @@ func newSession(config SessionConfig) (*Session, error) {
}
if isConnected {
isConnected = false
actionSessions--
onActiveSessionsChanged()
if actionSessions == 0 {
if decrActiveSessions() == 0 {
onLastSessionDisconnected()
}
}
@ -364,16 +390,16 @@ func newSession(config SessionConfig) (*Session, error) {
return session, nil
}
var actionSessions = 0
func onActiveSessionsChanged() {
requestDisplayUpdate(true, "active_sessions_changed")
}
func onFirstSessionConnected() {
_ = nativeInstance.VideoStart()
stopVideoSleepModeTicker()
}
func onLastSessionDisconnected() {
_ = nativeInstance.VideoStop()
startVideoSleepModeTicker()
}