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:
Alex P 2025-10-17 23:34:44 +03:00
commit 8dc013d8fe
11 changed files with 258 additions and 71 deletions

View File

@ -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 {

View File

@ -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() {

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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.

View File

@ -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")
}

View File

@ -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()

View File

@ -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>