mirror of https://github.com/jetkvm/kvm.git
Merge branch 'dev' into feat/multisession-support
Integrate upstream changes from dev branch including power saving feature and video improvements. Resolved conflict in hardware settings by merging permission checks with new power saving controls. Changes from dev: - Add HDMI sleep mode power saving feature - Video capture improvements - Native interface updates Multi-session features preserved: - Permission-based settings access control - All session management functionality intact
This commit is contained in:
commit
8dc013d8fe
|
|
@ -117,6 +117,7 @@ type Config struct {
|
|||
DefaultLogLevel string `json:"default_log_level"`
|
||||
SessionSettings *SessionSettings `json:"session_settings"`
|
||||
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
||||
VideoQualityFactor float64 `json:"video_quality_factor"`
|
||||
}
|
||||
|
||||
func (c *Config) GetDisplayRotation() uint16 {
|
||||
|
|
|
|||
|
|
@ -405,8 +405,8 @@ char *jetkvm_video_log_status() {
|
|||
return (char *)videoc_log_status();
|
||||
}
|
||||
|
||||
int jetkvm_video_init() {
|
||||
return video_init();
|
||||
int jetkvm_video_init(float factor) {
|
||||
return video_init(factor);
|
||||
}
|
||||
|
||||
void jetkvm_video_shutdown() {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const char *jetkvm_ui_get_lvgl_version();
|
|||
|
||||
const char *jetkvm_ui_event_code_to_name(int code);
|
||||
|
||||
int jetkvm_video_init();
|
||||
int jetkvm_video_init(float quality_factor);
|
||||
void jetkvm_video_shutdown();
|
||||
void jetkvm_video_start();
|
||||
void jetkvm_video_stop();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
#define VIDEO_DEV "/dev/video0"
|
||||
#define SUB_DEV "/dev/v4l-subdev2"
|
||||
#define SLEEP_MODE_FILE "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
|
||||
|
||||
#define RK_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
|
||||
#define RK_ALIGN_2(x) RK_ALIGN(x, 2)
|
||||
|
|
@ -39,6 +40,7 @@ int sub_dev_fd = -1;
|
|||
#define VENC_CHANNEL 0
|
||||
MB_POOL memPool = MB_INVALID_POOLID;
|
||||
|
||||
bool sleep_mode_available = false;
|
||||
bool should_exit = false;
|
||||
float quality_factor = 1.0f;
|
||||
|
||||
|
|
@ -51,6 +53,45 @@ RK_U64 get_us()
|
|||
return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */
|
||||
}
|
||||
|
||||
static void ensure_sleep_mode_disabled()
|
||||
{
|
||||
if (!sleep_mode_available)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int fd = open(SLEEP_MODE_FILE, O_RDWR);
|
||||
if (fd < 0)
|
||||
{
|
||||
log_error("Failed to open sleep mode file: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
char buffer[1];
|
||||
read(fd, buffer, 1);
|
||||
if (buffer[0] == '0') {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
log_warn("HDMI sleep mode is not disabled, disabling it");
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
write(fd, "0", 1);
|
||||
close(fd);
|
||||
|
||||
usleep(1000); // give some time to the system to disable the sleep mode
|
||||
return;
|
||||
}
|
||||
|
||||
static void detect_sleep_mode()
|
||||
{
|
||||
if (access(SLEEP_MODE_FILE, F_OK) != 0) {
|
||||
sleep_mode_available = false;
|
||||
return;
|
||||
}
|
||||
sleep_mode_available = true;
|
||||
ensure_sleep_mode_disabled();
|
||||
}
|
||||
|
||||
double calculate_bitrate(float bitrate_factor, int width, int height)
|
||||
{
|
||||
const int32_t base_bitrate_high = 2000;
|
||||
|
|
@ -190,8 +231,15 @@ static int32_t buf_init()
|
|||
|
||||
pthread_t *format_thread = NULL;
|
||||
|
||||
int video_init()
|
||||
int video_init(float factor)
|
||||
{
|
||||
detect_sleep_mode();
|
||||
|
||||
if (factor < 0 || factor > 1) {
|
||||
factor = 1.0f;
|
||||
}
|
||||
quality_factor = factor;
|
||||
|
||||
if (RK_MPI_SYS_Init() != RK_SUCCESS)
|
||||
{
|
||||
log_error("RK_MPI_SYS_Init failed");
|
||||
|
|
@ -301,11 +349,29 @@ static void *venc_read_stream(void *arg)
|
|||
}
|
||||
|
||||
uint32_t detected_width, detected_height;
|
||||
bool detected_signal = false, streaming_flag = false;
|
||||
bool detected_signal = false, streaming_flag = false, streaming_stopped = true;
|
||||
|
||||
pthread_t *streaming_thread = NULL;
|
||||
pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
bool get_streaming_flag()
|
||||
{
|
||||
log_info("getting streaming flag");
|
||||
pthread_mutex_lock(&streaming_mutex);
|
||||
bool flag = streaming_flag;
|
||||
pthread_mutex_unlock(&streaming_mutex);
|
||||
return flag;
|
||||
}
|
||||
|
||||
void set_streaming_flag(bool flag)
|
||||
{
|
||||
log_info("setting streaming flag to %d", flag);
|
||||
|
||||
pthread_mutex_lock(&streaming_mutex);
|
||||
streaming_flag = flag;
|
||||
pthread_mutex_unlock(&streaming_mutex);
|
||||
}
|
||||
|
||||
void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename)
|
||||
{
|
||||
FILE *file = fopen(filename, "wb");
|
||||
|
|
@ -319,6 +385,8 @@ void *run_video_stream(void *arg)
|
|||
|
||||
log_info("running video stream");
|
||||
|
||||
streaming_stopped = false;
|
||||
|
||||
while (streaming_flag)
|
||||
{
|
||||
if (detected_signal == false)
|
||||
|
|
@ -401,7 +469,7 @@ void *run_video_stream(void *arg)
|
|||
{
|
||||
log_error("get mb blk failed!");
|
||||
close(video_dev_fd);
|
||||
return ;
|
||||
return (void *)errno;
|
||||
}
|
||||
log_info("Got memory block for buffer %d", i);
|
||||
|
||||
|
|
@ -538,6 +606,18 @@ void *run_video_stream(void *arg)
|
|||
log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
// Explicitly free V4L2 buffer queue
|
||||
struct v4l2_requestbuffers req_free;
|
||||
memset(&req_free, 0, sizeof(req_free));
|
||||
req_free.count = 0;
|
||||
req_free.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||
req_free.memory = V4L2_MEMORY_DMABUF;
|
||||
|
||||
if (ioctl(video_dev_fd, VIDIOC_REQBUFS, &req_free) < 0)
|
||||
{
|
||||
log_error("Failed to free V4L2 buffers: %s", strerror(errno));
|
||||
}
|
||||
|
||||
venc_stop();
|
||||
|
||||
for (int i = 0; i < input_buffer_count; i++)
|
||||
|
|
@ -553,6 +633,9 @@ void *run_video_stream(void *arg)
|
|||
}
|
||||
|
||||
log_info("video stream thread exiting");
|
||||
|
||||
streaming_stopped = true;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -577,61 +660,80 @@ void video_shutdown()
|
|||
RK_MPI_MB_DestroyPool(memPool);
|
||||
}
|
||||
log_info("Destroyed memory pool");
|
||||
|
||||
|
||||
pthread_mutex_destroy(&streaming_mutex);
|
||||
log_info("Destroyed streaming mutex");
|
||||
}
|
||||
|
||||
|
||||
void video_start_streaming()
|
||||
{
|
||||
pthread_mutex_lock(&streaming_mutex);
|
||||
log_info("starting video streaming");
|
||||
if (streaming_thread != NULL)
|
||||
{
|
||||
if (streaming_stopped == true) {
|
||||
log_error("video streaming already stopped but streaming_thread is not NULL");
|
||||
assert(streaming_stopped == true);
|
||||
}
|
||||
log_warn("video streaming already started");
|
||||
goto cleanup;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
pthread_t *new_thread = malloc(sizeof(pthread_t));
|
||||
if (new_thread == NULL)
|
||||
{
|
||||
log_error("Failed to allocate memory for streaming thread");
|
||||
goto cleanup;
|
||||
return;
|
||||
}
|
||||
|
||||
streaming_flag = true;
|
||||
|
||||
set_streaming_flag(true);
|
||||
int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
|
||||
if (result != 0)
|
||||
{
|
||||
log_error("Failed to create streaming thread: %s", strerror(result));
|
||||
streaming_flag = false;
|
||||
set_streaming_flag(false);
|
||||
free(new_thread);
|
||||
goto cleanup;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only set streaming_thread after successful creation, and before unlocking the mutex
|
||||
|
||||
// Only set streaming_thread after successful creation
|
||||
streaming_thread = new_thread;
|
||||
cleanup:
|
||||
pthread_mutex_unlock(&streaming_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
void video_stop_streaming()
|
||||
{
|
||||
pthread_mutex_lock(&streaming_mutex);
|
||||
if (streaming_thread != NULL)
|
||||
{
|
||||
streaming_flag = false;
|
||||
log_info("stopping video streaming");
|
||||
// wait 100ms for the thread to exit
|
||||
usleep(1000000);
|
||||
log_info("waiting for video streaming thread to exit");
|
||||
pthread_join(*streaming_thread, NULL);
|
||||
free(streaming_thread);
|
||||
streaming_thread = NULL;
|
||||
log_info("video streaming stopped");
|
||||
if (streaming_thread == NULL) {
|
||||
log_info("video streaming already stopped");
|
||||
return;
|
||||
}
|
||||
pthread_mutex_unlock(&streaming_mutex);
|
||||
|
||||
log_info("stopping video streaming");
|
||||
set_streaming_flag(false);
|
||||
|
||||
log_info("waiting for video streaming thread to exit");
|
||||
int attempts = 0;
|
||||
while (!streaming_stopped && attempts < 30) {
|
||||
usleep(100000); // 100ms
|
||||
attempts++;
|
||||
}
|
||||
if (!streaming_stopped) {
|
||||
log_error("video streaming thread did not exit after 30s");
|
||||
}
|
||||
|
||||
pthread_join(*streaming_thread, NULL);
|
||||
free(streaming_thread);
|
||||
streaming_thread = NULL;
|
||||
|
||||
log_info("video streaming stopped");
|
||||
}
|
||||
|
||||
void video_restart_streaming()
|
||||
{
|
||||
if (get_streaming_flag() == true)
|
||||
{
|
||||
log_info("restarting video streaming");
|
||||
video_stop_streaming();
|
||||
}
|
||||
video_start_streaming();
|
||||
}
|
||||
|
||||
void *run_detect_format(void *arg)
|
||||
|
|
@ -650,6 +752,8 @@ void *run_detect_format(void *arg)
|
|||
|
||||
while (!should_exit)
|
||||
{
|
||||
ensure_sleep_mode_disabled();
|
||||
|
||||
memset(&dv_timings, 0, sizeof(dv_timings));
|
||||
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0)
|
||||
{
|
||||
|
|
@ -689,21 +793,17 @@ void *run_detect_format(void *arg)
|
|||
(dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
|
||||
dv_timings.bt.hbackporch));
|
||||
log_info("Frames per second: %.2f fps", frames_per_second);
|
||||
|
||||
bool should_restart = dv_timings.bt.width != detected_width || dv_timings.bt.height != detected_height || !detected_signal;
|
||||
|
||||
detected_width = dv_timings.bt.width;
|
||||
detected_height = dv_timings.bt.height;
|
||||
detected_signal = true;
|
||||
video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
|
||||
pthread_mutex_lock(&streaming_mutex);
|
||||
if (streaming_flag == true)
|
||||
{
|
||||
pthread_mutex_unlock(&streaming_mutex);
|
||||
log_info("restarting on going video streaming");
|
||||
video_stop_streaming();
|
||||
video_start_streaming();
|
||||
}
|
||||
else
|
||||
{
|
||||
pthread_mutex_unlock(&streaming_mutex);
|
||||
|
||||
if (should_restart) {
|
||||
log_info("restarting video streaming due to format change");
|
||||
video_restart_streaming();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -731,21 +831,9 @@ void video_set_quality_factor(float factor)
|
|||
quality_factor = factor;
|
||||
|
||||
// TODO: update venc bitrate without stopping streaming
|
||||
|
||||
pthread_mutex_lock(&streaming_mutex);
|
||||
if (streaming_flag == true)
|
||||
{
|
||||
pthread_mutex_unlock(&streaming_mutex);
|
||||
log_info("restarting on going video streaming due to quality factor change");
|
||||
video_stop_streaming();
|
||||
video_start_streaming();
|
||||
}
|
||||
else
|
||||
{
|
||||
pthread_mutex_unlock(&streaming_mutex);
|
||||
}
|
||||
video_restart_streaming();
|
||||
}
|
||||
|
||||
float video_get_quality_factor() {
|
||||
return quality_factor;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*
|
||||
* @return int 0 on success, -1 on failure
|
||||
*/
|
||||
int video_init();
|
||||
int video_init(float quality_factor);
|
||||
|
||||
/**
|
||||
* @brief Shutdown the video subsystem
|
||||
|
|
|
|||
|
|
@ -129,11 +129,13 @@ func uiTick() {
|
|||
C.jetkvm_ui_tick()
|
||||
}
|
||||
|
||||
func videoInit() error {
|
||||
func videoInit(factor float64) error {
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
ret := C.jetkvm_video_init()
|
||||
factorC := C.float(factor)
|
||||
|
||||
ret := C.jetkvm_video_init(factorC)
|
||||
if ret != 0 {
|
||||
return fmt.Errorf("failed to initialize video: %d", ret)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ type Native struct {
|
|||
systemVersion *semver.Version
|
||||
appVersion *semver.Version
|
||||
displayRotation uint16
|
||||
defaultQualityFactor float64
|
||||
onVideoStateChange func(state VideoState)
|
||||
onVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
onIndevEvent func(event string)
|
||||
|
|
@ -22,12 +23,14 @@ type Native struct {
|
|||
sleepModeSupported bool
|
||||
videoLock sync.Mutex
|
||||
screenLock sync.Mutex
|
||||
extraLock sync.Mutex
|
||||
}
|
||||
|
||||
type NativeOptions struct {
|
||||
SystemVersion *semver.Version
|
||||
AppVersion *semver.Version
|
||||
DisplayRotation uint16
|
||||
DefaultQualityFactor float64
|
||||
OnVideoStateChange func(state VideoState)
|
||||
OnVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
OnIndevEvent func(event string)
|
||||
|
|
@ -65,6 +68,11 @@ func NewNative(opts NativeOptions) *Native {
|
|||
|
||||
sleepModeSupported := isSleepModeSupported()
|
||||
|
||||
defaultQualityFactor := opts.DefaultQualityFactor
|
||||
if defaultQualityFactor < 0 || defaultQualityFactor > 1 {
|
||||
defaultQualityFactor = 1.0
|
||||
}
|
||||
|
||||
return &Native{
|
||||
ready: make(chan struct{}),
|
||||
l: nativeLogger,
|
||||
|
|
@ -72,6 +80,7 @@ func NewNative(opts NativeOptions) *Native {
|
|||
systemVersion: opts.SystemVersion,
|
||||
appVersion: opts.AppVersion,
|
||||
displayRotation: opts.DisplayRotation,
|
||||
defaultQualityFactor: defaultQualityFactor,
|
||||
onVideoStateChange: onVideoStateChange,
|
||||
onVideoFrameReceived: onVideoFrameReceived,
|
||||
onIndevEvent: onIndevEvent,
|
||||
|
|
@ -97,7 +106,7 @@ func (n *Native) Start() {
|
|||
n.initUI()
|
||||
go n.tickUI()
|
||||
|
||||
if err := videoInit(); err != nil {
|
||||
if err := videoInit(n.defaultQualityFactor); err != nil {
|
||||
n.l.Error().Err(err).Msg("failed to initialize video")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
|
||||
|
||||
// DefaultEDID is the default EDID for the video stream.
|
||||
const DefaultEDID = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
|
||||
|
||||
var extraLockTimeout = 5 * time.Second
|
||||
|
||||
// VideoState is the state of the video stream.
|
||||
type VideoState struct {
|
||||
Ready bool `json:"ready"`
|
||||
|
|
@ -66,12 +73,32 @@ func (n *Native) VideoSleepModeSupported() bool {
|
|||
return n.sleepModeSupported
|
||||
}
|
||||
|
||||
// useExtraLock uses the extra lock to execute a function.
|
||||
// if the lock is currently held by another goroutine, returns an error.
|
||||
//
|
||||
// it's used to ensure that only one change is made to the video stream at a time.
|
||||
// as the change usually requires to restart video streaming
|
||||
// TODO: check video streaming status instead of using a hardcoded timeout
|
||||
func (n *Native) useExtraLock(fn func() error) error {
|
||||
if !n.extraLock.TryLock() {
|
||||
return fmt.Errorf("the previous change hasn't been completed yet")
|
||||
}
|
||||
err := fn()
|
||||
if err == nil {
|
||||
time.Sleep(extraLockTimeout)
|
||||
}
|
||||
n.extraLock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// VideoSetQualityFactor sets the quality factor for the video stream.
|
||||
func (n *Native) VideoSetQualityFactor(factor float64) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
return videoSetStreamQualityFactor(factor)
|
||||
return n.useExtraLock(func() error {
|
||||
return videoSetStreamQualityFactor(factor)
|
||||
})
|
||||
}
|
||||
|
||||
// VideoGetQualityFactor gets the quality factor for the video stream.
|
||||
|
|
@ -87,7 +114,13 @@ func (n *Native) VideoSetEDID(edid string) error {
|
|||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
return videoSetEDID(edid)
|
||||
if edid == "" {
|
||||
edid = DefaultEDID
|
||||
}
|
||||
|
||||
return n.useExtraLock(func() error {
|
||||
return videoSetEDID(edid)
|
||||
})
|
||||
}
|
||||
|
||||
// VideoGetEDID gets the EDID for the video stream.
|
||||
|
|
|
|||
|
|
@ -366,7 +366,6 @@ func rpcGetEDID() (string, error) {
|
|||
func rpcSetEDID(edid string) error {
|
||||
if edid == "" {
|
||||
logger.Info().Msg("Restoring EDID to default")
|
||||
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
|
||||
} else {
|
||||
logger.Info().Str("edid", edid).Msg("Setting EDID")
|
||||
}
|
||||
|
|
|
|||
13
native.go
13
native.go
|
|
@ -17,9 +17,10 @@ var (
|
|||
|
||||
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
||||
nativeInstance = native.NewNative(native.NativeOptions{
|
||||
SystemVersion: systemVersion,
|
||||
AppVersion: appVersion,
|
||||
DisplayRotation: config.GetDisplayRotation(),
|
||||
SystemVersion: systemVersion,
|
||||
AppVersion: appVersion,
|
||||
DisplayRotation: config.GetDisplayRotation(),
|
||||
DefaultQualityFactor: config.VideoQualityFactor,
|
||||
OnVideoStateChange: func(state native.VideoState) {
|
||||
lastVideoState = state
|
||||
triggerVideoStateUpdate()
|
||||
|
|
@ -70,7 +71,13 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
|||
})
|
||||
},
|
||||
})
|
||||
|
||||
nativeInstance.Start()
|
||||
go func() {
|
||||
if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil {
|
||||
nativeLogger.Warn().Err(err).Msg("error setting EDID")
|
||||
}
|
||||
}()
|
||||
|
||||
if os.Getenv("JETKVM_CRASH_TESTING") == "1" {
|
||||
nativeInstance.DoNotUseThisIsForCrashTestingOnly()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { Permission } from "@/types/permissions";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
|
||||
import notifications from "../notifications";
|
||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||
|
|
@ -18,6 +20,7 @@ export default function SettingsHardwareRoute() {
|
|||
const settings = useSettingsStore();
|
||||
const { setDisplayRotation } = useSettingsStore();
|
||||
const { hasPermission, isLoading, permissions } = usePermissions();
|
||||
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
|
||||
|
||||
const handleDisplayRotationChange = (rotation: string) => {
|
||||
setDisplayRotation(rotation);
|
||||
|
|
@ -61,7 +64,21 @@ export default function SettingsHardwareRoute() {
|
|||
});
|
||||
};
|
||||
|
||||
// Check permissions before fetching settings data
|
||||
const handlePowerSavingChange = (enabled: boolean) => {
|
||||
setPowerSavingEnabled(enabled);
|
||||
const duration = enabled ? 90 : -1;
|
||||
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set power saving mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setPowerSavingEnabled(!enabled); // Revert on error
|
||||
return;
|
||||
}
|
||||
notifications.success(`Power saving mode ${enabled ? "enabled" : "disabled"}`);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch settings if user has permission
|
||||
if (!isLoading && permissions[Permission.SETTINGS_READ] === true) {
|
||||
|
|
@ -95,6 +112,17 @@ export default function SettingsHardwareRoute() {
|
|||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
send("getVideoSleepMode", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to get power saving mode:", resp.error);
|
||||
return;
|
||||
}
|
||||
const result = resp.result as { enabled: boolean; duration: number };
|
||||
setPowerSavingEnabled(result.duration >= 0);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
|
|
@ -192,6 +220,26 @@ export default function SettingsHardwareRoute() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<FeatureFlag minAppVersion="0.4.9">
|
||||
<div className="space-y-4">
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
<SettingsSectionHeader
|
||||
title="Power Saving"
|
||||
description="Reduce power consumption when not in use"
|
||||
/>
|
||||
<SettingsItem
|
||||
badge="Experimental"
|
||||
title="HDMI Sleep Mode"
|
||||
description="Turn off capture after 90 seconds of inactivity"
|
||||
>
|
||||
<Checkbox
|
||||
checked={powerSavingEnabled}
|
||||
onChange={(e) => handlePowerSavingChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</FeatureFlag>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbDeviceSetting />
|
||||
</FeatureFlag>
|
||||
|
|
|
|||
Loading…
Reference in New Issue