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"`
DefaultLogLevel string `json:"default_log_level"`
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

@ -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()
@ -220,10 +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");
@ -590,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++)
@ -603,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;
@ -632,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");
}
@ -649,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)
@ -666,7 +694,7 @@ void video_start_streaming()
free(new_thread);
return;
}
// Only set streaming_thread after successful creation
streaming_thread = new_thread;
}
@ -677,7 +705,7 @@ void video_stop_streaming()
log_info("video streaming already stopped");
return;
}
log_info("stopping video streaming");
set_streaming_flag(false);
@ -695,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()
@ -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.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));
@ -802,4 +836,4 @@ void video_set_quality_factor(float factor)
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,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.

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()
@ -60,9 +61,11 @@ 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() {
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

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