diff --git a/pkg/api/user.go b/pkg/api/user.go
index aef4305dc..189b61fc9 100644
--- a/pkg/api/user.go
+++ b/pkg/api/user.go
@@ -12,8 +12,9 @@ type (
PlayerIndex int `json:"player_index"`
}
GameStartUserResponse struct {
- RoomId string `json:"roomId"`
- Av *AppVideoInfo `json:"av"`
+ RoomId string `json:"roomId"`
+ Av *AppVideoInfo `json:"av"`
+ KbMouse bool `json:"kb_mouse"`
}
IceServer struct {
Urls string `json:"urls,omitempty"`
diff --git a/pkg/api/worker.go b/pkg/api/worker.go
index a4078f5e2..cd9284346 100644
--- a/pkg/api/worker.go
+++ b/pkg/api/worker.go
@@ -33,8 +33,9 @@ type (
}
StartGameResponse struct {
Room
- AV *AppVideoInfo `json:"av"`
- Record bool
+ AV *AppVideoInfo `json:"av"`
+ Record bool `json:"record"`
+ KbMouse bool `json:"kb_mouse"`
}
RecordGameRequest[T Id] struct {
StatefulRoom[T]
diff --git a/pkg/config/config.yaml b/pkg/config/config.yaml
index 39b695ea8..a0fc99fd5 100644
--- a/pkg/config/config.yaml
+++ b/pkg/config/config.yaml
@@ -186,6 +186,7 @@ emulator:
# A list of device IDs to bind to the input ports.
# Some cores allow binding multiple devices to a single port (DosBox), but typically,
# you should bind just one device to one port.
+ # - kbMouseSupport (bool) -- (temp) a flag if the core needs the keyboard and mouse on the client
# - vfr (bool)
# (experimental)
# Enable variable frame rate only for cores that can't produce a constant frame rate.
diff --git a/pkg/config/emulator.go b/pkg/config/emulator.go
index 49bf6713e..d06bea2c9 100644
--- a/pkg/config/emulator.go
+++ b/pkg/config/emulator.go
@@ -47,6 +47,7 @@ type LibretroCoreConfig struct {
Height int
Hid map[int][]int
IsGlAllowed bool
+ KbMouseSupport bool
Lib string
Options map[string]string
Roms []string
diff --git a/pkg/coordinator/userapi.go b/pkg/coordinator/userapi.go
index ed1ebcead..047fe1d13 100644
--- a/pkg/coordinator/userapi.go
+++ b/pkg/coordinator/userapi.go
@@ -37,6 +37,6 @@ func (u *User) SendWebrtcOffer(sdp string) { u.Notify(api.WebrtcOffer, sdp) }
func (u *User) SendWebrtcIceCandidate(candidate string) { u.Notify(api.WebrtcIce, candidate) }
// StartGame signals the user that everything is ready to start a game.
-func (u *User) StartGame(av *api.AppVideoInfo) {
- u.Notify(api.StartGame, api.GameStartUserResponse{RoomId: u.w.RoomId, Av: av})
+func (u *User) StartGame(av *api.AppVideoInfo, kbMouse bool) {
+ u.Notify(api.StartGame, api.GameStartUserResponse{RoomId: u.w.RoomId, Av: av, KbMouse: kbMouse})
}
diff --git a/pkg/coordinator/userhandlers.go b/pkg/coordinator/userhandlers.go
index 240f9dbe0..cf62d65ba 100644
--- a/pkg/coordinator/userhandlers.go
+++ b/pkg/coordinator/userhandlers.go
@@ -56,7 +56,7 @@ func (u *User) HandleStartGame(rq api.GameStartUserRequest, launcher games.Launc
return
}
u.log.Info().Str("id", startGameResp.Rid).Msg("Received room response from worker")
- u.StartGame(startGameResp.AV)
+ u.StartGame(startGameResp.AV, startGameResp.KbMouse)
// send back recording status
if conf.Recording.Enabled && rq.Record {
diff --git a/pkg/network/webrtc/webrtc.go b/pkg/network/webrtc/webrtc.go
index 92e5b0074..72699a175 100644
--- a/pkg/network/webrtc/webrtc.go
+++ b/pkg/network/webrtc/webrtc.go
@@ -17,6 +17,7 @@ type Peer struct {
log *logger.Logger
OnMessage func(data []byte)
OnKeyboard func(data []byte)
+ OnMouse func(data []byte)
a *webrtc.TrackLocalStaticSample
v *webrtc.TrackLocalStaticSample
@@ -109,6 +110,17 @@ func (p *Peer) NewCall(vCodec, aCodec string, onICECandidate func(ice any)) (sdp
})
p.log.Debug().Msg("Added [keyboard] chan")
+ mChan, err := p.addDataChannel("mouse")
+ if err != nil {
+ return "", err
+ }
+ mChan.OnMessage(func(m webrtc.DataChannelMessage) {
+ if p.OnMouse != nil {
+ p.OnMouse(m.Data)
+ }
+ })
+ p.log.Debug().Msg("Added [mouse] chan")
+
p.conn.OnICEConnectionStateChange(p.handleICEState(func() { p.log.Info().Msg("Connected") }))
// Stream provider supposes to send offer
offer, err := p.conn.CreateOffer(nil)
diff --git a/pkg/worker/caged/app/app.go b/pkg/worker/caged/app/app.go
index a6b042375..f341ea32e 100644
--- a/pkg/worker/caged/app/app.go
+++ b/pkg/worker/caged/app/app.go
@@ -15,6 +15,8 @@ type App interface {
SetDataCb(func([]byte))
InputGamepad(port int, data []byte)
InputKeyboard(port int, data []byte)
+ InputMouse(port int, data []byte)
+ KbMouseSupport() bool
}
type Audio struct {
diff --git a/pkg/worker/caged/libretro/caged.go b/pkg/worker/caged/libretro/caged.go
index e2c8532f5..347640b2e 100644
--- a/pkg/worker/caged/libretro/caged.go
+++ b/pkg/worker/caged/libretro/caged.go
@@ -88,6 +88,8 @@ func (c *Caged) ViewportSize() (int, int) { return c.base.ViewportSiz
func (c *Caged) Scale() float64 { return c.Emulator.Scale() }
func (c *Caged) InputGamepad(port int, data []byte) { c.base.Input(port, RetroPad, data) }
func (c *Caged) InputKeyboard(port int, data []byte) { c.base.Input(port, Keyboard, data) }
+func (c *Caged) InputMouse(port int, data []byte) { c.base.Input(port, Mouse, data) }
+func (c *Caged) KbMouseSupport() bool { return c.base.KbMouseSupport() }
func (c *Caged) Start() { go c.Emulator.Start() }
func (c *Caged) SetSaveOnClose(v bool) { c.base.SaveOnClose = v }
func (c *Caged) SetSessionId(name string) { c.base.SetSessionId(name) }
diff --git a/pkg/worker/caged/libretro/frontend.go b/pkg/worker/caged/libretro/frontend.go
index 6543ac429..3cd5425a1 100644
--- a/pkg/worker/caged/libretro/frontend.go
+++ b/pkg/worker/caged/libretro/frontend.go
@@ -73,6 +73,7 @@ type Device byte
const (
RetroPad = Device(nanoarch.RetroPad)
Keyboard = Device(nanoarch.Keyboard)
+ Mouse = Device(nanoarch.Mouse)
)
var (
@@ -151,6 +152,7 @@ func (f *Frontend) LoadCore(emu string) {
Options: conf.Options,
UsesLibCo: conf.UsesLibCo,
CoreAspectRatio: conf.CoreAspectRatio,
+ KbMouseSupport: conf.KbMouseSupport,
}
f.mu.Lock()
scale := 1.0
@@ -278,6 +280,7 @@ func (f *Frontend) FrameSize() (int, int) { return f.nano.BaseWidth(), f
func (f *Frontend) HasSave() bool { return os.Exists(f.HashPath()) }
func (f *Frontend) HashPath() string { return f.storage.GetSavePath() }
func (f *Frontend) IsPortrait() bool { return f.nano.IsPortrait() }
+func (f *Frontend) KbMouseSupport() bool { return f.nano.KbMouseSupport() }
func (f *Frontend) LoadGame(path string) error { return f.nano.LoadGame(path) }
func (f *Frontend) PixFormat() uint32 { return f.nano.Video.PixFmt.C }
func (f *Frontend) RestoreGameState() error { return f.Load() }
@@ -300,6 +303,8 @@ func (f *Frontend) Input(port int, d Device, data []byte) {
f.nano.InputRetropad(port, data)
case Keyboard:
f.nano.InputKeyboard(port, data)
+ case Mouse:
+ f.nano.InputMouse(port, data)
}
}
diff --git a/pkg/worker/caged/libretro/nanoarch/input.go b/pkg/worker/caged/libretro/nanoarch/input.go
index 00629b24a..f0b99dab5 100644
--- a/pkg/worker/caged/libretro/nanoarch/input.go
+++ b/pkg/worker/caged/libretro/nanoarch/input.go
@@ -14,14 +14,13 @@ const (
Pressed
)
-const RetroDeviceTypeShift = 8
-
// InputState stores full controller state.
// It consists of:
// - uint16 button values
// - int16 analog stick values
+type InputState [maxPort]RetroPadState
+
type (
- InputState [maxPort]RetroPadState
RetroPadState struct {
keys uint32
axes [dpadAxes]int32
@@ -31,13 +30,31 @@ type (
mod uint16
mu sync.Mutex
}
+ MouseState struct {
+ dx, dy atomic.Int32
+ buttons atomic.Int32
+ }
)
+type MouseBtnState int32
+
type Device byte
const (
RetroPad Device = iota
Keyboard
+ Mouse
+)
+
+const (
+ MouseMove = iota
+ MouseButton
+)
+
+const (
+ MouseLeft MouseBtnState = 1 << iota
+ MouseRight
+ MouseMiddle
)
const (
@@ -66,8 +83,6 @@ func (s *InputState) IsDpadTouched(port uint, axis uint) (shift C.int16_t) {
return C.int16_t(atomic.LoadInt32(&s[port].axes[axis]))
}
-func RetroDeviceSubclass(base, id int) int { return ((id + 1) << RetroDeviceTypeShift) | base }
-
func NewKeyboardState() KeyboardState {
return KeyboardState{
keys: make(map[uint]struct{}),
@@ -75,15 +90,33 @@ func NewKeyboardState() KeyboardState {
}
}
-func (ks *KeyboardState) Set(press bool, key uint, mod uint16) {
+// SetKey sets keyboard state.
+//
+// 0 1 2 3 4 5 6
+// [ KEY ] P MOD
+//
+// KEY contains Libretro code of the keyboard key (4 bytes).
+// P contains 0 or 1 if the key is pressed (1 byte).
+// MOD contains bitmask for Alt | Ctrl | Meta | Shift keys press state (2 bytes).
+//
+// Returns decoded state from the input bytes.
+func (ks *KeyboardState) SetKey(data []byte) (pressed bool, key uint, mod uint16) {
+ if len(data) != 7 {
+ return
+ }
+
+ pressed = data[4] == 1
+ key = uint(binary.BigEndian.Uint32(data))
+ mod = binary.BigEndian.Uint16(data[5:])
ks.mu.Lock()
- if press {
+ if pressed {
ks.keys[key] = struct{}{}
} else {
delete(ks.keys, key)
}
ks.mod = mod
ks.mu.Unlock()
+ return
}
func (ks *KeyboardState) Pressed(key uint) C.int16_t {
@@ -96,9 +129,32 @@ func (ks *KeyboardState) Pressed(key uint) C.int16_t {
return Released
}
-func decodeKeyboardState(data []byte) (bool, uint, uint16) {
- press := data[4] == 1
- key := uint(binary.LittleEndian.Uint32(data))
- mod := binary.LittleEndian.Uint16(data[5:])
- return press, key, mod
+// ShiftPos sets mouse relative position state.
+//
+// 0 1 2 3
+// [dx] [dy]
+//
+// dx and dy are relative mouse coordinates
+func (ms *MouseState) ShiftPos(data []byte) {
+ if len(data) != 4 {
+ return
+ }
+ dx := int16(data[0])<<8 + int16(data[1])
+ dy := int16(data[2])<<8 + int16(data[3])
+ ms.dx.Add(int32(dx))
+ ms.dy.Add(int32(dy))
+}
+
+func (ms *MouseState) PopX() C.int16_t { return C.int16_t(ms.dx.Swap(0)) }
+func (ms *MouseState) PopY() C.int16_t { return C.int16_t(ms.dy.Swap(0)) }
+
+// SetButtons sets the state MouseBtnState of mouse buttons.
+func (ms *MouseState) SetButtons(data byte) { ms.buttons.Store(int32(data)) }
+
+func (ms *MouseState) Buttons() (l, r, m bool) {
+ mbs := MouseBtnState(ms.buttons.Load())
+ l = mbs&MouseLeft != 0
+ r = mbs&MouseRight != 0
+ m = mbs&MouseMiddle != 0
+ return
}
diff --git a/pkg/worker/caged/libretro/nanoarch/input_test.go b/pkg/worker/caged/libretro/nanoarch/input_test.go
index d0c3a06ea..97015fd95 100644
--- a/pkg/worker/caged/libretro/nanoarch/input_test.go
+++ b/pkg/worker/caged/libretro/nanoarch/input_test.go
@@ -1,6 +1,7 @@
package nanoarch
import (
+ "encoding/binary"
"math/rand"
"sync"
"testing"
@@ -19,3 +20,54 @@ func TestConcurrentInput(t *testing.T) {
}
wg.Wait()
}
+
+func TestMousePos(t *testing.T) {
+ data := []byte{0, 0, 0, 0}
+
+ dx := 1111
+ dy := 2222
+
+ binary.BigEndian.PutUint16(data, uint16(dx))
+ binary.BigEndian.PutUint16(data[2:], uint16(dy))
+
+ ms := MouseState{}
+ ms.ShiftPos(data)
+
+ x := int(ms.PopX())
+ y := int(ms.PopY())
+
+ if x != dx || y != dy {
+ t.Errorf("invalid state, %v = %v, %v = %v", dx, x, dy, y)
+ }
+
+ if ms.dx.Load() != 0 || ms.dy.Load() != 0 {
+ t.Errorf("coordinates weren't cleared")
+ }
+}
+
+func TestMouseButtons(t *testing.T) {
+ tests := []struct {
+ name string
+ data byte
+ l bool
+ r bool
+ m bool
+ }{
+ {name: "l+r+m+", data: 1 + 2 + 4, l: true, r: true, m: true},
+ {name: "l-r-m-", data: 0},
+ {name: "l-r+m-", data: 2, r: true},
+ {name: "l+r-m+", data: 1 + 4, l: true, m: true},
+ }
+
+ ms := MouseState{}
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ ms.SetButtons(test.data)
+ l, r, m := ms.Buttons()
+ if l != test.l || r != test.r || m != test.m {
+ t.Errorf("wrong button state: %v -> %v, %v, %v", test.data, l, r, m)
+ }
+ })
+ }
+}
diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.go b/pkg/worker/caged/libretro/nanoarch/nanoarch.go
index c69644628..02edf7882 100644
--- a/pkg/worker/caged/libretro/nanoarch/nanoarch.go
+++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.go
@@ -402,7 +402,7 @@ func (n *Nanoarch) InputMouse(_ int, data []byte) {
case MouseMove:
n.mouse.ShiftPos(state)
case MouseButton:
- n.mouse.SetButtons(state)
+ n.mouse.SetButtons(state[0])
}
}
diff --git a/pkg/worker/coordinatorhandlers.go b/pkg/worker/coordinatorhandlers.go
index 3c5f41752..121f4a7a6 100644
--- a/pkg/worker/coordinatorhandlers.go
+++ b/pkg/worker/coordinatorhandlers.go
@@ -126,12 +126,14 @@ func (c *coordinator) HandleGameStart(rq api.StartGameRequest[com.Uid], w *Worke
}
}
- data, err := api.Wrap(api.Out{T: uint8(api.AppVideoChange), Payload: api.AppVideoInfo{
- W: m.VideoW,
- H: m.VideoH,
- A: app.AspectRatio(),
- S: int(app.Scale()),
- }})
+ data, err := api.Wrap(api.Out{
+ T: uint8(api.AppVideoChange),
+ Payload: api.AppVideoInfo{
+ W: m.VideoW,
+ H: m.VideoH,
+ A: app.AspectRatio(),
+ S: int(app.Scale()),
+ }})
if err != nil {
c.log.Error().Err(err).Msgf("wrap")
}
@@ -173,10 +175,15 @@ func (c *coordinator) HandleGameStart(rq api.StartGameRequest[com.Uid], w *Worke
s := room.WithWebRTC(user.Session)
s.OnMessage = func(data []byte) { r.App().InputGamepad(user.Index, data) }
s.OnKeyboard = func(data []byte) { r.App().InputKeyboard(user.Index, data) }
+ s.OnMouse = func(data []byte) { r.App().InputMouse(user.Index, data) }
c.RegisterRoom(r.Id())
- response := api.StartGameResponse{Room: api.Room{Rid: r.Id()}, Record: w.conf.Recording.Enabled}
+ response := api.StartGameResponse{
+ Room: api.Room{Rid: r.Id()},
+ Record: w.conf.Recording.Enabled,
+ KbMouse: r.App().KbMouseSupport(),
+ }
if r.App().AspectEnabled() {
ww, hh := r.App().ViewportSize()
response.AV = &api.AppVideoInfo{W: ww, H: hh, A: r.App().AspectRatio(), S: int(r.App().Scale())}
diff --git a/web/index.html b/web/index.html
index 854de29aa..3d2eceec7 100644
--- a/web/index.html
+++ b/web/index.html
@@ -107,24 +107,23 @@
-
-
-
+
-
+
+
diff --git a/web/js/api/api.js b/web/js/api/api.js
index 77d1de73e..a2cddf72f 100644
--- a/web/js/api/api.js
+++ b/web/js/api/api.js
@@ -80,36 +80,23 @@ const api = (() => {
})();
const mousePress = (() => {
- // 0 1 2 3
- // T L R M
- const buffer = new ArrayBuffer(4);
+ // 0 1
+ // T B
+ const buffer = new ArrayBuffer(2);
const dv = new DataView(buffer);
- // #define RETRO_DEVICE_ID_MOUSE_LEFT 2
- // #define RETRO_DEVICE_ID_MOUSE_RIGHT 3
- // #define RETRO_DEVICE_ID_MOUSE_WHEELUP 4
- // #define RETRO_DEVICE_ID_MOUSE_WHEELDOWN 5
- // #define RETRO_DEVICE_ID_MOUSE_MIDDLE 6
- // #define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP 7
- // #define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN 8
- // #define RETRO_DEVICE_ID_MOUSE_BUTTON_4 9
- // #define RETRO_DEVICE_ID_MOUSE_BUTTON_5 10
-
// 0: Main button pressed, usually the left button or the un-initialized state
// 1: Auxiliary button pressed, usually the wheel button or the middle button (if present)
// 2: Secondary button pressed, usually the right button
// 3: Fourth button, typically the Browser Back button
// 4: Fifth button, typically the Browser Forward button
- const b2r = [1, 3, 2, 9, 10] // browser mouse button to retro button
+ const b2r = [1, 4, 2, 0, 0] // browser mouse button to retro button
+ // assumed that only one button pressed / released
- /* @param buttons contains pressed state of mouse buttons according to
- * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
- */
return (button = 0, pressed = false) => {
- dv.setUint32(0, 0);
dv.setUint8(0, mouse.BUTTONS);
- dv.setUint8(b2r[button], +pressed);
+ dv.setUint8(1, pressed ? b2r[button] : 0);
webrtc.mouse(buffer);
}
})();
diff --git a/web/js/controller.js b/web/js/controller.js
index 2d90cceb6..ebf8c89a9 100644
--- a/web/js/controller.js
+++ b/web/js/controller.js
@@ -161,9 +161,8 @@
event.pub(WEBRTC_ICE_CANDIDATE_RECEIVED, {candidate: payload});
break;
case api.endpoint.GAME_START:
- if (payload.av) {
- event.pub(APP_VIDEO_CHANGED, payload.av)
- }
+ payload.av && event.pub(APP_VIDEO_CHANGED, payload.av)
+ payload.kb_mouse && event.pub(KB_MOUSE_FLAG);
event.pub(GAME_ROOM_AVAILABLE, {roomId: payload.roomId});
break;
case api.endpoint.GAME_SAVE:
@@ -212,25 +211,6 @@
state.keyPress(data.key, data.code);
};
- // Feature detection.
- const supportsKeyboardLock =
- ('keyboard' in navigator) && ('lock' in navigator.keyboard);
-
- if (supportsKeyboardLock) {
- document.addEventListener('fullscreenchange', async () => {
- if (document.fullscreenElement) {
- await navigator.keyboard.lock();
- keyboard.toggle(false);
- return console.log('Keyboard locked.');
- }
- navigator.keyboard.unlock();
- keyboard.toggle(true);
- console.log('Keyboard unlocked.');
- });
- } else {
- console.warn('Browser doesn\'t support keyboard lock!');
- }
-
// pre-state key release handler
const onKeyRelease = data => {
const button = keyButtons[data.key];
@@ -380,31 +360,13 @@
..._default,
name: 'game',
axisChanged: (id, value) => input.setAxisChanged(id, value),
- keyboardInput: (down, e) => {
- const buffer = new ArrayBuffer(7);
- const dv = new DataView(buffer);
-
- // 0 1 2 3 4 5 6
- dv.setUint8(4, down ? 1 : 0)
- const k = libretro.map('', e.code);
- dv.setUint32(0, k, true)
-
- // meta keys
- let mod = 0;
-
- e.altKey && (mod |= libretro.mod.ALT)
- e.ctrlKey && (mod |= libretro.mod.CTRL)
- e.metaKey && (mod |= libretro.mod.META)
- e.shiftKey && (mod |= libretro.mod.SHIFT)
-
- dv.setUint16(5, mod, true)
-
- webrtc.keyboard(buffer);
- },
- keyPress: (key, code) => {
+ keyboardInput: (pressed, e) => api.game.input.keyboard.press(pressed, e),
+ mouseMove: (e) => api.game.input.mouse.move(e.dx, e.dy),
+ mousePress: (e) => api.game.input.mouse.press(e.b, e.p),
+ keyPress: (key) => {
input.setKeyState(key, true);
},
- keyRelease: function (key, code) {
+ keyRelease: function (key) {
input.setKeyState(key, false);
switch (key) {
@@ -453,6 +415,15 @@
}
};
+ // Browser lock API
+ document.onpointerlockchange = () => {
+ event.pub(POINTER_LOCK_CHANGE, document.pointerLockElement);
+ }
+
+ document.onfullscreenchange = async () => {
+ event.pub(FULLSCREEN_CHANGE, document.fullscreenElement);
+ }
+
// subscriptions
event.sub(MESSAGE, onMessage);
@@ -490,11 +461,19 @@
event.sub(MENU_HANDLER_ATTACHED, (data) => {
menuScreen.addEventListener(data.event, data.handler, {passive: true});
});
- // separate keyboard handler
+
+ // keyboard handler in the Screen Lock mode
event.sub(KEYBOARD_KEY_DOWN, (v) => state.keyboardInput?.(true, v));
event.sub(KEYBOARD_KEY_UP, (v) => state.keyboardInput?.(false, v));
+
+ // mouse handler in the Screen Lock mode
+ event.sub(MOUSE_MOVED, (e) => state.mouseMove?.(e))
+ event.sub(MOUSE_PRESSED, (e) => state.mousePress?.(e))
+
+ // general keyboard handler
event.sub(KEY_PRESSED, onKeyPress);
event.sub(KEY_RELEASED, onKeyRelease);
+
event.sub(SETTINGS_CHANGED, () => message.show('Settings have been updated'));
event.sub(AXIS_CHANGED, onAxisChanged);
event.sub(CONTROLLER_UPDATED, data => webrtc.input(data));
diff --git a/web/js/event/event.js b/web/js/event/event.js
index be9fa5b7a..7894c378d 100644
--- a/web/js/event/event.js
+++ b/web/js/event/event.js
@@ -93,6 +93,11 @@ const KEYBOARD_KEY_DOWN = 'keyboardKeyDown';
const KEYBOARD_KEY_UP = 'keyboardKeyUp';
const AXIS_CHANGED = 'axisChanged';
const CONTROLLER_UPDATED = 'controllerUpdated';
+const MOUSE_MOVED = 'mouseMoved';
+const MOUSE_PRESSED = 'mousePressed';
+
+const FULLSCREEN_CHANGE = 'fsc';
+const POINTER_LOCK_CHANGE = 'plc';
const DPAD_TOGGLE = 'dpadToggle';
const STATS_TOGGLE = 'statsToggle';
@@ -104,3 +109,4 @@ const RECORDING_TOGGLED = 'recordingToggle'
const RECORDING_STATUS_CHANGED = 'recordingStatusChanged'
const APP_VIDEO_CHANGED = 'appVideoChanged'
+const KB_MOUSE_FLAG = 'kbMouseFlag'
diff --git a/web/js/input/keyboard.js b/web/js/input/keyboard.js
index d9ed2ca63..3659da46e 100644
--- a/web/js/input/keyboard.js
+++ b/web/js/input/keyboard.js
@@ -100,6 +100,22 @@ const keyboard = (() => {
event.sub(DPAD_TOGGLE, (data) => onDpadToggle(data.checked));
+ event.sub(KB_MOUSE_FLAG, () => {
+ const supportsKeyboardLock =
+ ('keyboard' in navigator) && ('lock' in navigator.keyboard);
+
+ if (supportsKeyboardLock) {
+ event.sub(FULLSCREEN_CHANGE, async (fullscreenEl) => {
+ enabled = !fullscreenEl;
+ enabled ? navigator.keyboard.unlock() : await navigator.keyboard.lock();
+ log.debug(`Keyboard lock: ${!enabled}`);
+ })
+ } else {
+ log.warn('Browser doesn\'t support keyboard lock!');
+ }
+ })
+
+
return {
init: () => {
keyMap = settings.loadOr(opts.INPUT_KEYBOARD_MAP, defaultMap);
@@ -135,8 +151,5 @@ const keyboard = (() => {
}, settings: {
remap
},
- toggle: (v) => {
- enabled = v !== undefined ? v : !enabled;
- }
}
})(event, document, KEY, log, opts, settings);
diff --git a/web/js/input/libretro.js b/web/js/input/libretro.js
deleted file mode 100644
index fd34ae16c..000000000
--- a/web/js/input/libretro.js
+++ /dev/null
@@ -1,182 +0,0 @@
-// RETRO_KEYBOARD
-const retro = {
- '': 0,
- 'Unknown': 0, // ???
- 'First': 0, // ???
- 'Backspace': 8,
- 'Tab': 9,
- 'Clear': 12,
- 'Enter': 13, 'Return': 13,
- 'Pause': 19,
- 'Escape': 27,
- 'Space': 32,
- 'Exclaim': 33,
- 'Quotedbl': 34,
- 'Hash': 35,
- 'Dollar': 36,
- 'Ampersand': 38,
- 'Quote': 39,
- 'Leftparen': 40, '(': 40,
- 'Rightparen': 41, ')': 41,
- 'Asterisk': 42,
- 'Plus': 43,
- 'Comma': 44,
- 'Minus': 45,
- 'Period': 46,
- 'Slash': 47,
- 'Digit0': 48,
- 'Digit1': 49,
- 'Digit2': 50,
- 'Digit3': 51,
- 'Digit4': 52,
- 'Digit5': 53,
- 'Digit6': 54,
- 'Digit7': 55,
- 'Digit8': 56,
- 'Digit9': 57,
- 'Colon': 58, ':': 58,
- 'Semicolon': 59, ';': 59,
- 'Less': 60, '<': 60,
- 'Equal': 61, '=': 61,
- 'Greater': 62, '>': 62,
- 'Question': 63, '?': 63,
- // RETROK_AT = 64,
- 'BracketLeft': 91, '[': 91,
- 'Backslash': 92, '\\': 92,
- 'BracketRight': 93, ']': 93,
- // RETROK_CARET = 94,
- // RETROK_UNDERSCORE = 95,
- 'Backquote': 96, '`': 96,
- 'KeyA': 97,
- 'KeyB': 98,
- 'KeyC': 99,
- 'KeyD': 100,
- 'KeyE': 101,
- 'KeyF': 102,
- 'KeyG': 103,
- 'KeyH': 104,
- 'KeyI': 105,
- 'KeyJ': 106,
- 'KeyK': 107,
- 'KeyL': 108,
- 'KeyM': 109,
- 'KeyN': 110,
- 'KeyO': 111,
- 'KeyP': 112,
- 'KeyQ': 113,
- 'KeyR': 114,
- 'KeyS': 115,
- 'KeyT': 116,
- 'KeyU': 117,
- 'KeyV': 118,
- 'KeyW': 119,
- 'KeyX': 120,
- 'KeyY': 121,
- 'KeyZ': 122,
- // RETROK_LEFTBRACE = 123,
- // RETROK_BAR = 124,
- // RETROK_RIGHTBRACE = 125,
- 'Tilde': 126, '~': 126,
- 'Delete': 127,
-
- // RETROK_KP0 = 256,
- // RETROK_KP1 = 257,
- // RETROK_KP2 = 258,
- // RETROK_KP3 = 259,
- // RETROK_KP4 = 260,
- // RETROK_KP5 = 261,
- // RETROK_KP6 = 262,
- // RETROK_KP7 = 263,
- // RETROK_KP8 = 264,
- // RETROK_KP9 = 265,
- // RETROK_KP_PERIOD = 266,
- // RETROK_KP_DIVIDE = 267,
- // RETROK_KP_MULTIPLY = 268,
- // RETROK_KP_MINUS = 269,
- // RETROK_KP_PLUS = 270,
- // RETROK_KP_ENTER = 271,
- // RETROK_KP_EQUALS = 272,
-
- 'ArrowUp': 273,
- 'ArrowDown': 274,
- 'ArrowRight': 275,
- 'ArrowLeft': 276,
- 'Insert': 277,
- 'Home': 278,
- 'End': 279,
- 'PageUp': 280,
- 'PageDown': 281,
-
- 'F1': 282,
- 'F2': 283,
- 'F3': 284,
- 'F4': 285,
- 'F5': 286,
- 'F6': 287,
- 'F7': 288,
- 'F8': 289,
- 'F9': 290,
- 'F10': 291,
- 'F11': 292,
- 'F12': 293,
- 'F13': 294,
- 'F14': 295,
- 'F15': 296,
-
- 'NumLock': 300,
- 'CapsLock': 301,
- 'ScrollLock': 302,
- 'ShiftRight': 303,
- 'ShiftLeft': 304,
- 'ControlRight': 305,
- 'ControlLeft': 306,
- 'AltRight': 307,
- 'AltLeft': 308,
- 'MetaRight': 309,
- 'MetaLeft': 310,
- // RETROK_LSUPER = 311,
- // RETROK_RSUPER = 312,
- // RETROK_MODE = 313,
- // RETROK_COMPOSE = 314,
-
- // RETROK_HELP = 315,
- // RETROK_PRINT = 316,
- // RETROK_SYSREQ = 317,
- // RETROK_BREAK = 318,
- // RETROK_MENU = 319,
- // RETROK_POWER = 320,
- // RETROK_EURO = 321,
- // RETROK_UNDO = 322,
- // RETROK_OEM_102 = 323,
-
- // RETROK_LAST,
-
- // RETROK_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
-};
-
-const retroMod = {
- NONE: 0x0000,
-
- SHIFT: 0x01,
- CTRL: 0x02,
- ALT: 0x04,
- META: 0x08,
-
- // RETROKMOD_NUMLOCK = 0x10,
- // RETROKMOD_CAPSLOCK = 0x20,
- // RETROKMOD_SCROLLOCK = 0x40,
-
- // RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
-};
-
-
-const libretro = (() => {
- const _map = (key = '', code = '') => {
- return retro[code] || retro[key] || 0
- }
-
- return {
- map: _map,
- mod: retroMod,
- }
-})()
diff --git a/web/js/network/webrtc.js b/web/js/network/webrtc.js
index 148fb2373..2129b20f8 100644
--- a/web/js/network/webrtc.js
+++ b/web/js/network/webrtc.js
@@ -14,6 +14,7 @@ const webrtc = (() => {
let connection;
let dataChannel;
let keyboardChannel;
+ let mouseChannel;
let mediaStream;
let candidates = [];
let isAnswered = false;
@@ -36,18 +37,24 @@ const webrtc = (() => {
if (e.channel.label === 'keyboard') {
keyboardChannel = e.channel;
- } else {
- dataChannel = e.channel;
- dataChannel.onopen = () => {
- log.info('[rtc] the input channel has been opened');
- inputReady = true;
- event.pub(WEBRTC_CONNECTION_READY)
- };
- if (onData) {
- dataChannel.onmessage = onData;
- }
- dataChannel.onclose = () => log.info('[rtc] the input channel has been closed');
+ return;
+ }
+
+ if (e.channel.label === 'mouse') {
+ mouseChannel = e.channel
+ return;
}
+
+ dataChannel = e.channel;
+ dataChannel.onopen = () => {
+ log.info('[rtc] the input channel has been opened');
+ inputReady = true;
+ event.pub(WEBRTC_CONNECTION_READY)
+ };
+ if (onData) {
+ dataChannel.onmessage = onData;
+ }
+ dataChannel.onclose = () => log.info('[rtc] the input channel has been closed');
}
connection.oniceconnectionstatechange = ice.onIceConnectionStateChange;
connection.onicegatheringstatechange = ice.onIceStateChange;
@@ -77,6 +84,10 @@ const webrtc = (() => {
keyboardChannel.close();
keyboardChannel = null;
}
+ if (mouseChannel) {
+ mouseChannel.close();
+ mouseChannel = null;
+ }
candidates = Array();
log.info('[rtc] WebRTC has been closed');
}
@@ -174,6 +185,8 @@ const webrtc = (() => {
});
isFlushing = false;
},
+ keyboard: (data) => keyboardChannel.send(data),
+ mouse: (data) => mouseChannel.send(data),
input: (data) => dataChannel.send(data),
isConnected: () => connected,
isInputReady: () => inputReady,
diff --git a/web/js/stream/stream.js b/web/js/stream/stream.js
index 0d60dddd0..673709b0b 100644
--- a/web/js/stream/stream.js
+++ b/web/js/stream/stream.js
@@ -17,6 +17,7 @@ const stream = (() => {
state = {
screen: screen,
fullscreen: false,
+ kbmLock: false,
timerId: null,
w: 0,
h: 0,
@@ -47,6 +48,23 @@ const stream = (() => {
const getVideoEl = () => screen
+ const getActualVideoSize = () => {
+ if (state.fullscreen) {
+ // we can't get real