mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
d48dc390df
...
081528062d
| Author | SHA1 | Date |
|---|---|---|
|
|
081528062d | |
|
|
2444817455 | |
|
|
61ee27e931 | |
|
|
19ff472f8b |
2
cloud.go
2
cloud.go
|
|
@ -478,7 +478,7 @@ func handleSessionRequest(
|
||||||
cloudLogger.Trace().Interface("session", session).Msg("new session accepted")
|
cloudLogger.Trace().Interface("session", session).Msg("new session accepted")
|
||||||
|
|
||||||
// Cancel any ongoing keyboard macro when session changes
|
// Cancel any ongoing keyboard macro when session changes
|
||||||
cancelKeyboardMacro()
|
cancelAllRunningKeyboardMacros()
|
||||||
|
|
||||||
currentSession = session
|
currentSession = session
|
||||||
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
|
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ type Config struct {
|
||||||
NetworkConfig *types.NetworkConfig `json:"network_config"`
|
NetworkConfig *types.NetworkConfig `json:"network_config"`
|
||||||
DefaultLogLevel string `json:"default_log_level"`
|
DefaultLogLevel string `json:"default_log_level"`
|
||||||
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
||||||
|
VideoQualityFactor float64 `json:"video_quality_factor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetDisplayRotation() uint16 {
|
func (c *Config) GetDisplayRotation() uint16 {
|
||||||
|
|
|
||||||
42
hidrpc.go
42
hidrpc.go
|
|
@ -26,20 +26,45 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session.hidRPCAvailable = true
|
session.hidRPCAvailable = true
|
||||||
|
|
||||||
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
|
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
|
||||||
rpcErr = handleHidRPCKeyboardInput(message)
|
rpcErr = handleHidRPCKeyboardInput(message)
|
||||||
|
|
||||||
case hidrpc.TypeKeyboardMacroReport:
|
case hidrpc.TypeKeyboardMacroReport:
|
||||||
keyboardMacroReport, err := message.KeyboardMacroReport()
|
keyboardMacroReport, err := message.KeyboardMacroReport()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to get keyboard macro report")
|
logger.Warn().Err(err).Msg("failed to get keyboard macro report")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps)
|
token := rpcExecuteKeyboardMacro(keyboardMacroReport.IsPaste, keyboardMacroReport.Steps)
|
||||||
|
logger.Debug().Str("token", token.String()).Msg("started keyboard macro")
|
||||||
|
message, err := hidrpc.NewKeyboardMacroTokenMessage(token).Marshal()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to marshal running macro token message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := session.HidChannel.Send(message); err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to send running macro token message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
case hidrpc.TypeCancelKeyboardMacroReport:
|
case hidrpc.TypeCancelKeyboardMacroReport:
|
||||||
rpcCancelKeyboardMacro()
|
rpcCancelKeyboardMacro()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case hidrpc.TypeKeyboardMacroTokenState:
|
||||||
|
tokenState, err := message.KeyboardMacroTokenState()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to get keyboard macro token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rpcCancelKeyboardMacroByToken(tokenState.Token)
|
||||||
|
return
|
||||||
|
|
||||||
case hidrpc.TypeKeypressKeepAliveReport:
|
case hidrpc.TypeKeypressKeepAliveReport:
|
||||||
rpcErr = handleHidRPCKeypressKeepAlive(session)
|
rpcErr = handleHidRPCKeypressKeepAlive(session)
|
||||||
|
|
||||||
case hidrpc.TypePointerReport:
|
case hidrpc.TypePointerReport:
|
||||||
pointerReport, err := message.PointerReport()
|
pointerReport, err := message.PointerReport()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -47,6 +72,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button)
|
rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button)
|
||||||
|
|
||||||
case hidrpc.TypeMouseReport:
|
case hidrpc.TypeMouseReport:
|
||||||
mouseReport, err := message.MouseReport()
|
mouseReport, err := message.MouseReport()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -54,6 +80,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
|
rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type")
|
logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type")
|
||||||
}
|
}
|
||||||
|
|
@ -65,15 +92,18 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
||||||
|
|
||||||
func onHidMessage(msg hidQueueMessage, session *Session) {
|
func onHidMessage(msg hidQueueMessage, session *Session) {
|
||||||
data := msg.Data
|
data := msg.Data
|
||||||
|
dataLen := len(data)
|
||||||
|
|
||||||
scopedLogger := hidRPCLogger.With().
|
scopedLogger := hidRPCLogger.With().
|
||||||
Str("channel", msg.channel).
|
Str("channel", msg.channel).
|
||||||
Bytes("data", data).
|
Dur("timelimit", msg.timelimit).
|
||||||
|
Int("data_len", dataLen).
|
||||||
|
Bytes("data", data[:min(dataLen, 32)]).
|
||||||
Logger()
|
Logger()
|
||||||
scopedLogger.Debug().Msg("HID RPC message received")
|
scopedLogger.Debug().Msg("HID RPC message received")
|
||||||
|
|
||||||
if len(data) < 1 {
|
if dataLen < 1 {
|
||||||
scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler")
|
scopedLogger.Warn().Msg("received empty data in HID RPC message handler")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +126,7 @@ func onHidMessage(msg hidQueueMessage, session *Session) {
|
||||||
r <- nil
|
r <- nil
|
||||||
}()
|
}()
|
||||||
select {
|
select {
|
||||||
case <-time.After(1 * time.Second):
|
case <-time.After(msg.timelimit * time.Second):
|
||||||
scopedLogger.Warn().Msg("HID RPC message timed out")
|
scopedLogger.Warn().Msg("HID RPC message timed out")
|
||||||
case <-r:
|
case <-r:
|
||||||
scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")
|
scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")
|
||||||
|
|
@ -212,6 +242,8 @@ func reportHidRPC(params any, session *Session) {
|
||||||
message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
|
message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
|
||||||
case hidrpc.KeyboardMacroState:
|
case hidrpc.KeyboardMacroState:
|
||||||
message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal()
|
message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal()
|
||||||
|
case hidrpc.KeyboardMacroTokenState:
|
||||||
|
message, err = hidrpc.NewKeyboardMacroTokenMessage(params.Token).Marshal()
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unknown HID RPC message type: %T", params)
|
err = fmt.Errorf("unknown HID RPC message type: %T", params)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package hidrpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,26 +24,34 @@ const (
|
||||||
TypeKeyboardLedState MessageType = 0x32
|
TypeKeyboardLedState MessageType = 0x32
|
||||||
TypeKeydownState MessageType = 0x33
|
TypeKeydownState MessageType = 0x33
|
||||||
TypeKeyboardMacroState MessageType = 0x34
|
TypeKeyboardMacroState MessageType = 0x34
|
||||||
|
TypeKeyboardMacroTokenState MessageType = 0x35
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type QueueIndex int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version byte = 0x01 // Version of the HID RPC protocol
|
Version byte = 0x01 // Version of the HID RPC protocol
|
||||||
|
HandshakeQueue int = 0 // Queue index for handshake messages
|
||||||
|
KeyboardQueue int = 1 // Queue index for keyboard messages
|
||||||
|
MouseQueue int = 2 // Queue index for mouse messages
|
||||||
|
MacroQueue int = 3 // Queue index for macro messages
|
||||||
|
OtherQueue int = 4 // Queue index for other messages
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetQueueIndex returns the index of the queue to which the message should be enqueued.
|
// GetQueueIndex returns the index of the queue to which the message should be enqueued.
|
||||||
func GetQueueIndex(messageType MessageType) int {
|
func GetQueueIndex(messageType MessageType) (int, time.Duration) {
|
||||||
switch messageType {
|
switch messageType {
|
||||||
case TypeHandshake:
|
case TypeHandshake:
|
||||||
return 0
|
return HandshakeQueue, 1
|
||||||
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardMacroReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
|
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
|
||||||
return 1
|
return KeyboardQueue, 1
|
||||||
case TypePointerReport, TypeMouseReport, TypeWheelReport:
|
case TypePointerReport, TypeMouseReport, TypeWheelReport:
|
||||||
return 2
|
return MouseQueue, 1
|
||||||
// we don't want to block the queue for this message
|
// we don't want to block the queue for these messages
|
||||||
case TypeCancelKeyboardMacroReport:
|
case TypeKeyboardMacroReport, TypeCancelKeyboardMacroReport, TypeKeyboardMacroTokenState:
|
||||||
return 3
|
return MacroQueue, 60 // 1 minute timeout
|
||||||
default:
|
default:
|
||||||
return 3
|
return OtherQueue, 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,3 +131,13 @@ func NewKeyboardMacroStateMessage(state bool, isPaste bool) *Message {
|
||||||
d: data,
|
d: data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewKeyboardMacroTokenMessage creates a new keyboard macro token message.
|
||||||
|
func NewKeyboardMacroTokenMessage(token uuid.UUID) *Message {
|
||||||
|
data, _ := token.MarshalBinary()
|
||||||
|
|
||||||
|
return &Message{
|
||||||
|
t: TypeKeyboardMacroTokenState,
|
||||||
|
d: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package hidrpc
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message ..
|
// Message ..
|
||||||
|
|
@ -23,6 +25,9 @@ func (m *Message) Type() MessageType {
|
||||||
func (m *Message) String() string {
|
func (m *Message) String() string {
|
||||||
switch m.t {
|
switch m.t {
|
||||||
case TypeHandshake:
|
case TypeHandshake:
|
||||||
|
if len(m.d) != 0 {
|
||||||
|
return fmt.Sprintf("Handshake{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
return "Handshake"
|
return "Handshake"
|
||||||
case TypeKeypressReport:
|
case TypeKeypressReport:
|
||||||
if len(m.d) < 2 {
|
if len(m.d) < 2 {
|
||||||
|
|
@ -45,12 +50,45 @@ func (m *Message) String() string {
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
|
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
|
||||||
case TypeKeypressKeepAliveReport:
|
case TypeKeypressKeepAliveReport:
|
||||||
|
if len(m.d) != 0 {
|
||||||
|
return fmt.Sprintf("KeypressKeepAliveReport{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
return "KeypressKeepAliveReport"
|
return "KeypressKeepAliveReport"
|
||||||
|
case TypeWheelReport:
|
||||||
|
if len(m.d) < 3 {
|
||||||
|
return fmt.Sprintf("WheelReport{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("WheelReport{Vertical: %d, Horizontal: %d}", int8(m.d[0]), int8(m.d[1]))
|
||||||
case TypeKeyboardMacroReport:
|
case TypeKeyboardMacroReport:
|
||||||
if len(m.d) < 5 {
|
if len(m.d) < 5 {
|
||||||
return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d)
|
return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5]))
|
return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5]))
|
||||||
|
case TypeCancelKeyboardMacroReport:
|
||||||
|
if len(m.d) != 0 {
|
||||||
|
return fmt.Sprintf("CancelKeyboardMacroReport{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return "CancelKeyboardMacroReport"
|
||||||
|
case TypeKeyboardMacroTokenState:
|
||||||
|
if len(m.d) != 16 {
|
||||||
|
return fmt.Sprintf("KeyboardMacroTokenState{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("KeyboardMacroTokenState{Token: %s}", uuid.Must(uuid.FromBytes(m.d)).String())
|
||||||
|
case TypeKeyboardLedState:
|
||||||
|
if len(m.d) < 1 {
|
||||||
|
return fmt.Sprintf("KeyboardLedState{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("KeyboardLedState{State: %d}", m.d[0])
|
||||||
|
case TypeKeydownState:
|
||||||
|
if len(m.d) < 1 {
|
||||||
|
return fmt.Sprintf("KeydownState{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("KeydownState{State: %d}", m.d[0])
|
||||||
|
case TypeKeyboardMacroState:
|
||||||
|
if len(m.d) < 2 {
|
||||||
|
return fmt.Sprintf("KeyboardMacroState{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("KeyboardMacroState{State: %v, IsPaste: %v}", m.d[0] == uint8(1), m.d[1] == uint8(1))
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
|
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +105,9 @@ func (m *Message) KeypressReport() (KeypressReport, error) {
|
||||||
if m.t != TypeKeypressReport {
|
if m.t != TypeKeypressReport {
|
||||||
return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||||
}
|
}
|
||||||
|
if len(m.d) < 2 {
|
||||||
|
return KeypressReport{}, fmt.Errorf("invalid message data length: %d", len(m.d))
|
||||||
|
}
|
||||||
return KeypressReport{
|
return KeypressReport{
|
||||||
Key: m.d[0],
|
Key: m.d[0],
|
||||||
Press: m.d[1] == uint8(1),
|
Press: m.d[1] == uint8(1),
|
||||||
|
|
@ -95,7 +135,7 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) {
|
||||||
// Macro ..
|
// Macro ..
|
||||||
type KeyboardMacroStep struct {
|
type KeyboardMacroStep struct {
|
||||||
Modifier byte // 1 byte
|
Modifier byte // 1 byte
|
||||||
Keys []byte // 6 bytes: hidKeyBufferSize
|
Keys []byte // 6 bytes: HidKeyBufferSize
|
||||||
Delay uint16 // 2 bytes
|
Delay uint16 // 2 bytes
|
||||||
}
|
}
|
||||||
type KeyboardMacroReport struct {
|
type KeyboardMacroReport struct {
|
||||||
|
|
@ -105,7 +145,7 @@ type KeyboardMacroReport struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HidKeyBufferSize is the size of the keys buffer in the keyboard report.
|
// HidKeyBufferSize is the size of the keys buffer in the keyboard report.
|
||||||
const HidKeyBufferSize = 6
|
const HidKeyBufferSize int = 6
|
||||||
|
|
||||||
// KeyboardMacroReport returns the keyboard macro report from the message.
|
// KeyboardMacroReport returns the keyboard macro report from the message.
|
||||||
func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
|
func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
|
||||||
|
|
@ -205,3 +245,29 @@ func (m *Message) KeyboardMacroState() (KeyboardMacroState, error) {
|
||||||
IsPaste: m.d[1] == uint8(1),
|
IsPaste: m.d[1] == uint8(1),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KeyboardMacroTokenState struct {
|
||||||
|
Token uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardMacroTokenState returns the keyboard macro token UUID from the message.
|
||||||
|
func (m *Message) KeyboardMacroTokenState() (KeyboardMacroTokenState, error) {
|
||||||
|
if m.t != TypeKeyboardMacroTokenState {
|
||||||
|
return KeyboardMacroTokenState{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.d) == 0 {
|
||||||
|
return KeyboardMacroTokenState{Token: uuid.Nil}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.d) != 16 {
|
||||||
|
return KeyboardMacroTokenState{}, fmt.Errorf("invalid UUID length: %d", len(m.d))
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := uuid.FromBytes(m.d)
|
||||||
|
if err != nil {
|
||||||
|
return KeyboardMacroTokenState{}, fmt.Errorf("invalid UUID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyboardMacroTokenState{Token: token}, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -405,8 +405,8 @@ char *jetkvm_video_log_status() {
|
||||||
return (char *)videoc_log_status();
|
return (char *)videoc_log_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
int jetkvm_video_init() {
|
int jetkvm_video_init(float factor) {
|
||||||
return video_init();
|
return video_init(factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void jetkvm_video_shutdown() {
|
void jetkvm_video_shutdown() {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ const char *jetkvm_ui_get_lvgl_version();
|
||||||
|
|
||||||
const char *jetkvm_ui_event_code_to_name(int code);
|
const char *jetkvm_ui_event_code_to_name(int code);
|
||||||
|
|
||||||
int jetkvm_video_init();
|
int jetkvm_video_init(float quality_factor);
|
||||||
void jetkvm_video_shutdown();
|
void jetkvm_video_shutdown();
|
||||||
void jetkvm_video_start();
|
void jetkvm_video_start();
|
||||||
void jetkvm_video_stop();
|
void jetkvm_video_stop();
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
#define VIDEO_DEV "/dev/video0"
|
#define VIDEO_DEV "/dev/video0"
|
||||||
#define SUB_DEV "/dev/v4l-subdev2"
|
#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(x, a) (((x) + (a)-1) & ~((a)-1))
|
||||||
#define RK_ALIGN_2(x) RK_ALIGN(x, 2)
|
#define RK_ALIGN_2(x) RK_ALIGN(x, 2)
|
||||||
|
|
@ -39,6 +40,7 @@ int sub_dev_fd = -1;
|
||||||
#define VENC_CHANNEL 0
|
#define VENC_CHANNEL 0
|
||||||
MB_POOL memPool = MB_INVALID_POOLID;
|
MB_POOL memPool = MB_INVALID_POOLID;
|
||||||
|
|
||||||
|
bool sleep_mode_available = false;
|
||||||
bool should_exit = false;
|
bool should_exit = false;
|
||||||
float quality_factor = 1.0f;
|
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 */
|
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)
|
double calculate_bitrate(float bitrate_factor, int width, int height)
|
||||||
{
|
{
|
||||||
const int32_t base_bitrate_high = 2000;
|
const int32_t base_bitrate_high = 2000;
|
||||||
|
|
@ -190,8 +231,15 @@ static int32_t buf_init()
|
||||||
|
|
||||||
pthread_t *format_thread = NULL;
|
pthread_t *format_thread = NULL;
|
||||||
|
|
||||||
int video_init()
|
int video_init(float factor)
|
||||||
{
|
{
|
||||||
|
detect_sleep_mode();
|
||||||
|
|
||||||
|
if (factor < 0 || factor > 1) {
|
||||||
|
factor = 1.0f;
|
||||||
|
}
|
||||||
|
quality_factor = factor;
|
||||||
|
|
||||||
if (RK_MPI_SYS_Init() != RK_SUCCESS)
|
if (RK_MPI_SYS_Init() != RK_SUCCESS)
|
||||||
{
|
{
|
||||||
log_error("RK_MPI_SYS_Init failed");
|
log_error("RK_MPI_SYS_Init failed");
|
||||||
|
|
@ -301,11 +349,29 @@ static void *venc_read_stream(void *arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t detected_width, detected_height;
|
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_t *streaming_thread = NULL;
|
||||||
pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER;
|
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)
|
void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename)
|
||||||
{
|
{
|
||||||
FILE *file = fopen(filename, "wb");
|
FILE *file = fopen(filename, "wb");
|
||||||
|
|
@ -319,6 +385,8 @@ void *run_video_stream(void *arg)
|
||||||
|
|
||||||
log_info("running video stream");
|
log_info("running video stream");
|
||||||
|
|
||||||
|
streaming_stopped = false;
|
||||||
|
|
||||||
while (streaming_flag)
|
while (streaming_flag)
|
||||||
{
|
{
|
||||||
if (detected_signal == false)
|
if (detected_signal == false)
|
||||||
|
|
@ -401,7 +469,7 @@ void *run_video_stream(void *arg)
|
||||||
{
|
{
|
||||||
log_error("get mb blk failed!");
|
log_error("get mb blk failed!");
|
||||||
close(video_dev_fd);
|
close(video_dev_fd);
|
||||||
return ;
|
return (void *)errno;
|
||||||
}
|
}
|
||||||
log_info("Got memory block for buffer %d", i);
|
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));
|
log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicitly free V4L2 buffer queue
|
||||||
|
struct v4l2_requestbuffers req_free;
|
||||||
|
memset(&req_free, 0, sizeof(req_free));
|
||||||
|
req_free.count = 0;
|
||||||
|
req_free.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||||
|
req_free.memory = V4L2_MEMORY_DMABUF;
|
||||||
|
|
||||||
|
if (ioctl(video_dev_fd, VIDIOC_REQBUFS, &req_free) < 0)
|
||||||
|
{
|
||||||
|
log_error("Failed to free V4L2 buffers: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
venc_stop();
|
venc_stop();
|
||||||
|
|
||||||
for (int i = 0; i < input_buffer_count; i++)
|
for (int i = 0; i < input_buffer_count; i++)
|
||||||
|
|
@ -553,6 +633,9 @@ void *run_video_stream(void *arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
log_info("video stream thread exiting");
|
log_info("video stream thread exiting");
|
||||||
|
|
||||||
|
streaming_stopped = true;
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,61 +660,80 @@ void video_shutdown()
|
||||||
RK_MPI_MB_DestroyPool(memPool);
|
RK_MPI_MB_DestroyPool(memPool);
|
||||||
}
|
}
|
||||||
log_info("Destroyed memory pool");
|
log_info("Destroyed memory pool");
|
||||||
|
|
||||||
pthread_mutex_destroy(&streaming_mutex);
|
pthread_mutex_destroy(&streaming_mutex);
|
||||||
log_info("Destroyed streaming mutex");
|
log_info("Destroyed streaming mutex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void video_start_streaming()
|
void video_start_streaming()
|
||||||
{
|
{
|
||||||
pthread_mutex_lock(&streaming_mutex);
|
log_info("starting video streaming");
|
||||||
if (streaming_thread != NULL)
|
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");
|
log_warn("video streaming already started");
|
||||||
goto cleanup;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_t *new_thread = malloc(sizeof(pthread_t));
|
pthread_t *new_thread = malloc(sizeof(pthread_t));
|
||||||
if (new_thread == NULL)
|
if (new_thread == NULL)
|
||||||
{
|
{
|
||||||
log_error("Failed to allocate memory for streaming thread");
|
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);
|
int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
log_error("Failed to create streaming thread: %s", strerror(result));
|
log_error("Failed to create streaming thread: %s", strerror(result));
|
||||||
streaming_flag = false;
|
set_streaming_flag(false);
|
||||||
free(new_thread);
|
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;
|
streaming_thread = new_thread;
|
||||||
cleanup:
|
|
||||||
pthread_mutex_unlock(&streaming_mutex);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void video_stop_streaming()
|
void video_stop_streaming()
|
||||||
{
|
{
|
||||||
pthread_mutex_lock(&streaming_mutex);
|
if (streaming_thread == NULL) {
|
||||||
if (streaming_thread != NULL)
|
log_info("video streaming already stopped");
|
||||||
{
|
return;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
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)
|
void *run_detect_format(void *arg)
|
||||||
|
|
@ -650,6 +752,8 @@ void *run_detect_format(void *arg)
|
||||||
|
|
||||||
while (!should_exit)
|
while (!should_exit)
|
||||||
{
|
{
|
||||||
|
ensure_sleep_mode_disabled();
|
||||||
|
|
||||||
memset(&dv_timings, 0, sizeof(dv_timings));
|
memset(&dv_timings, 0, sizeof(dv_timings));
|
||||||
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0)
|
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.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
|
||||||
dv_timings.bt.hbackporch));
|
dv_timings.bt.hbackporch));
|
||||||
log_info("Frames per second: %.2f fps", frames_per_second);
|
log_info("Frames per second: %.2f fps", frames_per_second);
|
||||||
|
|
||||||
|
bool should_restart = dv_timings.bt.width != detected_width || dv_timings.bt.height != detected_height || !detected_signal;
|
||||||
|
|
||||||
detected_width = dv_timings.bt.width;
|
detected_width = dv_timings.bt.width;
|
||||||
detected_height = dv_timings.bt.height;
|
detected_height = dv_timings.bt.height;
|
||||||
detected_signal = true;
|
detected_signal = true;
|
||||||
video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
|
video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
|
||||||
pthread_mutex_lock(&streaming_mutex);
|
|
||||||
if (streaming_flag == true)
|
if (should_restart) {
|
||||||
{
|
log_info("restarting video streaming due to format change");
|
||||||
pthread_mutex_unlock(&streaming_mutex);
|
video_restart_streaming();
|
||||||
log_info("restarting on going video streaming");
|
|
||||||
video_stop_streaming();
|
|
||||||
video_start_streaming();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pthread_mutex_unlock(&streaming_mutex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -731,21 +831,9 @@ void video_set_quality_factor(float factor)
|
||||||
quality_factor = factor;
|
quality_factor = factor;
|
||||||
|
|
||||||
// TODO: update venc bitrate without stopping streaming
|
// TODO: update venc bitrate without stopping streaming
|
||||||
|
video_restart_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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float video_get_quality_factor() {
|
float video_get_quality_factor() {
|
||||||
return quality_factor;
|
return quality_factor;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*
|
*
|
||||||
* @return int 0 on success, -1 on failure
|
* @return int 0 on success, -1 on failure
|
||||||
*/
|
*/
|
||||||
int video_init();
|
int video_init(float quality_factor);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Shutdown the video subsystem
|
* @brief Shutdown the video subsystem
|
||||||
|
|
|
||||||
|
|
@ -129,11 +129,13 @@ func uiTick() {
|
||||||
C.jetkvm_ui_tick()
|
C.jetkvm_ui_tick()
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoInit() error {
|
func videoInit(factor float64) error {
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
ret := C.jetkvm_video_init()
|
factorC := C.float(factor)
|
||||||
|
|
||||||
|
ret := C.jetkvm_video_init(factorC)
|
||||||
if ret != 0 {
|
if ret != 0 {
|
||||||
return fmt.Errorf("failed to initialize video: %d", ret)
|
return fmt.Errorf("failed to initialize video: %d", ret)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type Native struct {
|
||||||
systemVersion *semver.Version
|
systemVersion *semver.Version
|
||||||
appVersion *semver.Version
|
appVersion *semver.Version
|
||||||
displayRotation uint16
|
displayRotation uint16
|
||||||
|
defaultQualityFactor float64
|
||||||
onVideoStateChange func(state VideoState)
|
onVideoStateChange func(state VideoState)
|
||||||
onVideoFrameReceived func(frame []byte, duration time.Duration)
|
onVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||||
onIndevEvent func(event string)
|
onIndevEvent func(event string)
|
||||||
|
|
@ -22,12 +23,14 @@ type Native struct {
|
||||||
sleepModeSupported bool
|
sleepModeSupported bool
|
||||||
videoLock sync.Mutex
|
videoLock sync.Mutex
|
||||||
screenLock sync.Mutex
|
screenLock sync.Mutex
|
||||||
|
extraLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type NativeOptions struct {
|
type NativeOptions struct {
|
||||||
SystemVersion *semver.Version
|
SystemVersion *semver.Version
|
||||||
AppVersion *semver.Version
|
AppVersion *semver.Version
|
||||||
DisplayRotation uint16
|
DisplayRotation uint16
|
||||||
|
DefaultQualityFactor float64
|
||||||
OnVideoStateChange func(state VideoState)
|
OnVideoStateChange func(state VideoState)
|
||||||
OnVideoFrameReceived func(frame []byte, duration time.Duration)
|
OnVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||||
OnIndevEvent func(event string)
|
OnIndevEvent func(event string)
|
||||||
|
|
@ -65,6 +68,11 @@ func NewNative(opts NativeOptions) *Native {
|
||||||
|
|
||||||
sleepModeSupported := isSleepModeSupported()
|
sleepModeSupported := isSleepModeSupported()
|
||||||
|
|
||||||
|
defaultQualityFactor := opts.DefaultQualityFactor
|
||||||
|
if defaultQualityFactor < 0 || defaultQualityFactor > 1 {
|
||||||
|
defaultQualityFactor = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
return &Native{
|
return &Native{
|
||||||
ready: make(chan struct{}),
|
ready: make(chan struct{}),
|
||||||
l: nativeLogger,
|
l: nativeLogger,
|
||||||
|
|
@ -72,6 +80,7 @@ func NewNative(opts NativeOptions) *Native {
|
||||||
systemVersion: opts.SystemVersion,
|
systemVersion: opts.SystemVersion,
|
||||||
appVersion: opts.AppVersion,
|
appVersion: opts.AppVersion,
|
||||||
displayRotation: opts.DisplayRotation,
|
displayRotation: opts.DisplayRotation,
|
||||||
|
defaultQualityFactor: defaultQualityFactor,
|
||||||
onVideoStateChange: onVideoStateChange,
|
onVideoStateChange: onVideoStateChange,
|
||||||
onVideoFrameReceived: onVideoFrameReceived,
|
onVideoFrameReceived: onVideoFrameReceived,
|
||||||
onIndevEvent: onIndevEvent,
|
onIndevEvent: onIndevEvent,
|
||||||
|
|
@ -97,7 +106,7 @@ func (n *Native) Start() {
|
||||||
n.initUI()
|
n.initUI()
|
||||||
go n.tickUI()
|
go n.tickUI()
|
||||||
|
|
||||||
if err := videoInit(); err != nil {
|
if err := videoInit(n.defaultQualityFactor); err != nil {
|
||||||
n.l.Error().Err(err).Msg("failed to initialize video")
|
n.l.Error().Err(err).Msg("failed to initialize video")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
package native
|
package native
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
|
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
|
||||||
|
|
||||||
|
// 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.
|
// VideoState is the state of the video stream.
|
||||||
type VideoState struct {
|
type VideoState struct {
|
||||||
Ready bool `json:"ready"`
|
Ready bool `json:"ready"`
|
||||||
|
|
@ -66,12 +73,32 @@ func (n *Native) VideoSleepModeSupported() bool {
|
||||||
return n.sleepModeSupported
|
return n.sleepModeSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// useExtraLock uses the extra lock to execute a function.
|
||||||
|
// if the lock is currently held by another goroutine, returns an error.
|
||||||
|
//
|
||||||
|
// it's used to ensure that only one change is made to the video stream at a time.
|
||||||
|
// as the change usually requires to restart video streaming
|
||||||
|
// TODO: check video streaming status instead of using a hardcoded timeout
|
||||||
|
func (n *Native) useExtraLock(fn func() error) error {
|
||||||
|
if !n.extraLock.TryLock() {
|
||||||
|
return fmt.Errorf("the previous change hasn't been completed yet")
|
||||||
|
}
|
||||||
|
err := fn()
|
||||||
|
if err == nil {
|
||||||
|
time.Sleep(extraLockTimeout)
|
||||||
|
}
|
||||||
|
n.extraLock.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// VideoSetQualityFactor sets the quality factor for the video stream.
|
// VideoSetQualityFactor sets the quality factor for the video stream.
|
||||||
func (n *Native) VideoSetQualityFactor(factor float64) error {
|
func (n *Native) VideoSetQualityFactor(factor float64) error {
|
||||||
n.videoLock.Lock()
|
n.videoLock.Lock()
|
||||||
defer n.videoLock.Unlock()
|
defer n.videoLock.Unlock()
|
||||||
|
|
||||||
return videoSetStreamQualityFactor(factor)
|
return n.useExtraLock(func() error {
|
||||||
|
return videoSetStreamQualityFactor(factor)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// VideoGetQualityFactor gets the quality factor for the video stream.
|
// VideoGetQualityFactor gets the quality factor for the video stream.
|
||||||
|
|
@ -87,7 +114,13 @@ func (n *Native) VideoSetEDID(edid string) error {
|
||||||
n.videoLock.Lock()
|
n.videoLock.Lock()
|
||||||
defer n.videoLock.Unlock()
|
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.
|
// VideoGetEDID gets the EDID for the video stream.
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ var keyboardReportDesc = []byte{
|
||||||
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
|
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
|
||||||
0x09, 0x06, /* USAGE (Keyboard) */
|
0x09, 0x06, /* USAGE (Keyboard) */
|
||||||
0xa1, 0x01, /* COLLECTION (Application) */
|
0xa1, 0x01, /* COLLECTION (Application) */
|
||||||
|
|
||||||
|
/* 8 modifier bits */
|
||||||
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
||||||
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
|
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
|
||||||
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
|
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
|
||||||
|
|
@ -39,27 +41,47 @@ var keyboardReportDesc = []byte{
|
||||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||||
0x95, 0x08, /* REPORT_COUNT (8) */
|
0x95, 0x08, /* REPORT_COUNT (8) */
|
||||||
0x81, 0x02, /* INPUT (Data,Var,Abs) */
|
0x81, 0x02, /* INPUT (Data,Var,Abs) */
|
||||||
|
|
||||||
|
/* 8 bits of padding */
|
||||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||||
0x75, 0x08, /* REPORT_SIZE (8) */
|
0x75, 0x08, /* REPORT_SIZE (8) */
|
||||||
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
||||||
|
|
||||||
|
/* 6 key codes for the 104 key keyboard */
|
||||||
|
0x95, 0x06, /* REPORT_COUNT (6) */
|
||||||
|
0x75, 0x08, /* REPORT_SIZE (8) */
|
||||||
|
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
|
||||||
|
0x25, 0xE7, /* LOGICAL_MAXIMUM (104-key HID) */
|
||||||
|
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
||||||
|
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
|
||||||
|
0x29, 0xE7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
|
||||||
|
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
|
||||||
|
|
||||||
|
/* LED report 5 bits for Num Lock through Kana */
|
||||||
0x95, 0x05, /* REPORT_COUNT (5) */
|
0x95, 0x05, /* REPORT_COUNT (5) */
|
||||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||||
|
|
||||||
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
||||||
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
||||||
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
||||||
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
|
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
|
||||||
|
|
||||||
|
/* 1 bit of padding for the Power LED (ignored) */
|
||||||
|
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||||
|
0x75, 0x03, /* REPORT_SIZE (3) */
|
||||||
|
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
|
||||||
|
|
||||||
|
/* LED report 1 bit for Shift */
|
||||||
|
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||||
|
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||||
|
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
||||||
|
0x19, 0x07, /* USAGE_MINIMUM (Shift) */
|
||||||
|
0x29, 0x07, /* USAGE_MAXIMUM (Shift) */
|
||||||
|
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
|
||||||
|
|
||||||
|
/* 1 bit of padding for the rest of the byte */
|
||||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||||
0x75, 0x03, /* REPORT_SIZE (3) */
|
0x75, 0x03, /* REPORT_SIZE (3) */
|
||||||
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
|
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
|
||||||
0x95, 0x06, /* REPORT_COUNT (6) */
|
|
||||||
0x75, 0x08, /* REPORT_SIZE (8) */
|
|
||||||
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
|
|
||||||
0x25, 0x65, /* LOGICAL_MAXIMUM (101) */
|
|
||||||
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
|
||||||
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
|
|
||||||
0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
|
|
||||||
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
|
|
||||||
0xc0, /* END_COLLECTION */
|
0xc0, /* END_COLLECTION */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,6 +175,16 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
|
||||||
u.onKeysDownChange = &f
|
u.onKeysDownChange = &f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var suspendedKeyDownMessages bool = false
|
||||||
|
|
||||||
|
func (u *UsbGadget) SuspendKeyDownMessages() {
|
||||||
|
suspendedKeyDownMessages = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) ResumeSuspendKeyDownMessages() {
|
||||||
|
suspendedKeyDownMessages = false
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
|
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
|
||||||
u.onKeepAliveReset = &f
|
u.onKeepAliveReset = &f
|
||||||
}
|
}
|
||||||
|
|
@ -169,9 +201,9 @@ func (u *UsbGadget) scheduleAutoRelease(key byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
// We currently hardcode the duration to 100ms
|
// We currently hardcode the duration to the default of 100ms
|
||||||
// However, it should be the same as the duration of the keep-alive reset called baseExtension.
|
// However, it should be the same as the duration of the keep-alive reset called baseExtension.
|
||||||
u.kbdAutoReleaseTimers[key] = time.AfterFunc(100*time.Millisecond, func() {
|
u.kbdAutoReleaseTimers[key] = time.AfterFunc(DefaultAutoReleaseDuration, func() {
|
||||||
u.performAutoRelease(key)
|
u.performAutoRelease(key)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -314,6 +346,7 @@ var keyboardWriteHidFileLock sync.Mutex
|
||||||
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
|
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
|
||||||
keyboardWriteHidFileLock.Lock()
|
keyboardWriteHidFileLock.Lock()
|
||||||
defer keyboardWriteHidFileLock.Unlock()
|
defer keyboardWriteHidFileLock.Unlock()
|
||||||
|
|
||||||
if err := u.openKeyboardHidFile(); err != nil {
|
if err := u.openKeyboardHidFile(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +386,7 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
|
||||||
u.keysDownState = state
|
u.keysDownState = state
|
||||||
u.keyboardStateLock.Unlock()
|
u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
if u.onKeysDownChange != nil {
|
if u.onKeysDownChange != nil && !suspendedKeyDownMessages {
|
||||||
(*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...)
|
(*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
@ -484,6 +517,10 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := u.keyboardWriteHidFile(modifier, keys)
|
err := u.keyboardWriteHidFile(modifier, keys)
|
||||||
|
if err != nil {
|
||||||
|
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0")
|
||||||
|
}
|
||||||
|
|
||||||
return u.UpdateKeysDown(modifier, keys), err
|
return u.UpdateKeysDown(modifier, keys), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
154
jsonrpc.go
154
jsonrpc.go
|
|
@ -1,7 +1,6 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -14,6 +13,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"go.bug.st/serial"
|
"go.bug.st/serial"
|
||||||
|
|
@ -243,7 +243,6 @@ func rpcGetEDID() (string, error) {
|
||||||
func rpcSetEDID(edid string) error {
|
func rpcSetEDID(edid string) error {
|
||||||
if edid == "" {
|
if edid == "" {
|
||||||
logger.Info().Msg("Restoring EDID to default")
|
logger.Info().Msg("Restoring EDID to default")
|
||||||
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
|
|
||||||
} else {
|
} else {
|
||||||
logger.Info().Str("edid", edid).Msg("Setting EDID")
|
logger.Info().Str("edid", edid).Msg("Setting EDID")
|
||||||
}
|
}
|
||||||
|
|
@ -1089,91 +1088,154 @@ func rpcSetLocalLoopbackOnly(enabled bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RunningMacro struct {
|
||||||
|
cancel context.CancelFunc
|
||||||
|
isPaste bool
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
keyboardMacroCancel context.CancelFunc
|
keyboardMacroCancelMap map[uuid.UUID]RunningMacro
|
||||||
keyboardMacroLock sync.Mutex
|
keyboardMacroLock sync.Mutex
|
||||||
|
keyboardMacroOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// cancelKeyboardMacro cancels any ongoing keyboard macro execution
|
func getKeyboardMacroCancelMap() map[uuid.UUID]RunningMacro {
|
||||||
func cancelKeyboardMacro() {
|
keyboardMacroOnce.Do(func() {
|
||||||
|
keyboardMacroCancelMap = make(map[uuid.UUID]RunningMacro)
|
||||||
|
})
|
||||||
|
return keyboardMacroCancelMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func addKeyboardMacro(isPaste bool, cancel context.CancelFunc) uuid.UUID {
|
||||||
keyboardMacroLock.Lock()
|
keyboardMacroLock.Lock()
|
||||||
defer keyboardMacroLock.Unlock()
|
defer keyboardMacroLock.Unlock()
|
||||||
|
cancelMap := getKeyboardMacroCancelMap()
|
||||||
|
|
||||||
if keyboardMacroCancel != nil {
|
token := uuid.New() // Generate a unique token
|
||||||
keyboardMacroCancel()
|
cancelMap[token] = RunningMacro{
|
||||||
logger.Info().Msg("canceled keyboard macro")
|
isPaste: isPaste,
|
||||||
keyboardMacroCancel = nil
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeRunningKeyboardMacro(token uuid.UUID) {
|
||||||
|
keyboardMacroLock.Lock()
|
||||||
|
defer keyboardMacroLock.Unlock()
|
||||||
|
cancelMap := getKeyboardMacroCancelMap()
|
||||||
|
|
||||||
|
delete(cancelMap, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelRunningKeyboardMacro(token uuid.UUID) {
|
||||||
|
keyboardMacroLock.Lock()
|
||||||
|
defer keyboardMacroLock.Unlock()
|
||||||
|
cancelMap := getKeyboardMacroCancelMap()
|
||||||
|
|
||||||
|
if runningMacro, exists := cancelMap[token]; exists {
|
||||||
|
runningMacro.cancel()
|
||||||
|
delete(cancelMap, token)
|
||||||
|
logger.Info().Interface("token", token).Msg("canceled keyboard macro by token")
|
||||||
|
} else {
|
||||||
|
logger.Debug().Interface("token", token).Msg("no running keyboard macro found for token")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setKeyboardMacroCancel(cancel context.CancelFunc) {
|
func cancelAllRunningKeyboardMacros() {
|
||||||
keyboardMacroLock.Lock()
|
keyboardMacroLock.Lock()
|
||||||
defer keyboardMacroLock.Unlock()
|
defer keyboardMacroLock.Unlock()
|
||||||
|
cancelMap := getKeyboardMacroCancelMap()
|
||||||
|
|
||||||
keyboardMacroCancel = cancel
|
for token, runningMacro := range cancelMap {
|
||||||
|
runningMacro.cancel()
|
||||||
|
delete(cancelMap, token)
|
||||||
|
logger.Info().Interface("token", token).Msg("cancelled keyboard macro")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) error {
|
func reportRunningMacrosState() {
|
||||||
cancelKeyboardMacro()
|
if currentSession != nil {
|
||||||
|
keyboardMacroLock.Lock()
|
||||||
|
defer keyboardMacroLock.Unlock()
|
||||||
|
cancelMap := getKeyboardMacroCancelMap()
|
||||||
|
|
||||||
|
isPaste := false
|
||||||
|
for _, runningMacro := range cancelMap {
|
||||||
|
if runningMacro.isPaste {
|
||||||
|
isPaste = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state := hidrpc.KeyboardMacroState{
|
||||||
|
State: len(cancelMap) > 0,
|
||||||
|
IsPaste: isPaste,
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession.reportHidRPCKeyboardMacroState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcExecuteKeyboardMacro(isPaste bool, macro []hidrpc.KeyboardMacroStep) uuid.UUID {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
setKeyboardMacroCancel(cancel)
|
token := addKeyboardMacro(isPaste, cancel)
|
||||||
|
reportRunningMacrosState()
|
||||||
|
|
||||||
s := hidrpc.KeyboardMacroState{
|
go func() {
|
||||||
State: true,
|
defer reportRunningMacrosState() // this executes last, so the map is already updated
|
||||||
IsPaste: true,
|
defer removeRunningKeyboardMacro(token) // this executes first, to update the map
|
||||||
}
|
|
||||||
|
|
||||||
if currentSession != nil {
|
err := executeKeyboardMacro(ctx, isPaste, macro)
|
||||||
currentSession.reportHidRPCKeyboardMacroState(s)
|
if err != nil {
|
||||||
}
|
logger.Error().Err(err).Interface("token", token).Bool("isPaste", isPaste).Msg("keyboard macro execution failed")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
err := rpcDoExecuteKeyboardMacro(ctx, macro)
|
return token
|
||||||
|
|
||||||
setKeyboardMacroCancel(nil)
|
|
||||||
|
|
||||||
s.State = false
|
|
||||||
if currentSession != nil {
|
|
||||||
currentSession.reportHidRPCKeyboardMacroState(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcCancelKeyboardMacro() {
|
func rpcCancelKeyboardMacro() {
|
||||||
cancelKeyboardMacro()
|
defer reportRunningMacrosState()
|
||||||
|
cancelAllRunningKeyboardMacros()
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyboardClearStateKeys = make([]byte, hidrpc.HidKeyBufferSize)
|
func rpcCancelKeyboardMacroByToken(token uuid.UUID) {
|
||||||
|
defer reportRunningMacrosState()
|
||||||
|
|
||||||
func isClearKeyStep(step hidrpc.KeyboardMacroStep) bool {
|
if token == uuid.Nil {
|
||||||
return step.Modifier == 0 && bytes.Equal(step.Keys, keyboardClearStateKeys)
|
cancelAllRunningKeyboardMacros()
|
||||||
|
} else {
|
||||||
|
cancelRunningKeyboardMacro(token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) error {
|
func executeKeyboardMacro(ctx context.Context, isPaste bool, macro []hidrpc.KeyboardMacroStep) error {
|
||||||
logger.Debug().Interface("macro", macro).Msg("Executing keyboard macro")
|
logger.Debug().
|
||||||
|
Int("macro_steps", len(macro)).
|
||||||
|
Bool("isPaste", isPaste).
|
||||||
|
Msg("Executing keyboard macro")
|
||||||
|
|
||||||
|
// don't report keyboard state changes while executing the macro
|
||||||
|
gadget.SuspendKeyDownMessages()
|
||||||
|
defer gadget.ResumeSuspendKeyDownMessages()
|
||||||
|
|
||||||
for i, step := range macro {
|
for i, step := range macro {
|
||||||
delay := time.Duration(step.Delay) * time.Millisecond
|
delay := time.Duration(step.Delay) * time.Millisecond
|
||||||
|
|
||||||
err := rpcKeyboardReport(step.Modifier, step.Keys)
|
err := rpcKeyboardReport(step.Modifier, step.Keys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to execute keyboard macro")
|
logger.Warn().Err(err).Int("step", i).Msg("failed to execute keyboard macro")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify the device that the keyboard state is being cleared
|
|
||||||
if isClearKeyStep(step) {
|
|
||||||
gadget.UpdateKeysDown(0, keyboardClearStateKeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use context-aware sleep that can be cancelled
|
// Use context-aware sleep that can be cancelled
|
||||||
select {
|
select {
|
||||||
case <-time.After(delay):
|
case <-time.After(delay):
|
||||||
// Sleep completed normally
|
// Sleep completed normally
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// make sure keyboard state is reset
|
// make sure keyboard state is reset and the client gets notified
|
||||||
err := rpcKeyboardReport(0, keyboardClearStateKeys)
|
gadget.ResumeSuspendKeyDownMessages()
|
||||||
|
err := rpcKeyboardReport(0, make([]byte, hidrpc.HidKeyBufferSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to reset keyboard state")
|
logger.Warn().Err(err).Msg("failed to reset keyboard state")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
native.go
13
native.go
|
|
@ -17,9 +17,10 @@ var (
|
||||||
|
|
||||||
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
||||||
nativeInstance = native.NewNative(native.NativeOptions{
|
nativeInstance = native.NewNative(native.NativeOptions{
|
||||||
SystemVersion: systemVersion,
|
SystemVersion: systemVersion,
|
||||||
AppVersion: appVersion,
|
AppVersion: appVersion,
|
||||||
DisplayRotation: config.GetDisplayRotation(),
|
DisplayRotation: config.GetDisplayRotation(),
|
||||||
|
DefaultQualityFactor: config.VideoQualityFactor,
|
||||||
OnVideoStateChange: func(state native.VideoState) {
|
OnVideoStateChange: func(state native.VideoState) {
|
||||||
lastVideoState = state
|
lastVideoState = state
|
||||||
triggerVideoStateUpdate()
|
triggerVideoStateUpdate()
|
||||||
|
|
@ -58,7 +59,13 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
nativeInstance.Start()
|
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" {
|
if os.Getenv("JETKVM_CRASH_TESTING") == "1" {
|
||||||
nativeInstance.DoNotUseThisIsForCrashTestingOnly()
|
nativeInstance.DoNotUseThisIsForCrashTestingOnly()
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"validator": "^13.15.15",
|
"validator": "^13.15.15",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
|
|
@ -6907,6 +6908,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist-node/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/validator": {
|
"node_modules/validator": {
|
||||||
"version": "13.15.15",
|
"version": "13.15.15",
|
||||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"validator": "^13.15.15",
|
"validator": "^13.15.15",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -188,18 +188,18 @@ export default function PasteModal() {
|
||||||
type="number"
|
type="number"
|
||||||
label="Delay between keys"
|
label="Delay between keys"
|
||||||
placeholder="Delay between keys"
|
placeholder="Delay between keys"
|
||||||
min={50}
|
min={0}
|
||||||
max={65534}
|
max={65534}
|
||||||
value={delayValue}
|
value={delayValue}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setDelayValue(parseInt(e.target.value, 10));
|
setDelayValue(parseInt(e.target.value, 10));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{delayValue < 50 || delayValue > 65534 && (
|
{(delayValue < defaultDelay || delayValue > 65534) && (
|
||||||
<div className="mt-2 flex items-center gap-x-2">
|
<div className="mt-2 flex items-center gap-x-2">
|
||||||
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
||||||
<span className="text-xs text-red-500 dark:text-red-400">
|
<span className="text-xs text-red-500 dark:text-red-400">
|
||||||
Delay must be between 50 and 65534
|
Delay should be between 20 and 65534
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { parse as uuidParse , stringify as uuidStringify } from "uuid";
|
||||||
|
|
||||||
import { hidKeyBufferSize, KeyboardLedState, KeysDownState } from "./stores";
|
import { hidKeyBufferSize, KeyboardLedState, KeysDownState } from "./stores";
|
||||||
|
|
||||||
export const HID_RPC_MESSAGE_TYPES = {
|
export const HID_RPC_MESSAGE_TYPES = {
|
||||||
|
|
@ -13,6 +15,7 @@ export const HID_RPC_MESSAGE_TYPES = {
|
||||||
KeyboardLedState: 0x32,
|
KeyboardLedState: 0x32,
|
||||||
KeysDownState: 0x33,
|
KeysDownState: 0x33,
|
||||||
KeyboardMacroState: 0x34,
|
KeyboardMacroState: 0x34,
|
||||||
|
CancelKeyboardMacroByTokenReport: 0x35,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES];
|
export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES];
|
||||||
|
|
@ -299,7 +302,7 @@ export class KeyboardMacroStateMessage extends RpcMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unmarshal(data: Uint8Array): KeyboardMacroStateMessage | undefined {
|
public static unmarshal(data: Uint8Array): KeyboardMacroStateMessage | undefined {
|
||||||
if (data.length < 1) {
|
if (data.length < 2) {
|
||||||
throw new Error(`Invalid keyboard macro state report message length: ${data.length}`);
|
throw new Error(`Invalid keyboard macro state report message length: ${data.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,13 +381,30 @@ export class PointerReportMessage extends RpcMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CancelKeyboardMacroReportMessage extends RpcMessage {
|
export class CancelKeyboardMacroReportMessage extends RpcMessage {
|
||||||
|
token: string;
|
||||||
|
|
||||||
constructor() {
|
constructor(token: string) {
|
||||||
super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport);
|
super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport);
|
||||||
|
this.token = (token == null || token === undefined || token === "")
|
||||||
|
? "00000000-0000-0000-0000-000000000000"
|
||||||
|
: token;
|
||||||
}
|
}
|
||||||
|
|
||||||
marshal(): Uint8Array {
|
marshal(): Uint8Array {
|
||||||
return new Uint8Array([this.messageType]);
|
const tokenBytes = uuidParse(this.token);
|
||||||
|
return new Uint8Array([this.messageType, ...tokenBytes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unmarshal(data: Uint8Array): CancelKeyboardMacroReportMessage | undefined {
|
||||||
|
if (data.length == 0) {
|
||||||
|
return new CancelKeyboardMacroReportMessage("00000000-0000-0000-0000-000000000000");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length != 16) {
|
||||||
|
throw new Error(`Invalid cancel message length: ${data.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CancelKeyboardMacroReportMessage(uuidStringify(data.slice(0, 16)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,6 +450,7 @@ export const messageRegistry = {
|
||||||
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport]: CancelKeyboardMacroReportMessage,
|
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport]: CancelKeyboardMacroReportMessage,
|
||||||
[HID_RPC_MESSAGE_TYPES.KeyboardMacroState]: KeyboardMacroStateMessage,
|
[HID_RPC_MESSAGE_TYPES.KeyboardMacroState]: KeyboardMacroStateMessage,
|
||||||
[HID_RPC_MESSAGE_TYPES.KeypressKeepAliveReport]: KeypressKeepAliveMessage,
|
[HID_RPC_MESSAGE_TYPES.KeypressKeepAliveReport]: KeypressKeepAliveMessage,
|
||||||
|
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroByTokenReport]: CancelKeyboardMacroReportMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => {
|
export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => {
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
||||||
|
|
||||||
const cancelOngoingKeyboardMacro = useCallback(
|
const cancelOngoingKeyboardMacro = useCallback(
|
||||||
() => {
|
() => {
|
||||||
sendMessage(new CancelKeyboardMacroReportMessage());
|
sendMessage(new CancelKeyboardMacroReportMessage(""));
|
||||||
},
|
},
|
||||||
[sendMessage],
|
[sendMessage],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,6 @@ export default function useKeyboard() {
|
||||||
cancelKeepAlive();
|
cancelKeepAlive();
|
||||||
}, [cancelKeepAlive]);
|
}, [cancelKeepAlive]);
|
||||||
|
|
||||||
|
|
||||||
// executeMacro is used to execute a macro consisting of multiple steps.
|
// executeMacro is used to execute a macro consisting of multiple steps.
|
||||||
// Each step can have multiple keys, multiple modifiers and a delay.
|
// Each step can have multiple keys, multiple modifiers and a delay.
|
||||||
// The keys and modifiers are pressed together and held for the delay duration.
|
// The keys and modifiers are pressed together and held for the delay duration.
|
||||||
|
|
@ -292,9 +291,7 @@ export default function useKeyboard() {
|
||||||
for (const [_, step] of steps.entries()) {
|
for (const [_, step] of steps.entries()) {
|
||||||
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||||
const modifierMask: number = (step.modifiers || [])
|
const modifierMask: number = (step.modifiers || [])
|
||||||
|
|
||||||
.map(mod => modifiers[mod])
|
.map(mod => modifiers[mod])
|
||||||
|
|
||||||
.reduce((acc, val) => acc + val, 0);
|
.reduce((acc, val) => acc + val, 0);
|
||||||
|
|
||||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||||
|
|
@ -306,6 +303,7 @@ export default function useKeyboard() {
|
||||||
|
|
||||||
sendKeyboardMacroEventHidRpc(macro);
|
sendKeyboardMacroEventHidRpc(macro);
|
||||||
}, [sendKeyboardMacroEventHidRpc]);
|
}, [sendKeyboardMacroEventHidRpc]);
|
||||||
|
|
||||||
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
|
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
|
||||||
const promises: (() => Promise<void>)[] = [];
|
const promises: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { SettingsItem } from "@components/SettingsItem";
|
import { SettingsItem } from "@components/SettingsItem";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||||
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||||
|
import { Checkbox } from "@components/Checkbox";
|
||||||
|
|
||||||
import notifications from "../notifications";
|
import notifications from "../notifications";
|
||||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||||
|
|
@ -15,6 +17,7 @@ export default function SettingsHardwareRoute() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
const { setDisplayRotation } = useSettingsStore();
|
const { setDisplayRotation } = useSettingsStore();
|
||||||
|
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
|
||||||
|
|
||||||
const handleDisplayRotationChange = (rotation: string) => {
|
const handleDisplayRotationChange = (rotation: string) => {
|
||||||
setDisplayRotation(rotation);
|
setDisplayRotation(rotation);
|
||||||
|
|
@ -58,6 +61,21 @@ export default function SettingsHardwareRoute() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
|
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
|
|
@ -70,6 +88,17 @@ export default function SettingsHardwareRoute() {
|
||||||
});
|
});
|
||||||
}, [send, setBacklightSettings]);
|
}, [send, setBacklightSettings]);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
|
|
@ -167,6 +196,26 @@ export default function SettingsHardwareRoute() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<FeatureFlag minAppVersion="0.3.8">
|
||||||
<UsbDeviceSetting />
|
<UsbDeviceSetting />
|
||||||
</FeatureFlag>
|
</FeatureFlag>
|
||||||
|
|
|
||||||
2
web.go
2
web.go
|
|
@ -230,7 +230,7 @@ func handleWebRTCSession(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel any ongoing keyboard macro when session changes
|
// Cancel any ongoing keyboard macro when session changes
|
||||||
cancelKeyboardMacro()
|
cancelAllRunningKeyboardMacros()
|
||||||
|
|
||||||
currentSession = session
|
currentSession = session
|
||||||
c.JSON(http.StatusOK, gin.H{"sd": sd})
|
c.JSON(http.StatusOK, gin.H{"sd": sd})
|
||||||
|
|
|
||||||
56
webrtc.go
56
webrtc.go
|
|
@ -34,7 +34,7 @@ type Session struct {
|
||||||
lastTimerResetTime time.Time // Track when auto-release timer was last reset
|
lastTimerResetTime time.Time // Track when auto-release timer was last reset
|
||||||
keepAliveJitterLock sync.Mutex // Protect jitter compensation timing state
|
keepAliveJitterLock sync.Mutex // Protect jitter compensation timing state
|
||||||
hidQueueLock sync.Mutex
|
hidQueueLock sync.Mutex
|
||||||
hidQueue []chan hidQueueMessage
|
hidQueues []chan hidQueueMessage
|
||||||
|
|
||||||
keysDownStateQueue chan usbgadget.KeysDownState
|
keysDownStateQueue chan usbgadget.KeysDownState
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +76,8 @@ func (s *Session) resetKeepAliveTime() {
|
||||||
|
|
||||||
type hidQueueMessage struct {
|
type hidQueueMessage struct {
|
||||||
webrtc.DataChannelMessage
|
webrtc.DataChannelMessage
|
||||||
channel string
|
channel string
|
||||||
|
timelimit time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionConfig struct {
|
type SessionConfig struct {
|
||||||
|
|
@ -121,19 +122,20 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
||||||
return base64.StdEncoding.EncodeToString(localDescription), nil
|
return base64.StdEncoding.EncodeToString(localDescription), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) initQueues() {
|
func (s *Session) initHidQueues() {
|
||||||
s.hidQueueLock.Lock()
|
s.hidQueueLock.Lock()
|
||||||
defer s.hidQueueLock.Unlock()
|
defer s.hidQueueLock.Unlock()
|
||||||
|
|
||||||
s.hidQueue = make([]chan hidQueueMessage, 0)
|
s.hidQueues = make([]chan hidQueueMessage, hidrpc.OtherQueue+1)
|
||||||
for i := 0; i < 4; i++ {
|
s.hidQueues[hidrpc.HandshakeQueue] = make(chan hidQueueMessage, 2) // we don't really want to queue many handshake messages
|
||||||
q := make(chan hidQueueMessage, 256)
|
s.hidQueues[hidrpc.KeyboardQueue] = make(chan hidQueueMessage, 256)
|
||||||
s.hidQueue = append(s.hidQueue, q)
|
s.hidQueues[hidrpc.MouseQueue] = make(chan hidQueueMessage, 256)
|
||||||
}
|
s.hidQueues[hidrpc.MacroQueue] = make(chan hidQueueMessage, 10) // macros can be long, but we don't want to queue too many
|
||||||
|
s.hidQueues[hidrpc.OtherQueue] = make(chan hidQueueMessage, 256)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) handleQueues(index int) {
|
func (s *Session) handleQueue(queue chan hidQueueMessage) {
|
||||||
for msg := range s.hidQueue[index] {
|
for msg := range queue {
|
||||||
onHidMessage(msg, s)
|
onHidMessage(msg, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,17 +190,18 @@ func getOnHidMessageHandler(session *Session, scopedLogger *zerolog.Logger, chan
|
||||||
l.Trace().Msg("received data in HID RPC message handler")
|
l.Trace().Msg("received data in HID RPC message handler")
|
||||||
|
|
||||||
// Enqueue to ensure ordered processing
|
// Enqueue to ensure ordered processing
|
||||||
queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
|
queueIndex, timelimit := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
|
||||||
if queueIndex >= len(session.hidQueue) || queueIndex < 0 {
|
if queueIndex >= len(session.hidQueues) || queueIndex < 0 {
|
||||||
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found")
|
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found")
|
||||||
queueIndex = 3
|
queueIndex = hidrpc.OtherQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
queue := session.hidQueue[queueIndex]
|
queue := session.hidQueues[queueIndex]
|
||||||
if queue != nil {
|
if queue != nil {
|
||||||
queue <- hidQueueMessage{
|
queue <- hidQueueMessage{
|
||||||
DataChannelMessage: msg,
|
DataChannelMessage: msg,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
|
timelimit: timelimit,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil")
|
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil")
|
||||||
|
|
@ -248,7 +251,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
|
|
||||||
session := &Session{peerConnection: peerConnection}
|
session := &Session{peerConnection: peerConnection}
|
||||||
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
|
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
|
||||||
session.initQueues()
|
session.initHidQueues()
|
||||||
session.initKeysDownStateQueue()
|
session.initKeysDownStateQueue()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|
@ -258,8 +261,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for i := 0; i < len(session.hidQueue); i++ {
|
for queue := range session.hidQueues {
|
||||||
go session.handleQueues(i)
|
go session.handleQueue(session.hidQueues[queue])
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||||
|
|
@ -284,7 +287,11 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
session.RPCChannel = d
|
session.RPCChannel = d
|
||||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||||
// Enqueue to ensure ordered processing
|
// Enqueue to ensure ordered processing
|
||||||
session.rpcQueue <- msg
|
if session.rpcQueue != nil {
|
||||||
|
session.rpcQueue <- msg
|
||||||
|
} else {
|
||||||
|
scopedLogger.Warn().Msg("RPC message received but rpcQueue is nil")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
triggerVideoStateUpdate()
|
triggerVideoStateUpdate()
|
||||||
|
|
@ -352,22 +359,23 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
_ = peerConnection.Close()
|
_ = peerConnection.Close()
|
||||||
}
|
}
|
||||||
if connectionState == webrtc.ICEConnectionStateClosed {
|
if connectionState == webrtc.ICEConnectionStateClosed {
|
||||||
scopedLogger.Debug().Msg("ICE Connection State is closed, unmounting virtual media")
|
scopedLogger.Debug().Msg("ICE Connection State is closed, tearing down session")
|
||||||
if session == currentSession {
|
if session == currentSession {
|
||||||
// Cancel any ongoing keyboard report multi when session closes
|
// Cancel any ongoing keyboard report multi when session closes
|
||||||
cancelKeyboardMacro()
|
cancelAllRunningKeyboardMacros()
|
||||||
currentSession = nil
|
currentSession = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop RPC processor
|
// Stop RPC processor
|
||||||
if session.rpcQueue != nil {
|
if session.rpcQueue != nil {
|
||||||
close(session.rpcQueue)
|
close(session.rpcQueue)
|
||||||
session.rpcQueue = nil
|
session.rpcQueue = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop HID RPC processor
|
// Stop HID RPC processors
|
||||||
for i := 0; i < len(session.hidQueue); i++ {
|
for i := 0; i < len(session.hidQueues); i++ {
|
||||||
close(session.hidQueue[i])
|
close(session.hidQueues[i])
|
||||||
session.hidQueue[i] = nil
|
session.hidQueues[i] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
close(session.keysDownStateQueue)
|
close(session.keysDownStateQueue)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue