mirror of https://github.com/jetkvm/kvm.git
Compare commits
6 Commits
08b0dd0c37
...
f56e1480d1
| Author | SHA1 | Date |
|---|---|---|
|
|
f56e1480d1 | |
|
|
8189861bfa | |
|
|
c8808ee3b2 | |
|
|
8dc013d8fe | |
|
|
ba2fa34385 | |
|
|
2444817455 |
4
Makefile
4
Makefile
|
|
@ -2,8 +2,8 @@ BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
|
|||
BUILDDATE := $(shell date -u +%FT%T%z)
|
||||
BUILDTS := $(shell date -u +%s)
|
||||
REVISION := $(shell git rev-parse HEAD)
|
||||
VERSION_DEV := 0.4.9-dev$(shell date +%Y%m%d%H%M)
|
||||
VERSION := 0.4.8
|
||||
VERSION_DEV ?= 0.4.9-dev$(shell date +%Y%m%d%H%M)
|
||||
VERSION ?= 0.4.8
|
||||
|
||||
PROMETHEUS_TAG := github.com/prometheus/common/version
|
||||
KVM_PKG_NAME := github.com/jetkvm/kvm
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func updateDisplay() {
|
|||
nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Disconnected")
|
||||
_, _ = nativeInstance.UIObjClearState("hdmi_status_label", "LV_STATE_CHECKED")
|
||||
}
|
||||
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
|
||||
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", getActiveSessions()))
|
||||
|
||||
if networkManager != nil && networkManager.IsUp() {
|
||||
nativeInstance.UISetVar("main_screen", "home_screen")
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
13
native.go
13
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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,18 @@ func (sm *SessionManager) attemptEmergencyPromotion(ctx emergencyPromotionContex
|
|||
sm.emergencyWindowMutex.Lock()
|
||||
defer sm.emergencyWindowMutex.Unlock()
|
||||
|
||||
// CRITICAL: Bypass all rate limits if no primary exists to prevent deadlock
|
||||
// System availability takes priority over DoS protection
|
||||
noPrimaryExists := (sm.primarySessionID == "")
|
||||
if noPrimaryExists {
|
||||
sm.logger.Info().
|
||||
Str("triggerSessionID", ctx.triggerSessionID).
|
||||
Str("triggerReason", ctx.triggerReason).
|
||||
Msg("Bypassing emergency promotion rate limits - no primary exists")
|
||||
promotedSessionID := sm.findMostTrustedSessionForEmergency()
|
||||
return promotedSessionID, true, false
|
||||
}
|
||||
|
||||
const slidingWindowDuration = 60 * time.Second
|
||||
const maxEmergencyPromotionsPerMinute = 3
|
||||
|
||||
|
|
@ -187,19 +199,21 @@ func (sm *SessionManager) promoteAfterGraceExpiration(expiredSessionID string, n
|
|||
// handlePendingSessionTimeout removes timed-out pending sessions (DoS protection)
|
||||
// Returns true if any pending session was removed
|
||||
func (sm *SessionManager) handlePendingSessionTimeout(now time.Time) bool {
|
||||
needsCleanup := false
|
||||
toDelete := make([]string, 0)
|
||||
for id, session := range sm.sessions {
|
||||
if session.Mode == SessionModePending &&
|
||||
now.Sub(session.CreatedAt) > defaultPendingSessionTimeout {
|
||||
websocketLogger.Info().
|
||||
websocketLogger.Debug().
|
||||
Str("sessionId", id).
|
||||
Dur("age", now.Sub(session.CreatedAt)).
|
||||
Msg("Removing timed-out pending session")
|
||||
delete(sm.sessions, id)
|
||||
needsCleanup = true
|
||||
toDelete = append(toDelete, id)
|
||||
}
|
||||
}
|
||||
return needsCleanup
|
||||
for _, id := range toDelete {
|
||||
delete(sm.sessions, id)
|
||||
}
|
||||
return len(toDelete) > 0
|
||||
}
|
||||
|
||||
// handleObserverSessionCleanup removes inactive observer sessions with closed RPC channels
|
||||
|
|
@ -210,21 +224,23 @@ func (sm *SessionManager) handleObserverSessionCleanup(now time.Time) bool {
|
|||
observerTimeout = time.Duration(currentSessionSettings.ObserverTimeout) * time.Second
|
||||
}
|
||||
|
||||
needsCleanup := false
|
||||
toDelete := make([]string, 0)
|
||||
for id, session := range sm.sessions {
|
||||
if session.Mode == SessionModeObserver {
|
||||
if session.RPCChannel == nil && now.Sub(session.LastActive) > observerTimeout {
|
||||
sm.logger.Info().
|
||||
sm.logger.Debug().
|
||||
Str("sessionId", id).
|
||||
Dur("inactiveFor", now.Sub(session.LastActive)).
|
||||
Dur("observerTimeout", observerTimeout).
|
||||
Msg("Removing inactive observer session with closed RPC channel")
|
||||
delete(sm.sessions, id)
|
||||
needsCleanup = true
|
||||
toDelete = append(toDelete, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return needsCleanup
|
||||
for _, id := range toDelete {
|
||||
delete(sm.sessions, id)
|
||||
}
|
||||
return len(toDelete) > 0
|
||||
}
|
||||
|
||||
// handlePrimarySessionTimeout checks and handles primary session timeout
|
||||
|
|
|
|||
|
|
@ -250,6 +250,10 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
// Ensure session has auto-generated nickname if needed
|
||||
sm.ensureNickname(session)
|
||||
|
||||
if !nicknameReserved && session.Nickname != "" {
|
||||
sm.nicknameIndex[session.Nickname] = session
|
||||
}
|
||||
|
||||
sm.sessions[session.ID] = session
|
||||
|
||||
// If this was the primary, try to restore primary status
|
||||
|
|
@ -1200,8 +1204,8 @@ func (sm *SessionManager) transferPrimaryRole(fromSessionID, toSessionID, transf
|
|||
// Promote target session
|
||||
toSession.Mode = SessionModePrimary
|
||||
toSession.hidRPCAvailable = false
|
||||
// Reset LastActive only for emergency promotions to prevent immediate re-timeout
|
||||
if transferType == "emergency_timeout_promotion" || transferType == "emergency_promotion_deadlock_prevention" {
|
||||
// Reset LastActive for all emergency promotions to prevent immediate re-timeout
|
||||
if strings.HasPrefix(transferType, "emergency_") {
|
||||
toSession.LastActive = time.Now()
|
||||
}
|
||||
sm.primarySessionID = toSessionID
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -77,6 +94,17 @@ export default function SettingsHardwareRoute() {
|
|||
}
|
||||
}, [send, setBacklightSettings, isLoading, permissions]);
|
||||
|
||||
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 early if permissions are loading
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -898,10 +898,11 @@ export default function KvmIdRoute() {
|
|||
|
||||
useEffect(() => {
|
||||
if (appVersion) return;
|
||||
if (rpcDataChannel?.readyState !== "open") return;
|
||||
|
||||
getLocalVersion();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appVersion]);
|
||||
}, [appVersion, rpcDataChannel?.readyState]);
|
||||
|
||||
const ConnectionStatusElement = useMemo(() => {
|
||||
const isOtherSession = location.pathname.includes("other-session");
|
||||
|
|
|
|||
25
webrtc.go
25
webrtc.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
|
|
@ -66,24 +67,14 @@ type Session struct {
|
|||
keysDownStateQueue chan usbgadget.KeysDownState
|
||||
}
|
||||
|
||||
var (
|
||||
actionSessions int = 0
|
||||
activeSessionsMutex = &sync.Mutex{}
|
||||
)
|
||||
var actionSessions atomic.Int32
|
||||
|
||||
func incrActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
actionSessions++
|
||||
return actionSessions
|
||||
func incrActiveSessions() int32 {
|
||||
return actionSessions.Add(1)
|
||||
}
|
||||
|
||||
func getActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
return actionSessions
|
||||
func getActiveSessions() int32 {
|
||||
return actionSessions.Load()
|
||||
}
|
||||
|
||||
// CheckRPCRateLimit checks if the session has exceeded RPC rate limits (DoS protection)
|
||||
|
|
@ -494,9 +485,9 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
|
||||
if isConnected {
|
||||
isConnected = false
|
||||
actionSessions--
|
||||
newCount := actionSessions.Add(-1)
|
||||
onActiveSessionsChanged()
|
||||
if actionSessions == 0 {
|
||||
if newCount == 0 {
|
||||
onLastSessionDisconnected()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue