From 0704a7672ae8b48fbf41e3e436db259c22b1b575 Mon Sep 17 00:00:00 2001 From: Sergey Stepanov Date: Wed, 28 Feb 2024 12:27:59 +0300 Subject: [PATCH] Add the initial mouse support --- pkg/network/webrtc/webrtc.go | 12 + pkg/worker/caged/app/app.go | 1 + pkg/worker/caged/libretro/caged.go | 1 + pkg/worker/caged/libretro/frontend.go | 3 + pkg/worker/caged/libretro/nanoarch/input.go | 112 +++++- .../caged/libretro/nanoarch/nanoarch.go | 61 +++- pkg/worker/coordinatorhandlers.go | 1 + web/index.html | 13 +- web/js/api/api.js | 320 ++++++++++++++++-- web/js/controller.js | 76 ++--- web/js/event/event.js | 5 + web/js/input/keyboard.js | 16 +- web/js/input/libretro.js | 182 ---------- web/js/network/webrtc.js | 44 +-- web/js/stream/stream.js | 72 ++-- 15 files changed, 581 insertions(+), 338 deletions(-) delete mode 100644 web/js/input/libretro.js 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 8bf008eca..fc37827eb 100644 --- a/pkg/worker/caged/app/app.go +++ b/pkg/worker/caged/app/app.go @@ -14,6 +14,7 @@ type App interface { SetDataCb(func([]byte)) InputGamepad(port int, data []byte) InputKeyboard(port int, data []byte) + InputMouse(port int, data []byte) } type Audio struct { diff --git a/pkg/worker/caged/libretro/caged.go b/pkg/worker/caged/libretro/caged.go index e2c8532f5..979868e12 100644 --- a/pkg/worker/caged/libretro/caged.go +++ b/pkg/worker/caged/libretro/caged.go @@ -88,6 +88,7 @@ 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) 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 ce527779f..d46316800 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 ( @@ -300,6 +301,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 f13591fb9..5bd033923 100644 --- a/pkg/worker/caged/libretro/nanoarch/input.go +++ b/pkg/worker/caged/libretro/nanoarch/input.go @@ -12,8 +12,6 @@ import "C" const KeyPressed = C.int16_t(1) const KeyReleased = C.int16_t(0) -const RetroDeviceTypeShift = 8 - // InputState stores full controller state. // It consists of: // - uint16 button values @@ -29,6 +27,14 @@ type ( mod uint16 mu sync.Mutex } + MouseState struct { + x, y int16 + mp sync.Mutex + l bool + r bool + m bool + mb sync.Mutex + } ) type Device byte @@ -36,6 +42,12 @@ type Device byte const ( RetroPad Device = iota Keyboard + Mouse +) + +const ( + MouseMove = iota + MouseButton ) const ( @@ -64,8 +76,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{}), @@ -73,15 +83,35 @@ func NewKeyboardState() KeyboardState { } } -func (ks *KeyboardState) Set(press bool, key uint, mod uint16) { +// SetKey sets keyboard state. +// +// data format +// +// 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 { @@ -94,9 +124,69 @@ func (ks *KeyboardState) Pressed(key uint) C.int16_t { return KeyReleased } -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. +// +// data format +// +// 0 1 2 3 +// [x] [y] +// +// x and y contain relative (to the previous values) +// X, Y positions as signed int. +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.mp.Lock() + ms.x += dx + ms.y += dy + ms.mp.Unlock() +} + +func (ms *MouseState) PopX() C.int16_t { + var x int16 + ms.mp.Lock() + x = ms.x + ms.x = 0 + ms.mp.Unlock() + return C.int16_t(x) +} + +func (ms *MouseState) PopY() C.int16_t { + var y int16 + ms.mp.Lock() + y = ms.y + ms.y = 0 + ms.mp.Unlock() + return C.int16_t(y) +} + +// SetButtons sets the state of mouse buttons. +// +// data format +// +// 0 1 2 +// L R M +// +// L is the left button, R is right, and M is the middle button. +func (ms *MouseState) SetButtons(data []byte) { + if len(data) != 3 { + return + } + ms.mb.Lock() + ms.l = data[0] == 1 + ms.r = data[1] == 1 + ms.m = data[2] == 1 + ms.mb.Unlock() +} + +func (ms *MouseState) Buttons() (l, r, m bool) { + ms.mb.Lock() + l = ms.l + r = ms.r + m = ms.m + ms.mb.Unlock() + return } diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.go b/pkg/worker/caged/libretro/nanoarch/nanoarch.go index 81af66c50..a9f35db5b 100644 --- a/pkg/worker/caged/libretro/nanoarch/nanoarch.go +++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.go @@ -37,10 +37,10 @@ var ( type Nanoarch struct { Handlers - // keyboard keeps pressed buttons for input poll keyboard KeyboardState + mouse MouseState + retropad InputState - retropad InputState keyboardCb *C.struct_retro_keyboard_callback LastFrameTime int64 LibCo bool @@ -169,6 +169,8 @@ func (n *Nanoarch) CoreLoad(meta Metadata) { n.keyboardCb = nil n.keyboard = NewKeyboardState() + n.mouse = MouseState{} + n.options = &meta.Options filePath := meta.LibPath @@ -381,12 +383,26 @@ func (n *Nanoarch) InputKeyboard(_ int, data []byte) { return } - press, key, mod := decodeKeyboardState(data) - - n.keyboard.Set(press, key, mod) - C.bridge_retro_keyboard_callback(unsafe.Pointer(n.keyboardCb), C.bool(press), + // we should preserve the state of pressed buttons for the input poll function (each retro_run) + // and explicitly call the retro_keyboard_callback function when a keyboard event happens + pressed, key, mod := n.keyboard.SetKey(data) + C.bridge_retro_keyboard_callback(unsafe.Pointer(n.keyboardCb), C.bool(pressed), C.unsigned(key), C.uint32_t(0), C.uint16_t(mod)) } +func (n *Nanoarch) InputMouse(_ int, data []byte) { + if len(data) == 0 { + return + } + + t := data[0] + state := data[1:] + switch t { + case MouseMove: + n.mouse.ShiftPos(state) + case MouseButton: + n.mouse.SetButtons(state) + } +} func videoSetPixelFormat(format uint32) (C.bool, error) { switch format { @@ -615,10 +631,6 @@ func coreInputPoll() {} func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.unsigned) C.int16_t { //Nan0.log.Debug().Msgf("%v %v %v %v", port, device, index, id) - //if uint(port) >= uint(MaxPort) { - // return KeyReleased - //} - switch device { case C.RETRO_DEVICE_JOYPAD: return Nan0.retropad.IsKeyPressed(uint(port), int(id)) @@ -631,6 +643,33 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u } case C.RETRO_DEVICE_KEYBOARD: return Nan0.keyboard.Pressed(uint(id)) + case C.RETRO_DEVICE_MOUSE: + switch id { + case C.RETRO_DEVICE_ID_MOUSE_X: + x := Nan0.mouse.PopX() + return x + case C.RETRO_DEVICE_ID_MOUSE_Y: + y := Nan0.mouse.PopY() + return y + case C.RETRO_DEVICE_ID_MOUSE_LEFT: + if l, _, _ := Nan0.mouse.Buttons(); l { + return KeyPressed + } + case C.RETRO_DEVICE_ID_MOUSE_RIGHT: + if _, r, _ := Nan0.mouse.Buttons(); r { + return KeyPressed + } + case C.RETRO_DEVICE_ID_MOUSE_WHEELUP: + case C.RETRO_DEVICE_ID_MOUSE_WHEELDOWN: + case C.RETRO_DEVICE_ID_MOUSE_MIDDLE: + if _, _, m := Nan0.mouse.Buttons(); m { + return KeyPressed + } + case C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP: + case C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN: + case C.RETRO_DEVICE_ID_MOUSE_BUTTON_4: + case C.RETRO_DEVICE_ID_MOUSE_BUTTON_5: + } } return KeyReleased @@ -791,7 +830,7 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool { } cInfo.WriteString(fmt.Sprintf("%v: %v%s", cd[i].id, C.GoString(cd[i].desc), delim)) } - Nan0.log.Debug().Msgf("%v", cInfo.String()) + //Nan0.log.Debug().Msgf("%v", cInfo.String()) } return true case C.RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS: diff --git a/pkg/worker/coordinatorhandlers.go b/pkg/worker/coordinatorhandlers.go index ca170fecb..c37e2377b 100644 --- a/pkg/worker/coordinatorhandlers.go +++ b/pkg/worker/coordinatorhandlers.go @@ -172,6 +172,7 @@ 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()) diff --git a/web/index.html b/web/index.html index 952614920..c6a6666ee 100644 --- a/web/index.html +++ b/web/index.html @@ -120,25 +120,24 @@

Options

- - + - - + - + - - + + + diff --git a/web/js/api/api.js b/web/js/api/api.js index 20de8312b..93d219b59 100644 --- a/web/js/api/api.js +++ b/web/js/api/api.js @@ -1,3 +1,5 @@ +let libretro = {}; + /** * Server API. * @@ -25,6 +27,11 @@ const api = (() => { APP_VIDEO_CHANGE: 150, }); + const mouse = { + MOVEMENT: 0, + BUTTONS: 1 + } + const packet = (type, payload, id) => { const packet = {t: type}; if (id !== undefined) packet.id = id; @@ -35,36 +42,295 @@ const api = (() => { const decodeBytes = (b) => String.fromCharCode.apply(null, new Uint8Array(b)) + const keyboardPress = (pressed = false, e) => { + const buffer = new ArrayBuffer(7); + const dv = new DataView(buffer); + + // 0 1 2 3 4 5 6 + dv.setUint8(4, pressed ? 1 : 0) + const k = libretro.map('', e.code); + dv.setUint32(0, k) + + // 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) + + webrtc.keyboard(buffer); + } + + const mouseMove = (() => { + // 0 1 2 3 4 + // T DX DY + const buffer = new ArrayBuffer(5); + const dv = new DataView(buffer); + + return (dx = 0, dy = 0) => { + dv.setUint8(0, mouse.MOVEMENT); + dv.setInt16(1, dx); + dv.setInt16(3, dy); + webrtc.mouse(buffer); + } + })(); + + const mousePress = (() => { + // 0 1 2 3 + // T L R M + const buffer = new ArrayBuffer(4); + 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 + + /* @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); + webrtc.mouse(buffer); + } + })(); + return Object.freeze({ endpoint: endpoints, decode: (b) => JSON.parse(decodeBytes(b)), - server: - { - initWebrtc: () => packet(endpoints.INIT_WEBRTC), - sendIceCandidate: (candidate) => packet(endpoints.ICE_CANDIDATE, btoa(JSON.stringify(candidate))), - sendSdp: (sdp) => packet(endpoints.ANSWER, btoa(JSON.stringify(sdp))), - latencyCheck: (id, list) => packet(endpoints.LATENCY_CHECK, list, id), - getWorkerList: () => packet(endpoints.GET_WORKER_LIST), + server: { + initWebrtc: () => packet(endpoints.INIT_WEBRTC), + sendIceCandidate: (candidate) => packet(endpoints.ICE_CANDIDATE, btoa(JSON.stringify(candidate))), + sendSdp: (sdp) => packet(endpoints.ANSWER, btoa(JSON.stringify(sdp))), + latencyCheck: (id, list) => packet(endpoints.LATENCY_CHECK, list, id), + getWorkerList: () => packet(endpoints.GET_WORKER_LIST), + }, + game: { + input: { + keyboard: { + press: keyboardPress, + }, + mouse: { + move: mouseMove, + press: mousePress, + } }, - game: - { - load: () => packet(endpoints.GAME_LOAD), - save: () => packet(endpoints.GAME_SAVE), - setPlayerIndex: (i) => packet(endpoints.GAME_SET_PLAYER_INDEX, i), - start: (game, roomId, record, recordUser, player) => packet(endpoints.GAME_START, { - game_name: game, - room_id: roomId, - player_index: player, - record: record, - record_user: recordUser, + load: () => packet(endpoints.GAME_LOAD), + save: () => packet(endpoints.GAME_SAVE), + setPlayerIndex: (i) => packet(endpoints.GAME_SET_PLAYER_INDEX, i), + start: (game, roomId, record, recordUser, player) => packet(endpoints.GAME_START, { + game_name: game, + room_id: roomId, + player_index: player, + record: record, + record_user: recordUser, + }), + toggleMultitap: () => packet(endpoints.GAME_TOGGLE_MULTITAP), + toggleRecording: (active = false, userName = '') => + packet(endpoints.GAME_RECORDING, { + active: active, + user: userName, }), - toggleMultitap: () => packet(endpoints.GAME_TOGGLE_MULTITAP), - toggleRecording: (active = false, userName = '') => - packet(endpoints.GAME_RECORDING, { - active: active, - user: userName, - }), - quit: (roomId) => packet(endpoints.GAME_QUIT, {room_id: roomId}), - } + quit: (roomId) => packet(endpoints.GAME_QUIT, {room_id: roomId}), + } }) -})(socket); +})(socket, webrtc, libretro); + +libretro = function () {// 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 _map = (key = '', code = '') => { + return retro[code] || retro[key] || 0 + } + + return { + map: _map, + mod: retroMod, + } +}() diff --git a/web/js/controller.js b/web/js/controller.js index 7e294e941..42bb72f85 100644 --- a/web/js/controller.js +++ b/web/js/controller.js @@ -126,12 +126,6 @@ stream.play() - // TODO get current game from the URL and not from the list? - // if we are opening a share link it will send the default game name to the server - // currently it's a game with the index 1 - // on the server this game is ignored and the actual game will be extracted from the share link - // so there's no point in doing this and this' really confusing - api.game.start( gameList.selected, room.getId(), @@ -217,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]; @@ -295,7 +270,7 @@ const handleRecordingStatus = (data) => { if (data === 'ok') { - message.show(`Recording ${recording.isActive() ? 'on' : 'off'}`, true) + message.show(`Recording ${recording.isActive() ? 'on' : 'off'}`) if (recording.isActive()) { recording.setIndicator(true) } @@ -303,7 +278,7 @@ message.show(`Recording failed ):`) recording.setIndicator(false) } - console.log("recording is ", recording.isActive()) + log.debug("recording is ", recording.isActive()) } const _default = { @@ -386,31 +361,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) { @@ -462,6 +419,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); @@ -499,11 +465,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(SETTINGS_CLOSED, () => { state.keyRelease(KEY.SETTINGS); diff --git a/web/js/event/event.js b/web/js/event/event.js index 15c530fbf..9bc57ea82 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'; diff --git a/web/js/input/keyboard.js b/web/js/input/keyboard.js index 8f19f2596..9c798d212 100644 --- a/web/js/input/keyboard.js +++ b/web/js/input/keyboard.js @@ -101,6 +101,19 @@ const keyboard = (() => { event.sub(DPAD_TOGGLE, (data) => onDpadToggle(data.checked)); + 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); @@ -136,8 +149,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 8757bad4c..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'); } @@ -155,7 +166,6 @@ const webrtc = (() => { event.pub(WEBRTC_SDP_ANSWER, {sdp: answer}); media.srcObject = mediaStream; }, - // setMessageHandler: (handler) => onMessage = handler, addCandidate: (data) => { if (data === '') { event.pub(WEBRTC_ICE_CANDIDATES_FLUSH); @@ -175,16 +185,8 @@ const webrtc = (() => { }); isFlushing = false; }, - // message: (mess = '') => { - // try { - // inputChannel.send(mess) - // return true - // } catch (error) { - // log.error('[rtc] input channel broken ' + error) - // return 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 b15e36e2b..5659663fd 100644 --- a/web/js/stream/stream.js +++ b/web/js/stream/stream.js @@ -89,31 +89,8 @@ const stream = (() => { screen.blur(); }) - screen.addEventListener('fullscreenchange', () => { - const fullscreen = document.fullscreenElement - - const w = window.screen.width ?? window.innerWidth; - const h = window.screen.height ?? window.innerHeight; - - const ww = document.documentElement.innerWidth; - const hh = document.documentElement.innerHeight; - - screen.style.padding = '0' - if (fullscreen) { - const dw = (w - ww * state.aspect) / 2 - screen.style.padding = `0 ${dw}px` - // chrome bug - setTimeout(() => { - const dw = (h - hh * state.aspect) / 2 - screen.style.padding = `0 ${dw}px` - }, 1) - } - makeFullscreen(!!fullscreen); - - screen.blur(); - - // !to flipped - }) + const handlePointerDown = (e) => event.pub(MOUSE_PRESSED, {b: e.button, p: true}); + const handlePointerUp = (e) => event.pub(MOUSE_PRESSED, {b: e.button, p: false}); const makeFullscreen = (make = false) => { screen.classList.toggle('no-media-controls', make) @@ -175,6 +152,51 @@ const stream = (() => { } }); + let pointerLocked = false; + + event.sub(FULLSCREEN_CHANGE, async (fullscreenEl) => { + const w = window.screen.width ?? window.innerWidth; + const h = window.screen.height ?? window.innerHeight; + + const ww = document.documentElement.innerWidth; + const hh = document.documentElement.innerHeight; + + const fullscreen = !!fullscreenEl; + + screen.style.padding = '0' + if (fullscreen) { + const dw = (w - ww * state.aspect) / 2 + screen.style.padding = `0 ${dw}px` + // chrome bug + setTimeout(() => { + const dw = (h - hh * state.aspect) / 2 + screen.style.padding = `0 ${dw}px` + }, 1) + } + makeFullscreen(fullscreen); + + screen.blur(); + + if (fullscreen && !pointerLocked) { + // event.pub(POINTER_LOCK_CHANGE, screen); + await screen.requestPointerLock(); + } + + screen.onpointerdown = fullscreen ? handlePointerDown : null; + screen.onpointerup = fullscreen ? handlePointerUp : null; + + // !to flipped + }) + + const handlePointerMove = (e) => { + event.pub(MOUSE_MOVED, {dx: e.movementX, dy: e.movementY}); + } + + event.sub(POINTER_LOCK_CHANGE, (lockedEl) => { + pointerLocked = lockedEl === screen; + screen.onpointermove = pointerLocked ? handlePointerMove : null; + log.debug(`Pointer lock: ${pointerLocked}`); + }); const fit = 'contain'