Compare commits

..

6 Commits

Author SHA1 Message Date
Adam Shiervani c50d8f979f fix: update power saving mode condition to include zero duration 2025-10-17 15:48:03 +00:00
Siyuan da6f0337ee chore: do not restart video streaming if the format is the same 2025-10-17 14:55:43 +00:00
Siyuan 8d5505eb4a fix: why set EDID forever 2025-10-17 14:55:43 +00:00
Adam Shiervani 0f4e073a45 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.
2025-10-17 14:55:43 +00:00
Siyuan 38a987469f chore: add logging when disabling HDMI sleep mode 2025-10-17 14:55:43 +00:00
Siyuan 5ef7722d57 feat: add video quality factor config 2025-10-17 14:55:43 +00:00
10 changed files with 101 additions and 26 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

@ -66,9 +66,20 @@ static void ensure_sleep_mode_disabled()
log_error("Failed to open sleep mode file: %s", strerror(errno)); log_error("Failed to open sleep mode file: %s", strerror(errno));
return; 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); write(fd, "0", 1);
close(fd); close(fd);
usleep(1000); // give some time to the system to disable the sleep mode
return;
} }
static void detect_sleep_mode() static void detect_sleep_mode()
@ -220,10 +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(); 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");
@ -590,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++)
@ -765,12 +793,18 @@ 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);
video_restart_streaming(); if (should_restart) {
log_info("restarting video streaming due to format change");
video_restart_streaming();
}
} }
memset(&ev, 0, sizeof(ev)); memset(&ev, 0, sizeof(ev));

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,7 +1,9 @@
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"
@ -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. // DefaultEDID is the default EDID for the video stream.
const DefaultEDID = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b" 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"`
@ -69,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.
@ -94,7 +118,9 @@ func (n *Native) VideoSetEDID(edid string) error {
edid = DefaultEDID edid = DefaultEDID
} }
return videoSetEDID(edid) 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

@ -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()
@ -60,9 +61,11 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
}) })
nativeInstance.Start() nativeInstance.Start()
if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil { go func() {
nativeLogger.Warn().Err(err).Msg("error setting EDID") 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

@ -95,7 +95,7 @@ export default function SettingsHardwareRoute() {
return; return;
} }
const result = resp.result as { enabled: boolean; duration: number }; const result = resp.result as { enabled: boolean; duration: number };
setPowerSavingEnabled(result.duration > 0); setPowerSavingEnabled(result.duration >= 0);
}); });
}, [send]); }, [send]);