From 7b00f931e25c489bdc5236218811eb2d46dc960b Mon Sep 17 00:00:00 2001 From: Siyuan Date: Wed, 15 Oct 2025 16:43:09 +0000 Subject: [PATCH 01/10] 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 22fa378b6..203cef139 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 02/10] fix: streaming mutex --- internal/native/cgo/video.c | 120 +++++++++++++++++++++--------------- internal/native/video.go | 7 +++ jsonrpc.go | 1 - native.go | 2 + 4 files changed, 79 insertions(+), 51 deletions(-) diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index 203cef139..97caaea6d 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) + if (streaming_thread == NULL) { + log_info("video streaming already stopped"); + return; + } + + 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) { - 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"); + log_info("restarting video streaming"); + video_stop_streaming(); } - pthread_mutex_unlock(&streaming_mutex); + 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 d50087564..bc5ebaf82 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 d2d3f4014..2c06f12b8 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 4268bf2cc..a703bb5ae 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 03/10] fix: return value not checked --- native.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/native.go b/native.go index a703bb5ae..39c1fa0ae 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 04/10] 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 9475f4fe9..58578e4d5 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)} + /> + +
+ + From 5ef7722d574bd398ede074d5e31d42a299d5095a Mon Sep 17 00:00:00 2001 From: Siyuan Date: Thu, 16 Oct 2025 07:57:04 +0000 Subject: [PATCH 05/10] feat: add video quality factor config --- config.go | 1 + internal/native/cgo/ctrl.c | 4 ++-- internal/native/cgo/ctrl.h | 2 +- internal/native/cgo/video.c | 7 ++++++- internal/native/cgo/video.h | 2 +- internal/native/cgo_linux.go | 6 ++++-- internal/native/native.go | 11 ++++++++++- internal/native/video.go | 30 ++++++++++++++++++++++++++++-- native.go | 17 +++++++++++------ 9 files changed, 64 insertions(+), 16 deletions(-) diff --git a/config.go b/config.go index 36df92daa..26f54a45b 100644 --- a/config.go +++ b/config.go @@ -106,6 +106,7 @@ type Config struct { NetworkConfig *types.NetworkConfig `json:"network_config"` DefaultLogLevel string `json:"default_log_level"` VideoSleepAfterSec int `json:"video_sleep_after_sec"` + VideoQualityFactor float64 `json:"video_quality_factor"` } func (c *Config) GetDisplayRotation() uint16 { diff --git a/internal/native/cgo/ctrl.c b/internal/native/cgo/ctrl.c index dd2858595..0c10ee15a 100644 --- a/internal/native/cgo/ctrl.c +++ b/internal/native/cgo/ctrl.c @@ -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() { diff --git a/internal/native/cgo/ctrl.h b/internal/native/cgo/ctrl.h index 430e4c21f..774ee1473 100644 --- a/internal/native/cgo/ctrl.h +++ b/internal/native/cgo/ctrl.h @@ -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(); diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index 97caaea6d..1c88adcf7 100644 --- a/internal/native/cgo/video.c +++ b/internal/native/cgo/video.c @@ -220,10 +220,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"); diff --git a/internal/native/cgo/video.h b/internal/native/cgo/video.h index e9309be45..6fa00ca42 100644 --- a/internal/native/cgo/video.h +++ b/internal/native/cgo/video.h @@ -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 diff --git a/internal/native/cgo_linux.go b/internal/native/cgo_linux.go index 8cd6d4898..850da0e8e 100644 --- a/internal/native/cgo_linux.go +++ b/internal/native/cgo_linux.go @@ -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) } diff --git a/internal/native/native.go b/internal/native/native.go index b89b37a37..2a9055cea 100644 --- a/internal/native/native.go +++ b/internal/native/native.go @@ -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") } diff --git a/internal/native/video.go b/internal/native/video.go index bc5ebaf82..c556a9383 100644 --- a/internal/native/video.go +++ b/internal/native/video.go @@ -1,7 +1,9 @@ package native import ( + "fmt" "os" + "time" ) const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode" @@ -9,6 +11,8 @@ const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mod // 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"` @@ -69,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. @@ -94,7 +118,9 @@ func (n *Native) VideoSetEDID(edid string) error { edid = DefaultEDID } - return videoSetEDID(edid) + return n.useExtraLock(func() error { + return videoSetEDID(edid) + }) } // VideoGetEDID gets the EDID for the video stream. diff --git a/native.go b/native.go index 39c1fa0ae..ac393577b 100644 --- a/native.go +++ b/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() @@ -60,9 +61,13 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) { }) nativeInstance.Start() - if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil { - nativeLogger.Warn().Err(err).Msg("error setting EDID") - } + go func() { + for { + 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 38a987469fba6f78a4a704f0d05a695fe6afe6b9 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Thu, 16 Oct 2025 15:41:48 +0000 Subject: [PATCH 06/10] chore: add logging when disabling HDMI sleep mode --- internal/native/cgo/video.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index 1c88adcf7..2a4a03404 100644 --- a/internal/native/cgo/video.c +++ b/internal/native/cgo/video.c @@ -66,9 +66,20 @@ static void ensure_sleep_mode_disabled() 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() From 0f4e073a45cf1eebbf53c82289105e0508ced7f0 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Fri, 17 Oct 2025 11:14:53 +0000 Subject: [PATCH 07/10] Fix VIDIOC_S_FMT EBUSY error on video stream restart V4L2 buffers were not explicitly freed before closing the device, causing VIDIOC_S_FMT to fail with EBUSY on restart. Added VIDIOC_REQBUFS(count=0) call in cleanup to free buffer queue synchronously before close(). This ensures subsequent opens can set format immediately without EBUSY errors, eliminating 100ms+ retry delays. --- internal/native/cgo/video.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index 2a4a03404..e6add58f6 100644 --- a/internal/native/cgo/video.c +++ b/internal/native/cgo/video.c @@ -606,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++) @@ -619,7 +631,7 @@ 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; @@ -648,7 +660,7 @@ void video_shutdown() RK_MPI_MB_DestroyPool(memPool); } log_info("Destroyed memory pool"); - + pthread_mutex_destroy(&streaming_mutex); log_info("Destroyed streaming mutex"); } @@ -665,14 +677,14 @@ void video_start_streaming() log_warn("video streaming already started"); return; } - + pthread_t *new_thread = malloc(sizeof(pthread_t)); if (new_thread == NULL) { log_error("Failed to allocate memory for streaming thread"); return; } - + set_streaming_flag(true); int result = pthread_create(new_thread, NULL, run_video_stream, NULL); if (result != 0) @@ -682,7 +694,7 @@ void video_start_streaming() free(new_thread); return; } - + // Only set streaming_thread after successful creation streaming_thread = new_thread; } @@ -693,7 +705,7 @@ void video_stop_streaming() log_info("video streaming already stopped"); return; } - + log_info("stopping video streaming"); set_streaming_flag(false); @@ -711,7 +723,7 @@ void video_stop_streaming() free(streaming_thread); streaming_thread = NULL; - log_info("video streaming stopped"); + log_info("video streaming stopped"); } void video_restart_streaming() @@ -818,4 +830,4 @@ void video_set_quality_factor(float factor) float video_get_quality_factor() { return quality_factor; -} \ No newline at end of file +} From 8d5505eb4a13975dce808bad5ce2cd6d259de787 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Fri, 17 Oct 2025 14:52:38 +0000 Subject: [PATCH 08/10] fix: why set EDID forever --- native.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/native.go b/native.go index ac393577b..5f26c0145 100644 --- a/native.go +++ b/native.go @@ -62,10 +62,8 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) { nativeInstance.Start() go func() { - for { - if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil { - nativeLogger.Warn().Err(err).Msg("error setting EDID") - } + if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil { + nativeLogger.Warn().Err(err).Msg("error setting EDID") } }() From da6f0337eee40cab09478c11577984d77c40f990 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Fri, 17 Oct 2025 14:53:44 +0000 Subject: [PATCH 09/10] chore: do not restart video streaming if the format is the same --- internal/native/cgo/video.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index e6add58f6..917e91637 100644 --- a/internal/native/cgo/video.c +++ b/internal/native/cgo/video.c @@ -793,12 +793,18 @@ 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); - video_restart_streaming(); + if (should_restart) { + log_info("restarting video streaming due to format change"); + video_restart_streaming(); + } } memset(&ev, 0, sizeof(ev)); From c50d8f979ff71954ebe8fd09f6cf177014677460 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Fri, 17 Oct 2025 15:48:03 +0000 Subject: [PATCH 10/10] fix: update power saving mode condition to include zero duration --- ui/src/routes/devices.$id.settings.hardware.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx index 58578e4d5..dd3ba2ed1 100644 --- a/ui/src/routes/devices.$id.settings.hardware.tsx +++ b/ui/src/routes/devices.$id.settings.hardware.tsx @@ -95,7 +95,7 @@ export default function SettingsHardwareRoute() { return; } const result = resp.result as { enabled: boolean; duration: number }; - setPowerSavingEnabled(result.duration > 0); + setPowerSavingEnabled(result.duration >= 0); }); }, [send]);