mirror of https://github.com/jetkvm/kvm.git
Updates: merge 'dev' intu 'feat/audio-support'
This commit is contained in:
parent
9cb976ab8d
commit
d9072673c0
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
200
native.go
200
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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
343
native_shared.go
343
native_shared.go
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -341,7 +341,7 @@ export default function Actionbar({
|
|||
)}
|
||||
onClick={() => {
|
||||
if (isAudioEnabledInUsb) {
|
||||
setDisableFocusTrap(true);
|
||||
setDisableVideoFocusTrap(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue