diff --git a/input_rpc.go b/input_rpc.go index 1981a086..4087780a 100644 --- a/input_rpc.go +++ b/input_rpc.go @@ -112,7 +112,8 @@ func handleKeyboardReportDirect(params map[string]interface{}) (interface{}, err return nil, err } - return nil, rpcKeyboardReport(modifier, keys) + _, err = rpcKeyboardReport(modifier, keys) + return nil, err } // Direct handler for absolute mouse reports diff --git a/internal/usbgadget/interface.go b/internal/usbgadget/interface.go deleted file mode 100644 index 9c7b2645..00000000 --- a/internal/usbgadget/interface.go +++ /dev/null @@ -1,293 +0,0 @@ -package usbgadget - -import ( - "context" - "fmt" - "time" - - "github.com/rs/zerolog" -) - -// UsbGadgetInterface defines the interface for USB gadget operations -// This allows for mocking in tests and separating hardware operations from business logic -type UsbGadgetInterface interface { - // Configuration methods - Init() error - UpdateGadgetConfig() error - SetGadgetConfig(config *Config) - SetGadgetDevices(devices *Devices) - OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) - - // Hardware control methods - RebindUsb(ignoreUnbindError bool) error - IsUDCBound() (bool, error) - BindUDC() error - UnbindUDC() error - - // HID file management - PreOpenHidFiles() - CloseHidFiles() - - // Transaction methods - WithTransaction(fn func() error) error - WithTransactionTimeout(fn func() error, timeout time.Duration) error - - // Path methods - GetConfigPath(itemKey string) (string, error) - GetPath(itemKey string) (string, error) - - // Input methods (matching actual UsbGadget implementation) - KeyboardReport(modifier uint8, keys []uint8) error - AbsMouseReport(x, y int, buttons uint8) error - AbsMouseWheelReport(wheelY int8) error - RelMouseReport(mx, my int8, buttons uint8) error -} - -// Ensure UsbGadget implements the interface -var _ UsbGadgetInterface = (*UsbGadget)(nil) - -// MockUsbGadget provides a mock implementation for testing -type MockUsbGadget struct { - name string - enabledDevices Devices - customConfig Config - log *zerolog.Logger - - // Mock state - initCalled bool - updateConfigCalled bool - rebindCalled bool - udcBound bool - hidFilesOpen bool - transactionCount int - - // Mock behavior controls - ShouldFailInit bool - ShouldFailUpdateConfig bool - ShouldFailRebind bool - ShouldFailUDCBind bool - InitDelay time.Duration - UpdateConfigDelay time.Duration - RebindDelay time.Duration -} - -// NewMockUsbGadget creates a new mock USB gadget for testing -func NewMockUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *MockUsbGadget { - if enabledDevices == nil { - enabledDevices = &defaultUsbGadgetDevices - } - if config == nil { - config = &Config{isEmpty: true} - } - if logger == nil { - logger = defaultLogger - } - - return &MockUsbGadget{ - name: name, - enabledDevices: *enabledDevices, - customConfig: *config, - log: logger, - udcBound: false, - hidFilesOpen: false, - } -} - -// Init mocks USB gadget initialization -func (m *MockUsbGadget) Init() error { - if m.InitDelay > 0 { - time.Sleep(m.InitDelay) - } - if m.ShouldFailInit { - return m.logError("mock init failure", nil) - } - m.initCalled = true - m.udcBound = true - m.log.Info().Msg("mock USB gadget initialized") - return nil -} - -// UpdateGadgetConfig mocks gadget configuration update -func (m *MockUsbGadget) UpdateGadgetConfig() error { - if m.UpdateConfigDelay > 0 { - time.Sleep(m.UpdateConfigDelay) - } - if m.ShouldFailUpdateConfig { - return m.logError("mock update config failure", nil) - } - m.updateConfigCalled = true - m.log.Info().Msg("mock USB gadget config updated") - return nil -} - -// SetGadgetConfig mocks setting gadget configuration -func (m *MockUsbGadget) SetGadgetConfig(config *Config) { - if config != nil { - m.customConfig = *config - } -} - -// SetGadgetDevices mocks setting enabled devices -func (m *MockUsbGadget) SetGadgetDevices(devices *Devices) { - if devices != nil { - m.enabledDevices = *devices - } -} - -// OverrideGadgetConfig mocks gadget config override -func (m *MockUsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) { - m.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("mock override gadget config") - return nil, true -} - -// RebindUsb mocks USB rebinding -func (m *MockUsbGadget) RebindUsb(ignoreUnbindError bool) error { - if m.RebindDelay > 0 { - time.Sleep(m.RebindDelay) - } - if m.ShouldFailRebind { - return m.logError("mock rebind failure", nil) - } - m.rebindCalled = true - m.log.Info().Msg("mock USB gadget rebound") - return nil -} - -// IsUDCBound mocks UDC binding status check -func (m *MockUsbGadget) IsUDCBound() (bool, error) { - return m.udcBound, nil -} - -// BindUDC mocks UDC binding -func (m *MockUsbGadget) BindUDC() error { - if m.ShouldFailUDCBind { - return m.logError("mock UDC bind failure", nil) - } - m.udcBound = true - m.log.Info().Msg("mock UDC bound") - return nil -} - -// UnbindUDC mocks UDC unbinding -func (m *MockUsbGadget) UnbindUDC() error { - m.udcBound = false - m.log.Info().Msg("mock UDC unbound") - return nil -} - -// PreOpenHidFiles mocks HID file pre-opening -func (m *MockUsbGadget) PreOpenHidFiles() { - m.hidFilesOpen = true - m.log.Info().Msg("mock HID files pre-opened") -} - -// CloseHidFiles mocks HID file closing -func (m *MockUsbGadget) CloseHidFiles() { - m.hidFilesOpen = false - m.log.Info().Msg("mock HID files closed") -} - -// WithTransaction mocks transaction execution -func (m *MockUsbGadget) WithTransaction(fn func() error) error { - return m.WithTransactionTimeout(fn, 60*time.Second) -} - -// WithTransactionTimeout mocks transaction execution with timeout -func (m *MockUsbGadget) WithTransactionTimeout(fn func() error, timeout time.Duration) error { - m.transactionCount++ - m.log.Info().Int("transactionCount", m.transactionCount).Msg("mock transaction started") - - // Execute the function in a mock transaction context - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - done := make(chan error, 1) - go func() { - done <- fn() - }() - - select { - case err := <-done: - if err != nil { - m.log.Error().Err(err).Msg("mock transaction failed") - } else { - m.log.Info().Msg("mock transaction completed") - } - return err - case <-ctx.Done(): - m.log.Error().Dur("timeout", timeout).Msg("mock transaction timed out") - return ctx.Err() - } -} - -// GetConfigPath mocks getting configuration path -func (m *MockUsbGadget) GetConfigPath(itemKey string) (string, error) { - return "/mock/config/path/" + itemKey, nil -} - -// GetPath mocks getting path -func (m *MockUsbGadget) GetPath(itemKey string) (string, error) { - return "/mock/path/" + itemKey, nil -} - -// KeyboardReport mocks keyboard input -func (m *MockUsbGadget) KeyboardReport(modifier uint8, keys []uint8) error { - m.log.Debug().Uint8("modifier", modifier).Int("keyCount", len(keys)).Msg("mock keyboard input sent") - return nil -} - -// AbsMouseReport mocks absolute mouse input -func (m *MockUsbGadget) AbsMouseReport(x, y int, buttons uint8) error { - m.log.Debug().Int("x", x).Int("y", y).Uint8("buttons", buttons).Msg("mock absolute mouse input sent") - return nil -} - -// AbsMouseWheelReport mocks absolute mouse wheel input -func (m *MockUsbGadget) AbsMouseWheelReport(wheelY int8) error { - m.log.Debug().Int8("wheelY", wheelY).Msg("mock absolute mouse wheel input sent") - return nil -} - -// RelMouseReport mocks relative mouse input -func (m *MockUsbGadget) RelMouseReport(mx, my int8, buttons uint8) error { - m.log.Debug().Int8("mx", mx).Int8("my", my).Uint8("buttons", buttons).Msg("mock relative mouse input sent") - return nil -} - -// Helper methods for mock -func (m *MockUsbGadget) logError(msg string, err error) error { - if err == nil { - err = fmt.Errorf("%s", msg) - } - m.log.Error().Err(err).Msg(msg) - return err -} - -// Mock state inspection methods for testing -func (m *MockUsbGadget) IsInitCalled() bool { - return m.initCalled -} - -func (m *MockUsbGadget) IsUpdateConfigCalled() bool { - return m.updateConfigCalled -} - -func (m *MockUsbGadget) IsRebindCalled() bool { - return m.rebindCalled -} - -func (m *MockUsbGadget) IsHidFilesOpen() bool { - return m.hidFilesOpen -} - -func (m *MockUsbGadget) GetTransactionCount() int { - return m.transactionCount -} - -func (m *MockUsbGadget) GetEnabledDevices() Devices { - return m.enabledDevices -} - -func (m *MockUsbGadget) GetCustomConfig() Config { - return m.customConfig -} diff --git a/native.go b/native.go index 622b7fef..67f423a0 100644 --- a/native.go +++ b/native.go @@ -1,10 +1,9 @@ -//go:build linux - package kvm import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net" @@ -15,6 +14,8 @@ import ( "time" "github.com/jetkvm/kvm/resource" + + "github.com/pion/webrtc/v4/pkg/media" ) var ctrlSocketConn net.Conn @@ -55,15 +56,200 @@ func CallCtrlAction(action string, params map[string]any) (*CtrlResponse, error) Seq: seq, Params: params, } - cmd.Stdout = &nativeOutput{logger: nativeLogger} - cmd.Stderr = &nativeOutput{logger: nativeLogger} - err := cmd.Start() + responseChan := make(chan *CtrlResponse) + ongoingRequests[seq] = responseChan + seq++ + + jsonData, err := json.Marshal(ctrlAction) if err != nil { - return nil, err + delete(ongoingRequests, ctrlAction.Seq) + return nil, fmt.Errorf("error marshaling ctrl action: %w", err) } - return cmd, nil + scopedLogger := nativeLogger.With(). + Str("action", ctrlAction.Action). + Interface("params", ctrlAction.Params).Logger() + + scopedLogger.Debug().Msg("sending ctrl action") + + err = WriteCtrlMessage(jsonData) + if err != nil { + delete(ongoingRequests, ctrlAction.Seq) + return nil, ErrorfL(&scopedLogger, "error writing ctrl message", err) + } + + select { + case response := <-responseChan: + delete(ongoingRequests, seq) + if response.Error != "" { + return nil, ErrorfL( + &scopedLogger, + "error native response: %s", + errors.New(response.Error), + ) + } + return response, nil + case <-time.After(5 * time.Second): + close(responseChan) + delete(ongoingRequests, seq) + return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil) + } +} + +func WriteCtrlMessage(message []byte) error { + if ctrlSocketConn == nil { + return fmt.Errorf("ctrl socket not conn ected") + } + _, err := ctrlSocketConn.Write(message) + return err +} + +var nativeCtrlSocketListener net.Listener //nolint:unused +var nativeVideoSocketListener net.Listener //nolint:unused + +var ctrlClientConnected = make(chan struct{}) + +func waitCtrlClientConnected() { + <-ctrlClientConnected +} + +func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener { + scopedLogger := nativeLogger.With(). + Str("socket_path", socketPath). + Logger() + + // Remove the socket file if it already exists + if _, err := os.Stat(socketPath); err == nil { + if err := os.Remove(socketPath); err != nil { + scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file") + os.Exit(1) + } + } + + listener, err := net.Listen("unixpacket", socketPath) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to start server") + os.Exit(1) + } + + scopedLogger.Info().Msg("server listening") + + go func() { + for { + conn, err := listener.Accept() + + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to accept socket") + continue + } + if isCtrl { + // check if the channel is closed + select { + case <-ctrlClientConnected: + scopedLogger.Debug().Msg("ctrl client reconnected") + default: + close(ctrlClientConnected) + scopedLogger.Debug().Msg("first native ctrl socket client connected") + } + } + + go handleClient(conn) + } + }() + + return listener +} + +func StartNativeCtrlSocketServer() { + nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true) + nativeLogger.Debug().Msg("native app ctrl sock started") +} + +func StartNativeVideoSocketServer() { + nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false) + nativeLogger.Debug().Msg("native app video sock started") +} + +func handleCtrlClient(conn net.Conn) { + defer conn.Close() + + scopedLogger := nativeLogger.With(). + Str("addr", conn.RemoteAddr().String()). + Str("type", "ctrl"). + Logger() + + scopedLogger.Info().Msg("native ctrl socket client connected") + if ctrlSocketConn != nil { + scopedLogger.Debug().Msg("closing existing native socket connection") + ctrlSocketConn.Close() + } + + ctrlSocketConn = conn + + // Restore HDMI EDID if applicable + go restoreHdmiEdid() + + readBuf := make([]byte, 4096) + for { + n, err := conn.Read(readBuf) + if err != nil { + scopedLogger.Warn().Err(err).Msg("error reading from ctrl sock") + break + } + readMsg := string(readBuf[:n]) + + ctrlResp := CtrlResponse{} + err = json.Unmarshal([]byte(readMsg), &ctrlResp) + if err != nil { + scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing ctrl sock msg") + continue + } + scopedLogger.Trace().Interface("data", ctrlResp).Msg("ctrl sock msg") + + if ctrlResp.Seq != 0 { + responseChan, ok := ongoingRequests[ctrlResp.Seq] + if ok { + responseChan <- &ctrlResp + } + } + switch ctrlResp.Event { + case "video_input_state": + HandleVideoStateMessage(ctrlResp) + } + } + + scopedLogger.Debug().Msg("ctrl sock disconnected") +} + +func handleVideoClient(conn net.Conn) { + defer conn.Close() + + scopedLogger := nativeLogger.With(). + Str("addr", conn.RemoteAddr().String()). + Str("type", "video"). + Logger() + + scopedLogger.Info().Msg("native video socket client connected") + + inboundPacket := make([]byte, maxFrameSize) + lastFrame := time.Now() + for { + n, err := conn.Read(inboundPacket) + if err != nil { + scopedLogger.Warn().Err(err).Msg("error during read") + return + } + now := time.Now() + sinceLastFrame := now.Sub(lastFrame) + lastFrame = now + if currentSession != nil { + err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) + if err != nil { + scopedLogger.Warn().Err(err).Msg("error writing sample") + } + } + } } func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) { diff --git a/native_linux.go b/native_linux.go new file mode 100644 index 00000000..54d21501 --- /dev/null +++ b/native_linux.go @@ -0,0 +1,57 @@ +//go:build linux + +package kvm + +import ( + "fmt" + "os/exec" + "sync" + "syscall" + + "github.com/rs/zerolog" +) + +type nativeOutput struct { + mu *sync.Mutex + logger *zerolog.Event +} + +func (w *nativeOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + w.logger.Msg(string(p)) + return len(p), nil +} + +func startNativeBinary(binaryPath string) (*exec.Cmd, error) { + // Run the binary in the background + cmd := exec.Command(binaryPath) + + nativeOutputLock := sync.Mutex{} + nativeStdout := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stdout"), + } + nativeStderr := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stderr"), + } + + // Redirect stdout and stderr to the current process + cmd.Stdout = nativeStdout + cmd.Stderr = nativeStderr + + // Set the process group ID so we can kill the process and its children when this process exits + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pdeathsig: syscall.SIGKILL, + } + + // Start the command + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start binary: %w", err) + } + + return cmd, nil +} diff --git a/native_notlinux.go b/native_notlinux.go index b8dbd119..df6df74a 100644 --- a/native_notlinux.go +++ b/native_notlinux.go @@ -8,9 +8,5 @@ import ( ) func startNativeBinary(binaryPath string) (*exec.Cmd, error) { - return nil, fmt.Errorf("startNativeBinary is only supported on Linux") -} - -func ExtractAndRunNativeBin() error { - return fmt.Errorf("ExtractAndRunNativeBin is only supported on Linux") + return nil, fmt.Errorf("not supported") } diff --git a/native_shared.go b/native_shared.go deleted file mode 100644 index 202348bf..00000000 --- a/native_shared.go +++ /dev/null @@ -1,343 +0,0 @@ -package kvm - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "os" - "runtime" - "strings" - "sync" - "time" - - "github.com/jetkvm/kvm/resource" - "github.com/pion/webrtc/v4/pkg/media" -) - -type CtrlAction struct { - Action string `json:"action"` - Seq int32 `json:"seq,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` -} - -type CtrlResponse struct { - Seq int32 `json:"seq,omitempty"` - Error string `json:"error,omitempty"` - Errno int32 `json:"errno,omitempty"` - Result map[string]interface{} `json:"result,omitempty"` - Event string `json:"event,omitempty"` - Data json.RawMessage `json:"data,omitempty"` -} - -type EventHandler func(event CtrlResponse) - -var seq int32 = 1 - -var ongoingRequests = make(map[int32]chan *CtrlResponse) - -var lock = &sync.Mutex{} - -var ctrlSocketConn net.Conn - -var nativeCtrlSocketListener net.Listener //nolint:unused -var nativeVideoSocketListener net.Listener //nolint:unused - -var ctrlClientConnected = make(chan struct{}) - -func waitCtrlClientConnected() { - <-ctrlClientConnected -} - -func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) { - lock.Lock() - defer lock.Unlock() - ctrlAction := CtrlAction{ - Action: action, - Seq: seq, - Params: params, - } - - responseChan := make(chan *CtrlResponse) - ongoingRequests[seq] = responseChan - seq++ - - jsonData, err := json.Marshal(ctrlAction) - if err != nil { - delete(ongoingRequests, ctrlAction.Seq) - return nil, fmt.Errorf("error marshaling ctrl action: %w", err) - } - - scopedLogger := nativeLogger.With(). - Str("action", ctrlAction.Action). - Interface("params", ctrlAction.Params).Logger() - - scopedLogger.Debug().Msg("sending ctrl action") - - err = WriteCtrlMessage(jsonData) - if err != nil { - delete(ongoingRequests, ctrlAction.Seq) - return nil, ErrorfL(&scopedLogger, "error writing ctrl message", err) - } - - select { - case response := <-responseChan: - delete(ongoingRequests, seq) - if response.Error != "" { - return nil, ErrorfL( - &scopedLogger, - "error native response: %s", - errors.New(response.Error), - ) - } - return response, nil - case <-time.After(5 * time.Second): - close(responseChan) - delete(ongoingRequests, seq) - return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil) - } -} - -func WriteCtrlMessage(message []byte) error { - if ctrlSocketConn == nil { - return fmt.Errorf("ctrl socket not connected") - } - _, err := ctrlSocketConn.Write(message) - return err -} - -func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener { - scopedLogger := nativeLogger.With(). - Str("socket_path", socketPath). - Logger() - - // Remove the socket file if it already exists - if _, err := os.Stat(socketPath); err == nil { - if err := os.Remove(socketPath); err != nil { - scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file") - os.Exit(1) - } - } - - listener, err := net.Listen("unixpacket", socketPath) - if err != nil { - scopedLogger.Warn().Err(err).Msg("failed to start server") - os.Exit(1) - } - - scopedLogger.Info().Msg("server listening") - - go func() { - for { - conn, err := listener.Accept() - - if err != nil { - scopedLogger.Warn().Err(err).Msg("failed to accept socket") - continue - } - if isCtrl { - // check if the channel is closed - select { - case <-ctrlClientConnected: - scopedLogger.Debug().Msg("ctrl client reconnected") - default: - close(ctrlClientConnected) - scopedLogger.Debug().Msg("first native ctrl socket client connected") - } - } - - go handleClient(conn) - } - }() - - return listener -} - -func StartNativeCtrlSocketServer() { - nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true) - nativeLogger.Debug().Msg("native app ctrl sock started") -} - -func StartNativeVideoSocketServer() { - nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false) - nativeLogger.Debug().Msg("native app video sock started") -} - -func handleCtrlClient(conn net.Conn) { - // Lock to OS thread to isolate blocking socket I/O - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - defer conn.Close() - - scopedLogger := nativeLogger.With(). - Str("addr", conn.RemoteAddr().String()). - Str("type", "ctrl"). - Logger() - - scopedLogger.Info().Msg("native ctrl socket client connected (OS thread locked)") - if ctrlSocketConn != nil { - scopedLogger.Debug().Msg("closing existing native socket connection") - ctrlSocketConn.Close() - } - - ctrlSocketConn = conn - - // Restore HDMI EDID if applicable - go restoreHdmiEdid() - - readBuf := make([]byte, 4096) - for { - n, err := conn.Read(readBuf) - if err != nil { - scopedLogger.Warn().Err(err).Msg("error reading from ctrl sock") - break - } - readMsg := string(readBuf[:n]) - - ctrlResp := CtrlResponse{} - err = json.Unmarshal([]byte(readMsg), &ctrlResp) - if err != nil { - scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing ctrl sock msg") - continue - } - scopedLogger.Trace().Interface("data", ctrlResp).Msg("ctrl sock msg") - - if ctrlResp.Seq != 0 { - responseChan, ok := ongoingRequests[ctrlResp.Seq] - if ok { - responseChan <- &ctrlResp - } - } - switch ctrlResp.Event { - case "video_input_state": - HandleVideoStateMessage(ctrlResp) - } - } - - scopedLogger.Debug().Msg("ctrl sock disconnected") -} - -func handleVideoClient(conn net.Conn) { - // Lock to OS thread to isolate blocking video I/O - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - defer conn.Close() - - scopedLogger := nativeLogger.With(). - Str("addr", conn.RemoteAddr().String()). - Str("type", "video"). - Logger() - - scopedLogger.Info().Msg("native video socket client connected (OS thread locked)") - - inboundPacket := make([]byte, maxVideoFrameSize) - lastFrame := time.Now() - for { - n, err := conn.Read(inboundPacket) - if err != nil { - scopedLogger.Warn().Err(err).Msg("error during read") - return - } - now := time.Now() - sinceLastFrame := now.Sub(lastFrame) - lastFrame = now - if currentSession != nil { - err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) - if err != nil { - scopedLogger.Warn().Err(err).Msg("error writing sample") - } - } - } -} - -func shouldOverwrite(destPath string, srcHash []byte) bool { - if srcHash == nil { - nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, doing overwriting") - return true - } - - dstHash, err := os.ReadFile(destPath + ".sha256") - if err != nil { - nativeLogger.Debug().Msg("error reading existing jetkvm_native.sha256, doing overwriting") - return true - } - - return !bytes.Equal(srcHash, dstHash) -} - -func getNativeSha256() ([]byte, error) { - version, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256") - if err != nil { - return nil, err - } - return version, nil -} - -func GetNativeVersion() (string, error) { - version, err := getNativeSha256() - if err != nil { - return "", err - } - return strings.TrimSpace(string(version)), nil -} - -func ensureBinaryUpdated(destPath string) error { - // Lock to OS thread for file I/O operations - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - srcFile, err := resource.ResourceFS.Open("jetkvm_native") - if err != nil { - return err - } - defer srcFile.Close() - - srcHash, err := getNativeSha256() - if err != nil { - nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update") - srcHash = nil - } - - _, err = os.Stat(destPath) - if shouldOverwrite(destPath, srcHash) || err != nil { - nativeLogger.Info(). - Interface("hash", srcHash). - Msg("writing jetkvm_native") - - _ = os.Remove(destPath) - destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) - if err != nil { - return err - } - _, err = io.Copy(destFile, srcFile) - destFile.Close() - if err != nil { - return err - } - if srcHash != nil { - err = os.WriteFile(destPath+".sha256", srcHash, 0644) - if err != nil { - return err - } - } - nativeLogger.Info().Msg("jetkvm_native updated") - } - - return nil -} - -// Restore the HDMI EDID value from the config. -// Called after successful connection to jetkvm_native. -func restoreHdmiEdid() { - if config.EdidString != "" { - nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID") - _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString}) - if err != nil { - nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID") - } - } -} diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 7db7326d..cd1fde4e 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -341,7 +341,7 @@ export default function Actionbar({ )} onClick={() => { if (isAudioEnabledInUsb) { - setDisableFocusTrap(true); + setDisableVideoFocusTrap(true); } }} /> diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts index cdb30f29..b4fcc8ef 100644 --- a/ui/src/hooks/useJsonRpc.ts +++ b/ui/src/hooks/useJsonRpc.ts @@ -2,8 +2,6 @@ import { useCallback, useEffect } from "react"; import { useRTCStore } from "@/hooks/stores"; -import { devError } from '../utils/debug'; - export interface JsonRpcRequest { jsonrpc: string; method: string; diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 4087ebe2..af1e5e84 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -654,6 +654,10 @@ export default function KvmIdRoute() { const { send } = useJsonRpc(onJsonRpcRequest); + // Initialize microphone hook + const microphoneHook = useMicrophone(); + const { syncMicrophoneState } = microphoneHook; + // Handle audio device changes to sync microphone state const handleAudioDeviceChanged = useCallback((data: { enabled: boolean; reason: string }) => { console.log('[AudioDeviceChanged] Audio device changed:', data); diff --git a/video.go b/video.go index 125698b4..6fa77b94 100644 --- a/video.go +++ b/video.go @@ -5,7 +5,7 @@ import ( ) // max frame size for 1080p video, specified in mpp venc setting -const maxVideoFrameSize = 1920 * 1080 / 2 +const maxFrameSize = 1920 * 1080 / 2 func writeCtrlAction(action string) error { actionMessage := map[string]string{