diff --git a/config.go b/config.go index 36df92da..26f54a45 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 dd285859..0c10ee15 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 430e4c21..774ee147 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 22fa378b..917e9163 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,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; -} \ No newline at end of file +} diff --git a/internal/native/cgo/video.h b/internal/native/cgo/video.h index e9309be4..6fa00ca4 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 8cd6d489..850da0e8 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 b89b37a3..2a9055ce 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 d5008756..c556a938 100644 --- a/internal/native/video.go +++ b/internal/native/video.go @@ -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. 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..5f26c014 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() @@ -58,7 +59,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() diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx index 9475f4fe..dd3ba2ed 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)} + /> + +
+ +