chore: disable sleep mode when detecting video format (#887)

Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
Co-authored-by: Adam Shiervani <adamshiervani@fastmail.com>
This commit is contained in:
Aveline 2025-10-17 17:51:02 +02:00 committed by GitHub
parent 74e64f69a7
commit 2444817455
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 258 additions and 70 deletions

View File

@ -106,6 +106,7 @@ type Config struct {
NetworkConfig *types.NetworkConfig `json:"network_config"` NetworkConfig *types.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"` DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"` VideoSleepAfterSec int `json:"video_sleep_after_sec"`
VideoQualityFactor float64 `json:"video_quality_factor"`
} }
func (c *Config) GetDisplayRotation() uint16 { func (c *Config) GetDisplayRotation() uint16 {

View File

@ -405,8 +405,8 @@ char *jetkvm_video_log_status() {
return (char *)videoc_log_status(); return (char *)videoc_log_status();
} }
int jetkvm_video_init() { int jetkvm_video_init(float factor) {
return video_init(); return video_init(factor);
} }
void jetkvm_video_shutdown() { void jetkvm_video_shutdown() {

View File

@ -52,7 +52,7 @@ const char *jetkvm_ui_get_lvgl_version();
const char *jetkvm_ui_event_code_to_name(int code); 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_shutdown();
void jetkvm_video_start(); void jetkvm_video_start();
void jetkvm_video_stop(); void jetkvm_video_stop();

View File

@ -29,6 +29,7 @@
#define VIDEO_DEV "/dev/video0" #define VIDEO_DEV "/dev/video0"
#define SUB_DEV "/dev/v4l-subdev2" #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(x, a) (((x) + (a)-1) & ~((a)-1))
#define RK_ALIGN_2(x) RK_ALIGN(x, 2) #define RK_ALIGN_2(x) RK_ALIGN(x, 2)
@ -39,6 +40,7 @@ int sub_dev_fd = -1;
#define VENC_CHANNEL 0 #define VENC_CHANNEL 0
MB_POOL memPool = MB_INVALID_POOLID; MB_POOL memPool = MB_INVALID_POOLID;
bool sleep_mode_available = false;
bool should_exit = false; bool should_exit = false;
float quality_factor = 1.0f; 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 */ 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) double calculate_bitrate(float bitrate_factor, int width, int height)
{ {
const int32_t base_bitrate_high = 2000; const int32_t base_bitrate_high = 2000;
@ -190,8 +231,15 @@ static int32_t buf_init()
pthread_t *format_thread = NULL; 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) if (RK_MPI_SYS_Init() != RK_SUCCESS)
{ {
log_error("RK_MPI_SYS_Init failed"); log_error("RK_MPI_SYS_Init failed");
@ -301,11 +349,29 @@ static void *venc_read_stream(void *arg)
} }
uint32_t detected_width, detected_height; 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_t *streaming_thread = NULL;
pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER; 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) void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename)
{ {
FILE *file = fopen(filename, "wb"); FILE *file = fopen(filename, "wb");
@ -319,6 +385,8 @@ void *run_video_stream(void *arg)
log_info("running video stream"); log_info("running video stream");
streaming_stopped = false;
while (streaming_flag) while (streaming_flag)
{ {
if (detected_signal == false) if (detected_signal == false)
@ -401,7 +469,7 @@ void *run_video_stream(void *arg)
{ {
log_error("get mb blk failed!"); log_error("get mb blk failed!");
close(video_dev_fd); close(video_dev_fd);
return ; return (void *)errno;
} }
log_info("Got memory block for buffer %d", i); 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)); 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(); venc_stop();
for (int i = 0; i < input_buffer_count; i++) 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"); log_info("video stream thread exiting");
streaming_stopped = true;
return NULL; return NULL;
} }
@ -577,61 +660,80 @@ void video_shutdown()
RK_MPI_MB_DestroyPool(memPool); RK_MPI_MB_DestroyPool(memPool);
} }
log_info("Destroyed memory pool"); log_info("Destroyed memory pool");
pthread_mutex_destroy(&streaming_mutex); pthread_mutex_destroy(&streaming_mutex);
log_info("Destroyed streaming mutex"); log_info("Destroyed streaming mutex");
} }
void video_start_streaming() void video_start_streaming()
{ {
pthread_mutex_lock(&streaming_mutex); log_info("starting video streaming");
if (streaming_thread != NULL) 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"); log_warn("video streaming already started");
goto cleanup; return;
} }
pthread_t *new_thread = malloc(sizeof(pthread_t)); pthread_t *new_thread = malloc(sizeof(pthread_t));
if (new_thread == NULL) if (new_thread == NULL)
{ {
log_error("Failed to allocate memory for streaming thread"); 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); int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
if (result != 0) if (result != 0)
{ {
log_error("Failed to create streaming thread: %s", strerror(result)); log_error("Failed to create streaming thread: %s", strerror(result));
streaming_flag = false; set_streaming_flag(false);
free(new_thread); 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; streaming_thread = new_thread;
cleanup:
pthread_mutex_unlock(&streaming_mutex);
return;
} }
void video_stop_streaming() void video_stop_streaming()
{ {
pthread_mutex_lock(&streaming_mutex); if (streaming_thread == NULL) {
if (streaming_thread != NULL) log_info("video streaming already stopped");
{ return;
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");
} }
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) void *run_detect_format(void *arg)
@ -650,6 +752,8 @@ void *run_detect_format(void *arg)
while (!should_exit) while (!should_exit)
{ {
ensure_sleep_mode_disabled();
memset(&dv_timings, 0, sizeof(dv_timings)); memset(&dv_timings, 0, sizeof(dv_timings));
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0) 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.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
dv_timings.bt.hbackporch)); dv_timings.bt.hbackporch));
log_info("Frames per second: %.2f fps", frames_per_second); 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_width = dv_timings.bt.width;
detected_height = dv_timings.bt.height; detected_height = dv_timings.bt.height;
detected_signal = true; detected_signal = true;
video_report_format(true, NULL, detected_width, detected_height, frames_per_second); video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
pthread_mutex_lock(&streaming_mutex);
if (streaming_flag == true) if (should_restart) {
{ log_info("restarting video streaming due to format change");
pthread_mutex_unlock(&streaming_mutex); video_restart_streaming();
log_info("restarting on going video streaming");
video_stop_streaming();
video_start_streaming();
}
else
{
pthread_mutex_unlock(&streaming_mutex);
} }
} }
@ -731,21 +831,9 @@ void video_set_quality_factor(float factor)
quality_factor = factor; quality_factor = factor;
// TODO: update venc bitrate without stopping streaming // TODO: update venc bitrate without stopping streaming
video_restart_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);
}
} }
float video_get_quality_factor() { float video_get_quality_factor() {
return quality_factor; return quality_factor;
} }

View File

@ -6,7 +6,7 @@
* *
* @return int 0 on success, -1 on failure * @return int 0 on success, -1 on failure
*/ */
int video_init(); int video_init(float quality_factor);
/** /**
* @brief Shutdown the video subsystem * @brief Shutdown the video subsystem

View File

@ -129,11 +129,13 @@ func uiTick() {
C.jetkvm_ui_tick() C.jetkvm_ui_tick()
} }
func videoInit() error { func videoInit(factor float64) error {
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
ret := C.jetkvm_video_init() factorC := C.float(factor)
ret := C.jetkvm_video_init(factorC)
if ret != 0 { if ret != 0 {
return fmt.Errorf("failed to initialize video: %d", ret) return fmt.Errorf("failed to initialize video: %d", ret)
} }

View File

@ -15,6 +15,7 @@ type Native struct {
systemVersion *semver.Version systemVersion *semver.Version
appVersion *semver.Version appVersion *semver.Version
displayRotation uint16 displayRotation uint16
defaultQualityFactor float64
onVideoStateChange func(state VideoState) onVideoStateChange func(state VideoState)
onVideoFrameReceived func(frame []byte, duration time.Duration) onVideoFrameReceived func(frame []byte, duration time.Duration)
onIndevEvent func(event string) onIndevEvent func(event string)
@ -22,12 +23,14 @@ type Native struct {
sleepModeSupported bool sleepModeSupported bool
videoLock sync.Mutex videoLock sync.Mutex
screenLock sync.Mutex screenLock sync.Mutex
extraLock sync.Mutex
} }
type NativeOptions struct { type NativeOptions struct {
SystemVersion *semver.Version SystemVersion *semver.Version
AppVersion *semver.Version AppVersion *semver.Version
DisplayRotation uint16 DisplayRotation uint16
DefaultQualityFactor float64
OnVideoStateChange func(state VideoState) OnVideoStateChange func(state VideoState)
OnVideoFrameReceived func(frame []byte, duration time.Duration) OnVideoFrameReceived func(frame []byte, duration time.Duration)
OnIndevEvent func(event string) OnIndevEvent func(event string)
@ -65,6 +68,11 @@ func NewNative(opts NativeOptions) *Native {
sleepModeSupported := isSleepModeSupported() sleepModeSupported := isSleepModeSupported()
defaultQualityFactor := opts.DefaultQualityFactor
if defaultQualityFactor < 0 || defaultQualityFactor > 1 {
defaultQualityFactor = 1.0
}
return &Native{ return &Native{
ready: make(chan struct{}), ready: make(chan struct{}),
l: nativeLogger, l: nativeLogger,
@ -72,6 +80,7 @@ func NewNative(opts NativeOptions) *Native {
systemVersion: opts.SystemVersion, systemVersion: opts.SystemVersion,
appVersion: opts.AppVersion, appVersion: opts.AppVersion,
displayRotation: opts.DisplayRotation, displayRotation: opts.DisplayRotation,
defaultQualityFactor: defaultQualityFactor,
onVideoStateChange: onVideoStateChange, onVideoStateChange: onVideoStateChange,
onVideoFrameReceived: onVideoFrameReceived, onVideoFrameReceived: onVideoFrameReceived,
onIndevEvent: onIndevEvent, onIndevEvent: onIndevEvent,
@ -97,7 +106,7 @@ func (n *Native) Start() {
n.initUI() n.initUI()
go n.tickUI() 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") n.l.Error().Err(err).Msg("failed to initialize video")
} }

View File

@ -1,11 +1,18 @@
package native package native
import ( import (
"fmt"
"os" "os"
"time"
) )
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode" 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. // VideoState is the state of the video stream.
type VideoState struct { type VideoState struct {
Ready bool `json:"ready"` Ready bool `json:"ready"`
@ -66,12 +73,32 @@ func (n *Native) VideoSleepModeSupported() bool {
return n.sleepModeSupported 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. // VideoSetQualityFactor sets the quality factor for the video stream.
func (n *Native) VideoSetQualityFactor(factor float64) error { func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
return videoSetStreamQualityFactor(factor) return n.useExtraLock(func() error {
return videoSetStreamQualityFactor(factor)
})
} }
// VideoGetQualityFactor gets the quality factor for the video stream. // VideoGetQualityFactor gets the quality factor for the video stream.
@ -87,7 +114,13 @@ func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() 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. // VideoGetEDID gets the EDID for the video stream.

View File

@ -243,7 +243,6 @@ func rpcGetEDID() (string, error) {
func rpcSetEDID(edid string) error { func rpcSetEDID(edid string) error {
if edid == "" { if edid == "" {
logger.Info().Msg("Restoring EDID to default") logger.Info().Msg("Restoring EDID to default")
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
} else { } else {
logger.Info().Str("edid", edid).Msg("Setting EDID") logger.Info().Str("edid", edid).Msg("Setting EDID")
} }

View File

@ -17,9 +17,10 @@ var (
func initNative(systemVersion *semver.Version, appVersion *semver.Version) { func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeInstance = native.NewNative(native.NativeOptions{ nativeInstance = native.NewNative(native.NativeOptions{
SystemVersion: systemVersion, SystemVersion: systemVersion,
AppVersion: appVersion, AppVersion: appVersion,
DisplayRotation: config.GetDisplayRotation(), DisplayRotation: config.GetDisplayRotation(),
DefaultQualityFactor: config.VideoQualityFactor,
OnVideoStateChange: func(state native.VideoState) { OnVideoStateChange: func(state native.VideoState) {
lastVideoState = state lastVideoState = state
triggerVideoStateUpdate() triggerVideoStateUpdate()
@ -58,7 +59,13 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
} }
}, },
}) })
nativeInstance.Start() 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" { if os.Getenv("JETKVM_CRASH_TESTING") == "1" {
nativeInstance.DoNotUseThisIsForCrashTestingOnly() nativeInstance.DoNotUseThisIsForCrashTestingOnly()

View File

@ -1,11 +1,13 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { BacklightSettings, useSettingsStore } from "@/hooks/stores"; import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting"; import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
import { Checkbox } from "@components/Checkbox";
import notifications from "../notifications"; import notifications from "../notifications";
import { UsbInfoSetting } from "../components/UsbInfoSetting"; import { UsbInfoSetting } from "../components/UsbInfoSetting";
@ -15,6 +17,7 @@ export default function SettingsHardwareRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const settings = useSettingsStore(); const settings = useSettingsStore();
const { setDisplayRotation } = useSettingsStore(); const { setDisplayRotation } = useSettingsStore();
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
const handleDisplayRotationChange = (rotation: string) => { const handleDisplayRotationChange = (rotation: string) => {
setDisplayRotation(rotation); setDisplayRotation(rotation);
@ -58,6 +61,21 @@ export default function SettingsHardwareRoute() {
}); });
}; };
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(() => { useEffect(() => {
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => { send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
@ -70,6 +88,17 @@ export default function SettingsHardwareRoute() {
}); });
}, [send, setBacklightSettings]); }, [send, setBacklightSettings]);
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 ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
@ -167,6 +196,26 @@ export default function SettingsHardwareRoute() {
</p> </p>
</div> </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"> <FeatureFlag minAppVersion="0.3.8">
<UsbDeviceSetting /> <UsbDeviceSetting />
</FeatureFlag> </FeatureFlag>