From 7b00f931e25c489bdc5236218811eb2d46dc960b Mon Sep 17 00:00:00 2001 From: Siyuan Date: Wed, 15 Oct 2025 16:43:09 +0000 Subject: [PATCH 1/4] chore: disable sleep mode when detecting video format --- internal/native/cgo/video.c | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index 22fa378b..203cef13 100644 --- a/internal/native/cgo/video.c +++ b/internal/native/cgo/video.c @@ -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,34 @@ 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; + } + write(fd, "0", 1); + close(fd); + +} + +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; @@ -192,6 +222,8 @@ pthread_t *format_thread = NULL; int video_init() { + detect_sleep_mode(); + if (RK_MPI_SYS_Init() != RK_SUCCESS) { log_error("RK_MPI_SYS_Init failed"); @@ -401,7 +433,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); @@ -650,6 +682,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) { From dcb8c7982036e99cad875a09b98383a3f9e99576 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Wed, 15 Oct 2025 18:14:04 +0000 Subject: [PATCH 2/4] fix: streaming mutex --- internal/native/cgo/video.c | 122 +++++++++++++++++++++--------------- internal/native/video.go | 7 +++ jsonrpc.go | 1 - native.go | 2 + 4 files changed, 80 insertions(+), 52 deletions(-) diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index 203cef13..97caaea6 100644 --- a/internal/native/cgo/video.c +++ b/internal/native/cgo/video.c @@ -333,11 +333,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"); @@ -351,6 +369,8 @@ void *run_video_stream(void *arg) log_info("running video stream"); + streaming_stopped = false; + while (streaming_flag) { if (detected_signal == false) @@ -583,8 +603,11 @@ void *run_video_stream(void *arg) log_info("closing video capture device %s", VIDEO_DEV); close(video_dev_fd); } - + log_info("video stream thread exiting"); + + streaming_stopped = true; + return NULL; } @@ -614,56 +637,75 @@ void video_shutdown() 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) @@ -727,18 +769,8 @@ void *run_detect_format(void *arg) 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); - } + + video_restart_streaming(); } memset(&ev, 0, sizeof(ev)); @@ -765,19 +797,7 @@ 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() { diff --git a/internal/native/video.go b/internal/native/video.go index d5008756..bc5ebaf8 100644 --- a/internal/native/video.go +++ b/internal/native/video.go @@ -6,6 +6,9 @@ import ( 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" + // VideoState is the state of the video stream. type VideoState struct { Ready bool `json:"ready"` @@ -87,6 +90,10 @@ func (n *Native) VideoSetEDID(edid string) error { n.videoLock.Lock() defer n.videoLock.Unlock() + if edid == "" { + edid = DefaultEDID + } + return videoSetEDID(edid) } diff --git a/jsonrpc.go b/jsonrpc.go index d2d3f401..2c06f12b 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -243,7 +243,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") } diff --git a/native.go b/native.go index 4268bf2c..a703bb5a 100644 --- a/native.go +++ b/native.go @@ -58,7 +58,9 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) { } }, }) + nativeInstance.Start() + nativeInstance.VideoSetEDID(config.EdidString) if os.Getenv("JETKVM_CRASH_TESTING") == "1" { nativeInstance.DoNotUseThisIsForCrashTestingOnly() From cf781262f631f02d78614e299836f0c515c64362 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Wed, 15 Oct 2025 18:18:14 +0000 Subject: [PATCH 3/4] fix: return value not checked --- native.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/native.go b/native.go index a703bb5a..39c1fa0a 100644 --- a/native.go +++ b/native.go @@ -60,7 +60,9 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) { }) nativeInstance.Start() - nativeInstance.VideoSetEDID(config.EdidString) + 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() From b844a8a3e857b382bc08ab670c9dfabf598c74ba Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 16 Oct 2025 16:48:35 +0200 Subject: [PATCH 4/4] feat: add power saving mode toggle in hardware settings Implemented a new feature to enable or disable HDMI sleep mode, allowing users to reduce power consumption when the device is not in use. Added state management for the power saving setting and integrated it with the existing settings page. --- .../routes/devices.$id.settings.hardware.tsx | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx index 9475f4fe..58578e4d 100644 --- a/ui/src/routes/devices.$id.settings.hardware.tsx +++ b/ui/src/routes/devices.$id.settings.hardware.tsx @@ -1,11 +1,13 @@ -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 { Checkbox } from "@components/Checkbox"; import notifications from "../notifications"; import { UsbInfoSetting } from "../components/UsbInfoSetting"; @@ -15,6 +17,7 @@ export default function SettingsHardwareRoute() { const { send } = useJsonRpc(); const settings = useSettingsStore(); const { setDisplayRotation } = useSettingsStore(); + const [powerSavingEnabled, setPowerSavingEnabled] = useState(false); const handleDisplayRotationChange = (rotation: string) => { 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(() => { send("getBacklightSettings", {}, (resp: JsonRpcResponse) => { if ("error" in resp) { @@ -70,6 +88,17 @@ export default function SettingsHardwareRoute() { }); }, [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 (
+ +
+
+ + + handlePowerSavingChange(e.target.checked)} + /> + +
+ +