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