初始化
This commit is contained in:
109
client/src/charnumber.js
Normal file
109
client/src/charnumber.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// KeyboardEvent.charcode is already deprecated.
|
||||
//
|
||||
export const CharNumber = {
|
||||
"Backspace": 8,
|
||||
"Tab": 9,
|
||||
"Enter": 13,
|
||||
"Shift": 16,
|
||||
"Control": 17,
|
||||
"Alt": 18,
|
||||
"Pause": 19,
|
||||
"CapsLock": 20,
|
||||
"Escape": 27,
|
||||
" ": 32,
|
||||
"!": 33,
|
||||
"\"": 34,
|
||||
"#": 35,
|
||||
"$": 36,
|
||||
"%": 37,
|
||||
"&": 38,
|
||||
"'": 39,
|
||||
"(": 40,
|
||||
")": 41,
|
||||
"*": 42,
|
||||
"+": 43,
|
||||
",": 44,
|
||||
"-": 45,
|
||||
".": 46,
|
||||
"/": 47,
|
||||
"0": 48,
|
||||
"1": 49,
|
||||
"2": 50,
|
||||
"3": 51,
|
||||
"4": 52,
|
||||
"5": 53,
|
||||
"6": 54,
|
||||
"7": 55,
|
||||
"8": 56,
|
||||
"9": 57,
|
||||
":": 58,
|
||||
";": 59,
|
||||
"<": 60,
|
||||
"=": 61,
|
||||
">": 62,
|
||||
"?": 63,
|
||||
"@": 64,
|
||||
"A": 65,
|
||||
"B": 66,
|
||||
"C": 67,
|
||||
"D": 68,
|
||||
"E": 69,
|
||||
"F": 70,
|
||||
"G": 71,
|
||||
"H": 72,
|
||||
"I": 73,
|
||||
"J": 74,
|
||||
"K": 75,
|
||||
"L": 76,
|
||||
"M": 77,
|
||||
"N": 78,
|
||||
"O": 79,
|
||||
"P": 80,
|
||||
"Q": 81,
|
||||
"R": 82,
|
||||
"S": 83,
|
||||
"T": 84,
|
||||
"U": 85,
|
||||
"V": 86,
|
||||
"W": 87,
|
||||
"X": 88,
|
||||
"Y": 89,
|
||||
"Z": 90,
|
||||
"[": 91,
|
||||
"\\": 92,
|
||||
"]": 93,
|
||||
"^": 94,
|
||||
"_": 95,
|
||||
"`": 96,
|
||||
"a": 97,
|
||||
"b": 98,
|
||||
"c": 99,
|
||||
"d": 100,
|
||||
"e": 101,
|
||||
"f": 102,
|
||||
"g": 103,
|
||||
"h": 104,
|
||||
"i": 105,
|
||||
"j": 106,
|
||||
"k": 107,
|
||||
"l": 108,
|
||||
"m": 109,
|
||||
"n": 110,
|
||||
"o": 111,
|
||||
"p": 112,
|
||||
"q": 113,
|
||||
"r": 114,
|
||||
"s": 115,
|
||||
"t": 116,
|
||||
"u": 117,
|
||||
"v": 118,
|
||||
"w": 119,
|
||||
"x": 120,
|
||||
"y": 121,
|
||||
"z": 122,
|
||||
"{": 123,
|
||||
"|": 124,
|
||||
"}": 125,
|
||||
"~": 126,
|
||||
"Delete": 127
|
||||
};
|
||||
26
client/src/gamepadbutton.js
Normal file
26
client/src/gamepadbutton.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export const GamepadButton = {
|
||||
DpadUp: 0,
|
||||
DpadDown: 1,
|
||||
DpadLeft: 2,
|
||||
DpadRight: 3,
|
||||
North: 4,
|
||||
East: 5,
|
||||
South: 6,
|
||||
West: 7,
|
||||
LeftStick: 8,
|
||||
RightStick: 9,
|
||||
LeftShoulder: 10,
|
||||
RightShoulder: 11,
|
||||
Start: 12,
|
||||
Select: 13,
|
||||
LeftTrigger: 32,
|
||||
RightTrigger: 33,
|
||||
X: 7, // West
|
||||
Y: 4, // North
|
||||
A: 6, // South,
|
||||
B: 5, // East,
|
||||
Cross: 6, // South,
|
||||
Square: 7, // West,
|
||||
Triangle: 4, //North,
|
||||
Circle: 5 // East,
|
||||
};
|
||||
44
client/src/gamepadhandler.js
Normal file
44
client/src/gamepadhandler.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export class GamepadHandler extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this._controllers = {};
|
||||
window.requestAnimationFrame(this._updateStatus.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Gamepad} gamepad
|
||||
*/
|
||||
addGamepad(gamepad) {
|
||||
this._controllers[gamepad.index] = gamepad;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Gamepad} gamepad
|
||||
*/
|
||||
removeGamepad(gamepad) {
|
||||
delete this._controllers[gamepad.index];
|
||||
}
|
||||
|
||||
_updateStatus() {
|
||||
this._scanGamepad();
|
||||
for(let i in this._controllers) {
|
||||
const controller = this._controllers[i];
|
||||
|
||||
// gamepadupdated event type is own definition
|
||||
this.dispatchEvent(new GamepadEvent('gamepadupdated', {
|
||||
gamepad: controller
|
||||
}));
|
||||
}
|
||||
window.requestAnimationFrame(this._updateStatus.bind(this));
|
||||
}
|
||||
|
||||
_scanGamepad() {
|
||||
let gamepads = navigator.getGamepads();
|
||||
for (let i = 0; i < gamepads.length; i++) {
|
||||
if (gamepads[i] && (gamepads[i].index in this._controllers)) {
|
||||
this._controllers[gamepads[i].index] = gamepads[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
718
client/src/inputdevice.js
Normal file
718
client/src/inputdevice.js
Normal file
@@ -0,0 +1,718 @@
|
||||
import {
|
||||
MemoryHelper,
|
||||
} from "./memoryhelper.js";
|
||||
|
||||
import { CharNumber } from "./charnumber.js";
|
||||
import { Keymap } from "./keymap.js";
|
||||
import { MouseButton } from "./mousebutton.js";
|
||||
import { GamepadButton } from "./gamepadbutton.js";
|
||||
import { TouchPhase } from "./touchphase.js";
|
||||
import { TouchFlags } from "./touchflags.js";
|
||||
|
||||
export class FourCC {
|
||||
/**
|
||||
* {Number} _code;
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} a
|
||||
* @param {String} b
|
||||
* @param {String} c
|
||||
* @param {String} d
|
||||
*/
|
||||
constructor(a, b, c, d) {
|
||||
this._code = (a.charCodeAt() << 24)
|
||||
| (b.charCodeAt() << 16)
|
||||
| (c.charCodeAt() << 8)
|
||||
| d.charCodeAt();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
toInt32() {
|
||||
return this._code;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class InputDevice {
|
||||
|
||||
/**
|
||||
*
|
||||
* name;
|
||||
* layout;
|
||||
* deviceId;
|
||||
* usages;
|
||||
* description;
|
||||
*
|
||||
* _inputState;
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} name
|
||||
* @param {String} layout
|
||||
* @param {Number} deviceId
|
||||
* @param {String[]} usages
|
||||
* @param {Object} description
|
||||
*/
|
||||
constructor(name, layout, deviceId, usages, description) {
|
||||
this.name = name;
|
||||
this.layout = layout;
|
||||
this.deviceId = deviceId;
|
||||
this.usages = usages;
|
||||
this.description = description;
|
||||
|
||||
this._inputState = null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IInputState} state
|
||||
*/
|
||||
updateState(state) {
|
||||
this._inputState = state;
|
||||
}
|
||||
|
||||
queueEvent(event) {
|
||||
throw new Error(`Please implement this method. event:${event}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {IInputState}
|
||||
*/
|
||||
get currentState() {
|
||||
return this._inputState;
|
||||
}
|
||||
}
|
||||
|
||||
export class Mouse extends InputDevice {
|
||||
/**
|
||||
* @param {(MouseEvent|WheelEvent)} event
|
||||
*/
|
||||
queueEvent(event) {
|
||||
this.updateState(new MouseState(event));
|
||||
}
|
||||
}
|
||||
|
||||
export class Keyboard extends InputDevice {
|
||||
static get keycount() { return 110; }
|
||||
/**
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
queueEvent(event) {
|
||||
this.updateState(new KeyboardState(event, this.currentState));
|
||||
}
|
||||
}
|
||||
|
||||
export class Touchscreen extends InputDevice {
|
||||
/**
|
||||
* @param {TouchScreenEvent} event
|
||||
*/
|
||||
queueEvent(event, time) {
|
||||
this.updateState(new TouchscreenState(event, this.currentState, time));
|
||||
}
|
||||
}
|
||||
|
||||
export class Gamepad extends InputDevice {
|
||||
/**
|
||||
* @param {GamepadButtonEvent | GamepadAxisEvent} event
|
||||
*/
|
||||
queueEvent(event) {
|
||||
this.updateState(new GamepadState(event));
|
||||
}
|
||||
}
|
||||
|
||||
export class InputEvent {
|
||||
static get invalidEventId() { return 0; }
|
||||
static get size() { return 20; }
|
||||
|
||||
/**
|
||||
* field offset 0
|
||||
* @member {Number} type;
|
||||
*
|
||||
* field offset 4
|
||||
* @member {Number} sizeInBytes;
|
||||
*
|
||||
* field offset 6
|
||||
* @member {Number} deviceId;
|
||||
*
|
||||
* field offset 8
|
||||
* @member {Number} time;
|
||||
*
|
||||
* field offset 16
|
||||
* @member {Number} eventId;
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} type
|
||||
* @param {Number} sizeInBytes
|
||||
* @param {Number} deviceId
|
||||
* @param {Number} time
|
||||
*/
|
||||
constructor(type, sizeInBytes, deviceId, time) {
|
||||
this.type = type;
|
||||
this.sizeInBytes = sizeInBytes;
|
||||
this.deviceId = deviceId;
|
||||
this.time = time;
|
||||
this.eventId = InputEvent.invalidEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
let _buffer = new ArrayBuffer(InputEvent.size);
|
||||
let view = new DataView(_buffer);
|
||||
view.setInt32(0, this.type, true);
|
||||
view.setInt16(4, this.sizeInBytes, true);
|
||||
view.setInt16(6, this.deviceId, true);
|
||||
view.setFloat64(8, this.time, true);
|
||||
view.setInt16(16, this.sizeInBytes, true);
|
||||
return _buffer;
|
||||
}
|
||||
}
|
||||
|
||||
export class IInputState {
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
throw new Error('Please implement this field');
|
||||
}
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
get format() {
|
||||
throw new Error('Please implement this field');
|
||||
}
|
||||
}
|
||||
|
||||
export class MouseState extends IInputState {
|
||||
static get size() { return 30; }
|
||||
static get format() { return new FourCC('M', 'O', 'U', 'S').toInt32(); }
|
||||
|
||||
/**
|
||||
* field offset 0
|
||||
* @member {Array} position;
|
||||
*
|
||||
* field offset 8
|
||||
* @member {Array} delta;
|
||||
*
|
||||
* field offset 16
|
||||
* @member {Array} scroll;
|
||||
*
|
||||
* field offset 24
|
||||
* @member {ArrayBuffer} buttons;
|
||||
*
|
||||
* field offset 26
|
||||
* @member {Array} displayIndex;
|
||||
*
|
||||
* field offset 28
|
||||
* @member {Array} clickCount;
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MouseEvent | WheelEvent} event
|
||||
*/
|
||||
constructor(event) {
|
||||
super();
|
||||
|
||||
this.position = [event.clientX, event.clientY];
|
||||
this.delta = [event.movementX, -event.movementY];
|
||||
this.scroll = [0, 0];
|
||||
if(event.type === 'wheel') {
|
||||
this.scroll = [event.deltaX, event.deltaY];
|
||||
}
|
||||
this.buttons = new ArrayBuffer(2);
|
||||
|
||||
const left = event.buttons & 1 << 0;
|
||||
const right = event.buttons & 1 << 1;
|
||||
const middle = event.buttons & 1 << 2;
|
||||
const back = event.buttons & 1 << 3;
|
||||
const forward = event.buttons & 1 << 4;
|
||||
|
||||
MemoryHelper.writeSingleBit(this.buttons, MouseButton.Left, left);
|
||||
MemoryHelper.writeSingleBit(this.buttons, MouseButton.Right, right);
|
||||
MemoryHelper.writeSingleBit(this.buttons, MouseButton.Middle, middle);
|
||||
MemoryHelper.writeSingleBit(this.buttons, MouseButton.Forward, forward);
|
||||
MemoryHelper.writeSingleBit(this.buttons, MouseButton.Back, back);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
const size = MouseState.size;
|
||||
const buttons = new Uint16Array(this.buttons)[0];
|
||||
let _buffer = new ArrayBuffer(size);
|
||||
let view = new DataView(_buffer);
|
||||
view.setFloat32(0, this.position[0], true);
|
||||
view.setFloat32(4, this.position[1], true);
|
||||
view.setFloat32(8, this.delta[0], true);
|
||||
view.setFloat32(12, this.delta[1], true);
|
||||
view.setFloat32(16, this.scroll[0], true);
|
||||
view.setFloat32(20, this.scroll[1], true);
|
||||
view.setUint16(24, buttons, true);
|
||||
view.setUint16(26, this.displayIndex, true);
|
||||
view.setUint16(28, this.clickCount, true);
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
get format() {
|
||||
return MouseState.format;
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyboardState extends IInputState {
|
||||
static get sizeInBits() { return Keyboard.keycount; }
|
||||
static get sizeInBytes() { return (KeyboardState.sizeInBits + 7) >> 3; }
|
||||
static get format() { return new FourCC('K', 'E', 'Y', 'S').toInt32(); }
|
||||
|
||||
/**
|
||||
* field offset 0
|
||||
* @number {ArrayBuffer} keys;
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
constructor(event, state) {
|
||||
super();
|
||||
if (state == null || state.keys == null) {
|
||||
this.keys = new ArrayBuffer(KeyboardState.sizeInBytes);
|
||||
} else {
|
||||
this.keys = state.keys;
|
||||
}
|
||||
let value = false;
|
||||
switch(event.type) {
|
||||
case 'keydown':
|
||||
value = true;
|
||||
break;
|
||||
case 'keyup':
|
||||
value = false;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown event type ${event.type})`);
|
||||
}
|
||||
const key = Keymap[event.code];
|
||||
MemoryHelper.writeSingleBit(this.keys, key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
return this.keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
get format() {
|
||||
return KeyboardState.format;
|
||||
}
|
||||
}
|
||||
|
||||
export class TouchState {
|
||||
static get format() { return new FourCC('T', 'O', 'U', 'C').toInt32(); }
|
||||
static get size() { return 56; }
|
||||
static incrementTouchId() {
|
||||
if(TouchState._currentTouchId === undefined) {
|
||||
TouchState._currentTouchId = 0;
|
||||
}
|
||||
return ++TouchState._currentTouchId;
|
||||
}
|
||||
static prevTouches() {
|
||||
if(TouchState._prevTouches === undefined) {
|
||||
// max touch count is 10
|
||||
TouchState._prevTouches = new Array(10);
|
||||
}
|
||||
return TouchState._prevTouches;
|
||||
}
|
||||
|
||||
/**
|
||||
* field offset 0
|
||||
* @number {Number} touchId;
|
||||
* field offset 4
|
||||
* @number {Number[]} position;
|
||||
* field offset 12
|
||||
* @number {Number[]} delta;
|
||||
* field offset 20
|
||||
* @number {Number} pressure;
|
||||
* field offset 24
|
||||
* @number {Number[]} radius;
|
||||
* field offset 32
|
||||
* @number {Number} phase;
|
||||
* field offset 33
|
||||
* @number {Number} tapCount;
|
||||
* field offset 34
|
||||
* @number {Number} displayIndex;
|
||||
* field offset 35
|
||||
* @number {Number} flag;
|
||||
* field offset 36
|
||||
* @number {Number} padding;
|
||||
* field offset 40
|
||||
* @number {Number} startTime;
|
||||
* field offset 48
|
||||
* @number {Number[]} startPosition;
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @param {Touch} touchId
|
||||
* @param {TouchState} prevState
|
||||
* @param {Number[]} position
|
||||
* @param {Number} pressure
|
||||
* @param {Number[]} radius
|
||||
* @param {TouchPhase} phaseId
|
||||
* @param {Number} time
|
||||
*/
|
||||
constructor(touchId, prevState, position, pressure, radius, phaseId, time) {
|
||||
this.touchId = touchId;
|
||||
this.position = position != null ? position.slice() : null;
|
||||
if(phaseId == TouchPhase.Moved) {
|
||||
this.delta = [this.position[0] - prevState.position[0], this.position[1] - prevState.position[1]];
|
||||
} else {
|
||||
this.delta = [0, 0];
|
||||
}
|
||||
this.pressure = pressure;
|
||||
this.radius = radius != null ? radius.slice(): null;
|
||||
this.phaseId = phaseId;
|
||||
this.tapCount = 0;
|
||||
this.displayIndex = 0;
|
||||
this.flags = 0;
|
||||
this.padding = 0;
|
||||
if(phaseId == TouchPhase.Began) {
|
||||
this.startTime = time;
|
||||
this.startPosition = this.position.slice();
|
||||
} else {
|
||||
this.startTime = prevState != null ? prevState.startTime : null;
|
||||
this.startPosition = prevState != null ? prevState.startPosition.slice() : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
copy() {
|
||||
let state = new TouchState();
|
||||
state.touchId = this.touchId;
|
||||
state.position = this.position.slice();
|
||||
state.delta = this.delta.slice();
|
||||
state.pressure = this.pressure;
|
||||
state.radius = this.radius.slice();
|
||||
state.phaseId = this.phaseId;
|
||||
state.tapCount = this.tapCount;
|
||||
state.displayIndex = this.displayIndex;
|
||||
state.flags = this.flags;
|
||||
state.padding = this.padding;
|
||||
state.startTime = this.startTime;
|
||||
state.startPosition = this.startPosition.slice();
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
const size = TouchState.size; // todo
|
||||
let _buffer = new ArrayBuffer(size);
|
||||
let view = new DataView(_buffer);
|
||||
|
||||
view.setInt32(0, this.touchId, true);
|
||||
view.setFloat32(4, this.position[0], true);
|
||||
view.setFloat32(8, this.position[1], true);
|
||||
view.setFloat32(12, this.delta[0], true);
|
||||
view.setFloat32(16, this.delta[1], true);
|
||||
view.setFloat32(20, this.pressure, true);
|
||||
view.setFloat32(24, this.radius[0], true);
|
||||
view.setFloat32(28, this.radius[1], true);
|
||||
view.setInt8(32, this.phaseId, true);
|
||||
view.setInt8(33, this.tapCount, true);
|
||||
view.setInt8(34, this.displayIndex, true);
|
||||
view.setInt8(35, this.flags, true);
|
||||
view.setInt32(36, this.padding, true);
|
||||
view.setFloat64(40, this.startTime, true);
|
||||
view.setFloat32(48, this.startPosition[0], true);
|
||||
view.setFloat32(52, this.startPosition[1], true);
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
get format() {
|
||||
return TouchState.format;
|
||||
}
|
||||
}
|
||||
|
||||
export class TouchscreenState extends IInputState {
|
||||
static get maxTouches() { return 10; }
|
||||
static get format() { return new FourCC('T', 'S', 'C', 'R').toInt32(); }
|
||||
static convertPhaseId(type) {
|
||||
let phaseId = TouchPhase.Stationary;
|
||||
switch(type) {
|
||||
case 'touchstart':
|
||||
phaseId = TouchPhase.Began; break;
|
||||
case 'touchend':
|
||||
phaseId = TouchPhase.Ended; break;
|
||||
case 'touchmove':
|
||||
phaseId = TouchPhase.Moved; break;
|
||||
case 'touchcancel':
|
||||
phaseId = TouchPhase.Canceled; break;
|
||||
}
|
||||
return phaseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TouchEvent} event
|
||||
* @param {TouchScreenState} state
|
||||
* @param {Date} time
|
||||
*/
|
||||
constructor(event, state, time) {
|
||||
super();
|
||||
|
||||
switch(event.type) {
|
||||
// `click` event is called when releasing mouse button or finger on screen.
|
||||
case 'click' : {
|
||||
this.touchData = new Array(state.touchData.length);
|
||||
for(let i = 0; i < state.touchData.length; i++) {
|
||||
this.touchData[i] = state.touchData[i];
|
||||
if(this.touchData[i].phaseId == TouchPhase.Ended) {
|
||||
this.touchData[i].tapCount = 1;
|
||||
this.touchData[i].flags |= TouchFlags.Tap;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let touches = event.changedTouches;
|
||||
this.touchData = new Array(touches.length);
|
||||
for(let i = 0; i < touches.length; i++) {
|
||||
const touch = touches[i];
|
||||
const position = [touch.clientX, touch.clientY];
|
||||
const phaseId = TouchscreenState.convertPhaseId(event.type);
|
||||
const pressure = touch.force;
|
||||
const radius = [touch.radiusX, touch.radiusY];
|
||||
|
||||
// `touchId` in InputSystem must be set uniquely.
|
||||
// The numbers of `touch.identifier` in Web API are reused, so these are not unique.
|
||||
const touchId = phaseId == TouchPhase.Began ? TouchState.incrementTouchId() : TouchState.prevTouches()[touch.identifier].touchId;
|
||||
const prevState = phaseId != TouchPhase.Began ? TouchState.prevTouches()[touch.identifier] : null;
|
||||
const touchData = new TouchState(touchId, prevState, position, pressure, radius, phaseId, time);
|
||||
|
||||
// cache state
|
||||
TouchState.prevTouches()[touch.identifier] = touchData;
|
||||
this.touchData[i] = touchData;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
const size = TouchState.size * this.touchData.length;
|
||||
let _buffer = new ArrayBuffer(size);
|
||||
let view = new Uint8Array(_buffer);
|
||||
for(let i = 0; i < this.touchData.length; i++) {
|
||||
view.set(new Uint8Array(this.touchData[i].buffer), TouchState.size * i);
|
||||
}
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
get format() {
|
||||
return TouchscreenState.format;
|
||||
}
|
||||
}
|
||||
|
||||
export class GamepadState extends IInputState {
|
||||
static get size() { return 28; }
|
||||
static get format() { return new FourCC('G', 'P', 'A', 'D').toInt32(); }
|
||||
|
||||
/**
|
||||
* field offset 0
|
||||
* @member buttons;
|
||||
*
|
||||
* field offset 4
|
||||
* @member leftStick;
|
||||
*
|
||||
* field offset 12
|
||||
* @member rightStick;
|
||||
*
|
||||
* field offset 20
|
||||
* @member leftTrigger;
|
||||
*
|
||||
* field offset 24
|
||||
* @member rightTrigger;
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {GamepadButtonEvent | GamepadAxisEvent} event
|
||||
*/
|
||||
constructor(event) {
|
||||
super();
|
||||
const gamepad = event.gamepad;
|
||||
const buttons = event.gamepad.buttons;
|
||||
|
||||
this.buttons = new ArrayBuffer(4);
|
||||
this.leftStick = [ gamepad.axes[0], -gamepad.axes[1] ];
|
||||
this.rightStick = [ gamepad.axes[2], -gamepad.axes[3] ];
|
||||
this.leftTrigger = buttons[6].value;
|
||||
this.rightTrigger = buttons[7].value;
|
||||
|
||||
// see https://w3c.github.io/gamepad/#remapping
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.A, buttons[0].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.B, buttons[1].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.X, buttons[2].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.Y, buttons[3].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.LeftShoulder, buttons[4].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.RightShoulder, buttons[5].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.LeftTrigger, buttons[6].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.RightTrigger, buttons[7].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.Select, buttons[8].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.Start, buttons[9].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.LeftStick, buttons[10].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.RightStick, buttons[11].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.DpadUp, buttons[12].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.DpadDown, buttons[13].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.DpadLeft, buttons[14].pressed);
|
||||
MemoryHelper.writeSingleBit(this.buttons, GamepadButton.DpadRight, buttons[15].pressed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
const size = GamepadState.size;
|
||||
let _buffer = new ArrayBuffer(size);
|
||||
let view = new DataView(_buffer);
|
||||
view.setUint32(0, new Uint32Array(this.buttons)[0], true);
|
||||
view.setFloat32(4, this.leftStick[0], true);
|
||||
view.setFloat32(8, this.leftStick[1], true);
|
||||
view.setFloat32(12, this.rightStick[0], true);
|
||||
view.setFloat32(16, this.rightStick[1], true);
|
||||
view.setFloat32(20, this.leftTrigger, true);
|
||||
view.setFloat32(24, this.rightTrigger, true);
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
get format() {
|
||||
return GamepadState.format;
|
||||
}
|
||||
}
|
||||
|
||||
export class TextEvent {
|
||||
static get format() { return new FourCC('T', 'E', 'X', 'T').toInt32(); }
|
||||
|
||||
/**
|
||||
* field offset 0
|
||||
* @member {InputEvent} baseEvent;
|
||||
*
|
||||
* field offset 20
|
||||
* @member {Number} character;
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} deviceId
|
||||
* @param {KeyboardEvent} event
|
||||
* @param {Number} time
|
||||
* @returns {TextEvent}
|
||||
|
||||
*/
|
||||
static create(deviceId, event, time) {
|
||||
const eventSize = InputEvent.size + MemoryHelper.sizeOfInt;
|
||||
|
||||
let textEvent = new TextEvent();
|
||||
textEvent.baseEvent = new InputEvent(TextEvent.format, eventSize, deviceId, time);
|
||||
textEvent.character = CharNumber[event.key];
|
||||
return textEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
const size = InputEvent.size + MemoryHelper.sizeOfInt;
|
||||
let _buffer = new ArrayBuffer(size);
|
||||
let arrayView = new Uint8Array(_buffer);
|
||||
let dataView = new DataView(_buffer);
|
||||
arrayView.set(new Uint8Array(this.baseEvent.buffer), 0);
|
||||
dataView.setInt32(InputEvent.size, this.character, true);
|
||||
return _buffer;
|
||||
}
|
||||
}
|
||||
|
||||
export class StateEvent {
|
||||
static get format() { return new FourCC('S', 'T', 'A', 'T').toInt32(); }
|
||||
|
||||
/**
|
||||
* field offset 0
|
||||
* @member {InputEvent} baseEvent;
|
||||
*
|
||||
* field offset 20
|
||||
* @member {Number} stateFormat;
|
||||
*
|
||||
* field offset 24
|
||||
* @member {ArrayBuffer} stateData;
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {InputDevice} device
|
||||
* @param {Number} time
|
||||
* @returns {StateEvent}
|
||||
*/
|
||||
static from(device, time) {
|
||||
return StateEvent.fromState(device.currentState, device.deviceId, time);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IInputState} state
|
||||
* @param {Number} deviceId
|
||||
* @param {Number} time
|
||||
*/
|
||||
static fromState(state, deviceId, time) {
|
||||
const stateData = state.buffer;
|
||||
const stateSize = stateData.byteLength;
|
||||
const eventSize = InputEvent.size + MemoryHelper.sizeOfInt + stateSize;
|
||||
|
||||
let stateEvent = new StateEvent();
|
||||
stateEvent.baseEvent = new InputEvent(StateEvent.format, eventSize, deviceId, time);
|
||||
stateEvent.stateFormat = state.format;
|
||||
stateEvent.stateData = stateData;
|
||||
return stateEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
const stateSize = this.stateData.byteLength;
|
||||
const size = InputEvent.size + MemoryHelper.sizeOfInt + stateSize;
|
||||
let _buffer = new ArrayBuffer(size);
|
||||
let uint8View = new Uint8Array(_buffer);
|
||||
let dataView = new DataView(_buffer);
|
||||
uint8View.set(new Uint8Array(this.baseEvent.buffer), 0);
|
||||
dataView.setInt32(InputEvent.size, this.stateFormat, true);
|
||||
uint8View.set(new Uint8Array(this.stateData), InputEvent.size+MemoryHelper.sizeOfInt);
|
||||
return _buffer;
|
||||
}
|
||||
}
|
||||
299
client/src/inputremoting.js
Normal file
299
client/src/inputremoting.js
Normal file
@@ -0,0 +1,299 @@
|
||||
import {
|
||||
StateEvent,
|
||||
} from "./inputdevice.js";
|
||||
|
||||
import {
|
||||
MemoryHelper
|
||||
} from "./memoryhelper.js";
|
||||
|
||||
export class LocalInputManager {
|
||||
constructor() {
|
||||
this._onevent = new EventTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* event type 'event', 'changedeviceusage'
|
||||
* @return {Event}
|
||||
*/
|
||||
get onEvent() {
|
||||
return this._onevent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Event}
|
||||
*/
|
||||
get devices() {
|
||||
throw new Error(`Please implement this method.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Number} time (sec)
|
||||
*/
|
||||
get startTime() {
|
||||
return this._startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Number} time (sec)
|
||||
*/
|
||||
get timeSinceStartup() {
|
||||
return Date.now()/1000 - this.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} time (sec)
|
||||
*/
|
||||
setStartTime(time) {
|
||||
this._startTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
export const InputDeviceChange = {
|
||||
Added: 0,
|
||||
Removed: 1,
|
||||
Disconnected: 2,
|
||||
Reconnected: 3,
|
||||
Enabled: 4,
|
||||
Disabled: 5,
|
||||
UsageChanged: 6,
|
||||
ConfigurationChanged: 7,
|
||||
Destroyed: 8,
|
||||
};
|
||||
|
||||
export class InputRemoting {
|
||||
/**
|
||||
* @param {LocalInputManager} manager
|
||||
*/
|
||||
constructor(manager) {
|
||||
this._localManager = manager;
|
||||
this._subscribers = new Array();
|
||||
this._sending = false;
|
||||
}
|
||||
|
||||
startSending() {
|
||||
if(this._sending) {
|
||||
return;
|
||||
}
|
||||
this._sending = true;
|
||||
|
||||
const onEvent = e => {
|
||||
this._sendEvent(e.detail.event);
|
||||
};
|
||||
|
||||
const onDeviceChange = e => {
|
||||
this._sendDeviceChange(e.detail.device, e.detail.change);
|
||||
};
|
||||
|
||||
this._localManager.setStartTime(Date.now()/1000);
|
||||
this._localManager.onEvent.addEventListener("event", onEvent);
|
||||
this._localManager.onEvent.addEventListener("changedeviceusage", onDeviceChange);
|
||||
this._sendInitialMessages();
|
||||
}
|
||||
|
||||
stopSending() {
|
||||
if (!this._sending) {
|
||||
return;
|
||||
}
|
||||
this._sending = false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Observer} observer
|
||||
*/
|
||||
subscribe(observer) {
|
||||
this._subscribers.push(observer);
|
||||
}
|
||||
|
||||
_sendInitialMessages() {
|
||||
this._sendAllGeneratedLayouts();
|
||||
this._sendAllDevices();
|
||||
}
|
||||
|
||||
_sendAllGeneratedLayouts() {
|
||||
// todo:
|
||||
}
|
||||
|
||||
_sendAllDevices() {
|
||||
var devices = this._localManager.devices;
|
||||
if(devices == null)
|
||||
return;
|
||||
for (const device of devices) {
|
||||
this._sendDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
_sendDevice(device) {
|
||||
const newDeviceMessage = NewDeviceMsg.create(device);
|
||||
this._send(newDeviceMessage);
|
||||
|
||||
// Send current state. We do this here in this case as the device
|
||||
// may have been added some time ago and thus have already received events.
|
||||
|
||||
// todo:
|
||||
// const stateEventMessage = NewEventsMsg.createStateEvent(device);
|
||||
// this._send(stateEventMessage);
|
||||
}
|
||||
|
||||
_sendEvent(event) {
|
||||
const message = NewEventsMsg.create(event);
|
||||
this._send(message);
|
||||
}
|
||||
|
||||
_sendDeviceChange(device, change) {
|
||||
if (this._subscribers == null)
|
||||
return;
|
||||
|
||||
let msg = null;
|
||||
switch (change) {
|
||||
case InputDeviceChange.Added:
|
||||
msg = NewDeviceMsg.Create(device);
|
||||
break;
|
||||
case InputDeviceChange.Removed:
|
||||
msg = RemoveDeviceMsg.Create(device);
|
||||
break;
|
||||
case InputDeviceChange.UsageChanged:
|
||||
msg = ChangeUsageMsg.Create(device);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
this._send(msg);
|
||||
}
|
||||
|
||||
_send(message) {
|
||||
for(let subscriber of this._subscribers) {
|
||||
subscriber.onNext(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MessageType = {
|
||||
Connect: 0,
|
||||
Disconnect: 1,
|
||||
NewLayout: 2,
|
||||
NewDevice: 3,
|
||||
NewEvents: 4,
|
||||
RemoveDevice: 5,
|
||||
RemoveLayout: 6,
|
||||
ChangeUsages: 7,
|
||||
StartSending: 8,
|
||||
StopSending: 9,
|
||||
};
|
||||
|
||||
export class Message {
|
||||
/**
|
||||
* field offset 0
|
||||
* {Number} participant_id;
|
||||
*
|
||||
* field offset 4
|
||||
* {Number} type;
|
||||
*
|
||||
* field offset 8
|
||||
* {Number} length;
|
||||
*
|
||||
* field offset 12
|
||||
* {ArrayBuffer} data;
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} participantId
|
||||
* @param {MessageType} type
|
||||
* @param {ArrayBuffer} data
|
||||
*/
|
||||
constructor(participantId, type, data) {
|
||||
this.participant_id = participantId;
|
||||
this.type = type;
|
||||
this.length = data.byteLength;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
get buffer() {
|
||||
const totalSize =
|
||||
MemoryHelper.sizeOfInt + // size of this.participant_id
|
||||
MemoryHelper.sizeOfInt + // size of this.type
|
||||
MemoryHelper.sizeOfInt + // size of this.length
|
||||
this.data.byteLength; // size of this.data
|
||||
|
||||
let buffer = new ArrayBuffer(totalSize);
|
||||
let dataView = new DataView(buffer);
|
||||
let uint8view = new Uint8Array(buffer);
|
||||
dataView.setUint32(0, this.participant_id, true);
|
||||
dataView.setUint32(4, this.type, true);
|
||||
dataView.setUint32(8, this.length, true);
|
||||
uint8view.set(new Uint8Array(this.data), 12);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
export class NewDeviceMsg {
|
||||
/**
|
||||
* @param {InputDevice} device
|
||||
* @returns {Message}
|
||||
*/
|
||||
static create(device) {
|
||||
const data = {
|
||||
name: device.name,
|
||||
layout: device.layout,
|
||||
deviceId: device.deviceId,
|
||||
variants: device.variants,
|
||||
description: device.description
|
||||
};
|
||||
const json = JSON.stringify(data);
|
||||
let buffer = new ArrayBuffer(json.length*2); // 2 bytes for each char
|
||||
let view = new Uint8Array(buffer);
|
||||
const length = json.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
view[i] = json.charCodeAt(i);
|
||||
}
|
||||
return new Message(0, MessageType.NewDevice, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
export class NewEventsMsg {
|
||||
/**
|
||||
*
|
||||
* @param {InputDevice} device
|
||||
* @returns {Message}
|
||||
*/
|
||||
static createStateEvent(device) {
|
||||
const events = StateEvent.from(device);
|
||||
return NewEventsMsg.create(events);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {StateEvent} event
|
||||
* @returns {Message}
|
||||
*/
|
||||
static create(event) {
|
||||
return new Message(0, MessageType.NewEvents, event.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveDeviceMsg {
|
||||
/**
|
||||
*
|
||||
* @param {InputDevice} device
|
||||
* @returns {Message}
|
||||
*/
|
||||
static create(device) {
|
||||
let buffer = new ArrayBuffer(MemoryHelper.sizeOfInt);
|
||||
let view = new DataView(buffer);
|
||||
view.setInt32(device.deviceId);
|
||||
return new Message(0, MessageType.RemoveDevice, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
export class ChangeUsageMsg {
|
||||
|
||||
static create(device) {
|
||||
// todo:
|
||||
throw new Error(`ChangeUsageMsg class is not implemented. device=${device}`);
|
||||
}
|
||||
}
|
||||
120
client/src/keymap.js
Normal file
120
client/src/keymap.js
Normal file
@@ -0,0 +1,120 @@
|
||||
export const Keymap = {
|
||||
"Space": 1,
|
||||
"Enter": 2,
|
||||
"Tab": 3,
|
||||
"Backquote": 4,
|
||||
"Quote": 5,
|
||||
"Semicolon": 6,
|
||||
"Comma": 7,
|
||||
"Period": 8,
|
||||
"Slash": 9,
|
||||
"Backslash": 10,
|
||||
"BracketLeft": 11,
|
||||
"BracketRight": 12,
|
||||
"Minus": 13,
|
||||
"Equal": 14,
|
||||
"KeyA": 15,
|
||||
"KeyB": 16,
|
||||
"KeyC": 17,
|
||||
"KeyD": 18,
|
||||
"KeyE": 19,
|
||||
"KeyF": 20,
|
||||
"KeyG": 21,
|
||||
"KeyH": 22,
|
||||
"KeyI": 23,
|
||||
"KeyJ": 24,
|
||||
"KeyK": 25,
|
||||
"KeyL": 26,
|
||||
"KeyM": 27,
|
||||
"KeyN": 28,
|
||||
"KeyO": 29,
|
||||
"KeyP": 30,
|
||||
"KeyQ": 31,
|
||||
"KeyR": 32,
|
||||
"KeyS": 33,
|
||||
"KeyT": 34,
|
||||
"KeyU": 35,
|
||||
"KeyV": 36,
|
||||
"KeyW": 37,
|
||||
"KeyX": 38,
|
||||
"KeyY": 39,
|
||||
"KeyZ": 40,
|
||||
"Digit1": 41,
|
||||
"Digit2": 42,
|
||||
"Digit3": 43,
|
||||
"Digit4": 44,
|
||||
"Digit5": 45,
|
||||
"Digit6": 46,
|
||||
"Digit7": 47,
|
||||
"Digit8": 48,
|
||||
"Digit9": 49,
|
||||
"Digit0": 50,
|
||||
"ShiftLeft": 51,
|
||||
"ShiftRight": 52,
|
||||
"AltLeft": 53,
|
||||
"AltRight": 54,
|
||||
// "AltGr": 54,
|
||||
"ControlLeft": 55,
|
||||
"ControlRight": 56,
|
||||
"MetaLeft": 57,
|
||||
"MetaRight": 58,
|
||||
// "LeftWindows": 57,
|
||||
// "RightWindows": 58,
|
||||
// "LeftApple": 57,
|
||||
// "RightApple": 58,
|
||||
// "LeftCommand": 57,
|
||||
// "RightCommand": 58,
|
||||
"ContextMenu": 59,
|
||||
"Escape": 60,
|
||||
"ArrowLeft": 61,
|
||||
"ArrowRight": 62,
|
||||
"ArrowUp": 63,
|
||||
"ArrowDown": 64,
|
||||
"Backspace": 65,
|
||||
"PageDown": 66,
|
||||
"PageUp": 67,
|
||||
"Home": 68,
|
||||
"End": 69,
|
||||
"Insert": 70,
|
||||
"Delete": 71,
|
||||
"CapsLock": 72,
|
||||
"NumLock": 73,
|
||||
"PrintScreen": 74,
|
||||
"ScrollLock": 75,
|
||||
"Pause": 76,
|
||||
"NumpadEnter": 77,
|
||||
"NumpadDivide": 78,
|
||||
"NumpadMultiply": 79,
|
||||
"NumpadAdd": 80,
|
||||
"NumpadSubtract": 81,
|
||||
"NumpadDecimal": 82,
|
||||
"NumpadEquals": 83,
|
||||
"Numpad0": 84,
|
||||
"Numpad1": 85,
|
||||
"Numpad2": 86,
|
||||
"Numpad3": 87,
|
||||
"Numpad4": 88,
|
||||
"Numpad5": 89,
|
||||
"Numpad6": 90,
|
||||
"Numpad7": 91,
|
||||
"Numpad8": 92,
|
||||
"Numpad9": 93,
|
||||
"F1": 94,
|
||||
"F2": 95,
|
||||
"F3": 96,
|
||||
"F4": 97,
|
||||
"F5": 98,
|
||||
"F6": 99,
|
||||
"F7": 100,
|
||||
"F8": 101,
|
||||
"F9": 102,
|
||||
"F10": 103,
|
||||
"F11": 104,
|
||||
"F12": 105,
|
||||
// "OEM1": 106,
|
||||
// "OEM2": 107,
|
||||
// "OEM3": 108,
|
||||
// "OEM4": 109,
|
||||
// "OEM5": 110,
|
||||
// "IMESelected": 111,
|
||||
};
|
||||
29
client/src/logger.js
Normal file
29
client/src/logger.js
Normal file
@@ -0,0 +1,29 @@
|
||||
let isDebug = false;
|
||||
|
||||
export function enable() {
|
||||
isDebug = true;
|
||||
}
|
||||
|
||||
export function disable() {
|
||||
isDebug = false;
|
||||
}
|
||||
|
||||
export function debug(msg) {
|
||||
isDebug && console.debug(msg);
|
||||
}
|
||||
|
||||
export function info(msg) {
|
||||
isDebug && console.info(msg);
|
||||
}
|
||||
|
||||
export function log(msg) {
|
||||
isDebug && console.log(msg);
|
||||
}
|
||||
|
||||
export function warn(msg) {
|
||||
isDebug && console.warn(msg);
|
||||
}
|
||||
|
||||
export function error(msg) {
|
||||
isDebug && console.error(msg);
|
||||
}
|
||||
28
client/src/memoryhelper.js
Normal file
28
client/src/memoryhelper.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export class MemoryHelper {
|
||||
/**
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @param {number} bitOffset
|
||||
* @param {boolean} value
|
||||
*/
|
||||
static writeSingleBit(buffer, bitOffset, value) {
|
||||
let view = new Uint8Array(buffer);
|
||||
const index = Math.floor(bitOffset / 8);
|
||||
bitOffset = bitOffset % 8;
|
||||
const byte = view[index];
|
||||
let newByte = 1 << bitOffset;
|
||||
if(value) {
|
||||
newByte = newByte | byte;
|
||||
}
|
||||
else {
|
||||
newByte = ~newByte & byte;
|
||||
}
|
||||
view[index] = newByte;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Number}
|
||||
*/
|
||||
static get sizeOfInt() {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
7
client/src/mousebutton.js
Normal file
7
client/src/mousebutton.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const MouseButton = {
|
||||
Left: 0,
|
||||
Right: 1,
|
||||
Middle: 2,
|
||||
Foaward: 3,
|
||||
Back: 4,
|
||||
};
|
||||
187
client/src/peer.js
Normal file
187
client/src/peer.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import * as Logger from "./logger.js";
|
||||
|
||||
export default class Peer extends EventTarget {
|
||||
constructor(connectionId, polite, config, resendIntervalMsec = 5000) {
|
||||
super();
|
||||
const _this = this;
|
||||
this.connectionId = connectionId;
|
||||
this.polite = polite;
|
||||
this.config = config;
|
||||
this.pc = new RTCPeerConnection(this.config);
|
||||
this.makingOffer = false;
|
||||
this.waitingAnswer = false;
|
||||
this.ignoreOffer = false;
|
||||
this.srdAnswerPending = false;
|
||||
this.log = str => void Logger.log(`[${_this.polite ? 'POLITE' : 'IMPOLITE'}] ${str}`);
|
||||
this.warn = str => void Logger.warn(`[${_this.polite ? 'POLITE' : 'IMPOLITE'}] ${str}`);
|
||||
this.assert_equals = window.assert_equals ? window.assert_equals : (a, b, msg) => { if (a === b) { return; } throw new Error(`${msg} expected ${b} but got ${a}`); };
|
||||
this.interval = resendIntervalMsec;
|
||||
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
|
||||
|
||||
this.pc.ontrack = e => {
|
||||
_this.log(`ontrack:${e}`);
|
||||
_this.dispatchEvent(new CustomEvent('trackevent', { detail: e }));
|
||||
};
|
||||
this.pc.ondatachannel = e => {
|
||||
_this.log(`ondatachannel:${e}`);
|
||||
_this.dispatchEvent(new CustomEvent('adddatachannel', { detail: e }));
|
||||
};
|
||||
this.pc.onicecandidate = ({ candidate }) => {
|
||||
_this.log(`send candidate:${candidate}`);
|
||||
if (candidate == null) {
|
||||
return;
|
||||
}
|
||||
_this.dispatchEvent(new CustomEvent('sendcandidate', { detail: { connectionId: _this.connectionId, candidate: candidate.candidate, sdpMLineIndex: candidate.sdpMLineIndex, sdpMid: candidate.sdpMid } }));
|
||||
};
|
||||
|
||||
this.pc.onnegotiationneeded = this._onNegotiation.bind(this);
|
||||
|
||||
this.pc.onsignalingstatechange = () => {
|
||||
_this.log(`signalingState changed:${_this.pc.signalingState}`);
|
||||
};
|
||||
|
||||
this.pc.oniceconnectionstatechange = () => {
|
||||
_this.log(`iceConnectionState changed:${_this.pc.iceConnectionState}`);
|
||||
if (_this.pc.iceConnectionState === 'failed') {
|
||||
this.dispatchEvent(new Event('disconnect'));
|
||||
}
|
||||
};
|
||||
|
||||
this.pc.onicegatheringstatechange = () => {
|
||||
_this.log(`iceGatheringState changed:${_this.pc.iceGatheringState}'`);
|
||||
};
|
||||
|
||||
this.loopResendOffer();
|
||||
}
|
||||
|
||||
async _onNegotiation() {
|
||||
try {
|
||||
this.log(`SLD due to negotiationneeded`);
|
||||
this.assert_equals(this.pc.signalingState, 'stable', 'negotiationneeded always fires in stable state');
|
||||
this.assert_equals(this.makingOffer, false, 'negotiationneeded not already in progress');
|
||||
this.makingOffer = true;
|
||||
await this.pc.setLocalDescription();
|
||||
this.assert_equals(this.pc.signalingState, 'have-local-offer', 'negotiationneeded not racing with onmessage');
|
||||
this.assert_equals(this.pc.localDescription.type, 'offer', 'negotiationneeded SLD worked');
|
||||
this.waitingAnswer = true;
|
||||
this.dispatchEvent(new CustomEvent('sendoffer', { detail: { connectionId: this.connectionId, sdp: this.pc.localDescription.sdp } }));
|
||||
} catch (e) {
|
||||
this.log(e);
|
||||
} finally {
|
||||
this.makingOffer = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loopResendOffer() {
|
||||
while (this.connectionId) {
|
||||
if (this.pc && this.waitingAnswer) {
|
||||
this.dispatchEvent(new CustomEvent('sendoffer', { detail: { connectionId: this.connectionId, sdp: this.pc.localDescription.sdp } }));
|
||||
}
|
||||
await this.sleep(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.connectionId = null;
|
||||
if (this.pc) {
|
||||
this.pc.close();
|
||||
this.pc = null;
|
||||
}
|
||||
}
|
||||
|
||||
getTransceivers(connectionId) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pc.getTransceivers();
|
||||
}
|
||||
|
||||
addTrack(connectionId, track) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pc.addTrack(track);
|
||||
}
|
||||
|
||||
addTransceiver(connectionId, trackOrKind, init) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pc.addTransceiver(trackOrKind, init);
|
||||
}
|
||||
|
||||
createDataChannel(connectionId, label) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pc.createDataChannel(label);
|
||||
}
|
||||
|
||||
async getStats(connectionId) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.pc.getStats();
|
||||
}
|
||||
|
||||
async onGotDescription(connectionId, description) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _this = this;
|
||||
const isStable =
|
||||
this.pc.signalingState == 'stable' ||
|
||||
(this.pc.signalingState == 'have-local-offer' && this.srdAnswerPending);
|
||||
this.ignoreOffer =
|
||||
description.type == 'offer' && !this.polite && (this.makingOffer || !isStable);
|
||||
|
||||
if (this.ignoreOffer) {
|
||||
_this.log(`glare - ignoring offer`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.waitingAnswer = false;
|
||||
this.srdAnswerPending = description.type == 'answer';
|
||||
_this.log(`SRD(${description.type})`);
|
||||
await this.pc.setRemoteDescription(description);
|
||||
this.srdAnswerPending = false;
|
||||
|
||||
if (description.type == 'offer') {
|
||||
_this.dispatchEvent(new CustomEvent('ongotoffer', { detail: { connectionId: _this.connectionId } }));
|
||||
|
||||
_this.assert_equals(this.pc.signalingState, 'have-remote-offer', 'Remote offer');
|
||||
_this.assert_equals(this.pc.remoteDescription.type, 'offer', 'SRD worked');
|
||||
_this.log('SLD to get back to stable');
|
||||
await this.pc.setLocalDescription();
|
||||
_this.assert_equals(this.pc.signalingState, 'stable', 'onmessage not racing with negotiationneeded');
|
||||
_this.assert_equals(this.pc.localDescription.type, 'answer', 'onmessage SLD worked');
|
||||
_this.dispatchEvent(new CustomEvent('sendanswer', { detail: { connectionId: _this.connectionId, sdp: _this.pc.localDescription.sdp } }));
|
||||
|
||||
} else {
|
||||
_this.dispatchEvent(new CustomEvent('ongotanswer', { detail: { connectionId: _this.connectionId } }));
|
||||
|
||||
_this.assert_equals(this.pc.remoteDescription.type, 'answer', 'Answer was set');
|
||||
_this.assert_equals(this.pc.signalingState, 'stable', 'answered');
|
||||
this.pc.dispatchEvent(new Event('negotiated'));
|
||||
}
|
||||
}
|
||||
|
||||
async onGotCandidate(connectionId, candidate) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.pc.addIceCandidate(candidate);
|
||||
} catch (e) {
|
||||
if (this.pc && !this.ignoreOffer)
|
||||
this.warn(`${this.pc} this candidate can't accept current signaling state ${this.pc.signalingState}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
client/src/pointercorrect.js
Normal file
124
client/src/pointercorrect.js
Normal file
@@ -0,0 +1,124 @@
|
||||
export const LetterBoxType = {
|
||||
Vertical: 0,
|
||||
Horizontal: 1
|
||||
};
|
||||
|
||||
export class PointerCorrector {
|
||||
/**
|
||||
* @param {Number} videoWidth
|
||||
* @param {Number} videoHeight
|
||||
* @param {HTMLVideoElement} videoElem
|
||||
*/
|
||||
constructor(videoWidth, videoHeight, videoElem) {
|
||||
this.reset(videoWidth, videoHeight, videoElem);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number[]} position MouseEvent.clientX, MouseEvent.clientY
|
||||
* @returns {Number[]}
|
||||
*/
|
||||
map(position) {
|
||||
var rect = this._videoElem.getBoundingClientRect();
|
||||
const _position = new Array(2);
|
||||
|
||||
// (1) set origin point to zero
|
||||
_position[0] = position[0] - rect.left;
|
||||
_position[1] = position[1] - rect.top;
|
||||
|
||||
// (2) translate Unity coordinate system (reverse y-axis)
|
||||
_position[1] = rect.height - _position[1];
|
||||
|
||||
// (3) add offset of letterbox
|
||||
_position[0] -= this._contentRect.x;
|
||||
_position[1] -= this._contentRect.y;
|
||||
|
||||
// (4) mapping element rectangle to video rectangle
|
||||
_position[0] = _position[0] / this._contentRect.width * this._videoWidth;
|
||||
_position[1] = _position[1] / this._contentRect.height * this._videoHeight;
|
||||
|
||||
return _position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} videoWidth
|
||||
*/
|
||||
setVideoWidth(videoWidth) {
|
||||
this._videoWidth = videoWidth;
|
||||
this._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} videoHeight
|
||||
*/
|
||||
setVideoHeight(videoHeight) {
|
||||
this._videoHeight = videoHeight;
|
||||
this._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLVideoElement} videoElem
|
||||
*/
|
||||
setRect(videoElem) {
|
||||
this._videoElem = videoElem;
|
||||
this._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} videoWidth
|
||||
* @param {Number} videoHeight
|
||||
* @param {HTMLVideoElement} videoElem
|
||||
*/
|
||||
reset(videoWidth, videoHeight, videoElem) {
|
||||
this._videoWidth = videoWidth;
|
||||
this._videoHeight = videoHeight;
|
||||
this._videoElem = videoElem;
|
||||
this._reset();
|
||||
}
|
||||
|
||||
get letterBoxType() {
|
||||
const videoRatio = this._videoHeight / this._videoWidth;
|
||||
var rect = this._videoElem.getBoundingClientRect();
|
||||
const rectRatio = rect.height / rect.width;
|
||||
return videoRatio > rectRatio ? LetterBoxType.Vertical : LetterBoxType.Horizontal;
|
||||
}
|
||||
|
||||
get letterBoxSize() {
|
||||
var rect = this._videoElem.getBoundingClientRect();
|
||||
switch(this.letterBoxType) {
|
||||
case LetterBoxType.Horizontal: {
|
||||
const ratioWidth = rect.width / this._videoWidth;
|
||||
const height = this._videoHeight * ratioWidth;
|
||||
return (rect.height - height) * 0.5;
|
||||
}
|
||||
case LetterBoxType.Vertical: {
|
||||
const ratioHeight = rect.height / this._videoHeight;
|
||||
const width = this._videoWidth * ratioHeight;
|
||||
return (rect.width - width) * 0.5;
|
||||
}
|
||||
}
|
||||
throw 'invalid status';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rectangle for displaying video with the origin at the left-top of the element.
|
||||
* Not considered applying CSS like `object-fit`.
|
||||
* @returns {Object}
|
||||
*/
|
||||
get contentRect() {
|
||||
const letterBoxType = this.letterBoxType;
|
||||
const letterBoxSize = this.letterBoxSize;
|
||||
|
||||
var rect = this._videoElem.getBoundingClientRect();
|
||||
|
||||
const x = letterBoxType == LetterBoxType.Vertical ? letterBoxSize : 0;
|
||||
const y = letterBoxType == LetterBoxType.Horizontal ? letterBoxSize : 0;
|
||||
const width = letterBoxType == LetterBoxType.Vertical ? rect.width - letterBoxSize * 2 : rect.width;
|
||||
const height = letterBoxType == LetterBoxType.Horizontal ? rect.height - letterBoxSize * 2 : rect.height;
|
||||
|
||||
return {x: x, y: y, width: width, height: height};
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._contentRect = this.contentRect;
|
||||
}
|
||||
}
|
||||
317
client/src/renderstreaming.js
Normal file
317
client/src/renderstreaming.js
Normal file
@@ -0,0 +1,317 @@
|
||||
import Peer from "./peer.js";
|
||||
import * as Logger from "./logger.js";
|
||||
|
||||
function uuid4() {
|
||||
var temp_url = URL.createObjectURL(new Blob());
|
||||
var uuid = temp_url.toString();
|
||||
URL.revokeObjectURL(temp_url);
|
||||
return uuid.split(/[:/]/g).pop().toLowerCase();
|
||||
}
|
||||
|
||||
export class RenderStreaming {
|
||||
constructor(signaling, config) {
|
||||
this._peer = null; // participant端:单一peer
|
||||
this._peers = new Map(); // host端:多peer Map (participantId → Peer)
|
||||
this._connectionId = null;
|
||||
this._participantId = null; // 自己的participantId
|
||||
this._isHost = false;
|
||||
this.onConnect = function (connectionId, data) { Logger.log(`Connect peer on ${connectionId}.`); };
|
||||
this.onDisconnect = function (connectionId) { Logger.log(`Disconnect peer on ${connectionId}.`); };
|
||||
this.onGotOffer = function (connectionId) { Logger.log(`On got Offer on ${connectionId}.`); };
|
||||
this.onGotAnswer = function (connectionId) { Logger.log(`On got Answer on ${connectionId}.`); };
|
||||
this.onTrackEvent = function (data) { Logger.log(`OnTrack event peer with data:${data}`); };
|
||||
this.onAddChannel = function (data) { Logger.log(`onAddChannel event peer with data:${data}`); };
|
||||
this.onMessage = function (data) { Logger.log(`On message: ${data}`); };
|
||||
this.onParticipantLeft = function (participantId) { Logger.log(`Participant left: ${participantId}.`); };
|
||||
this.onParticipantJoined = function (participantId) { Logger.log(`Participant joined: ${participantId}.`); };
|
||||
this.onNewPeer = function (participantId) { Logger.log(`New peer created for ${participantId}.`); };
|
||||
this._config = config;
|
||||
this._signaling = signaling;
|
||||
this._signaling.addEventListener('connect', this._onConnect.bind(this));
|
||||
this._signaling.addEventListener('disconnect', this._onDisconnect.bind(this));
|
||||
this._signaling.addEventListener('offer', this._onOffer.bind(this));
|
||||
this._signaling.addEventListener('answer', this._onAnswer.bind(this));
|
||||
this._signaling.addEventListener('candidate', this._onIceCandidate.bind(this));
|
||||
this._signaling.addEventListener('on-message', this._onMessage.bind(this));
|
||||
this._signaling.addEventListener('participant-left', this._onParticipantLeft.bind(this));
|
||||
this._signaling.addEventListener('participant-joined', this._onParticipantJoined.bind(this));
|
||||
}
|
||||
|
||||
async _onConnect(e) {
|
||||
const data = e.detail;
|
||||
if (this._connectionId == data.connectionId) {
|
||||
this._participantId = data.participantId;
|
||||
this._isHost = data.role === 'host';
|
||||
|
||||
if (!this._isHost) {
|
||||
// participant端:立即创建单一peer并开始协商
|
||||
this._preparePeerConnection(this._connectionId, data.polite, null);
|
||||
}
|
||||
// host端:不在connect时创建peer,等participant加入后再创建
|
||||
|
||||
this.onConnect(data.connectionId, data);
|
||||
}
|
||||
}
|
||||
|
||||
async _onDisconnect(e) {
|
||||
const data = e.detail;
|
||||
if (this._connectionId == data.connectionId) {
|
||||
this.onDisconnect(data.connectionId);
|
||||
if (this._peer) {
|
||||
this._peer.close();
|
||||
this._peer = null;
|
||||
}
|
||||
// 关闭所有host端peers
|
||||
this._peers.forEach((peer, participantId) => {
|
||||
peer.close();
|
||||
});
|
||||
this._peers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async _onOffer(e) {
|
||||
const offer = e.detail;
|
||||
const participantId = offer.participantId;
|
||||
|
||||
if (this._isHost) {
|
||||
// host端:为该participant创建或复用peer
|
||||
let peer = this._peers.get(participantId);
|
||||
if (!peer || (peer.pc && peer.pc.iceConnectionState === 'disconnected')) {
|
||||
if (peer) peer.close();
|
||||
peer = this._preparePeerConnection(this._connectionId, offer.polite, participantId);
|
||||
}
|
||||
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
|
||||
try {
|
||||
await peer.onGotDescription(this._connectionId, desc);
|
||||
} catch (error) {
|
||||
Logger.warn(`Error on GotDescription for participant ${participantId}: ${error}`);
|
||||
}
|
||||
} else {
|
||||
// participant端:使用单一peer
|
||||
if (this._peer && this._peer.pc && this._peer.pc.iceConnectionState === 'disconnected') {
|
||||
this._peer.close();
|
||||
this._peer = null;
|
||||
}
|
||||
if (!this._peer) {
|
||||
this._preparePeerConnection(offer.connectionId, offer.polite, null);
|
||||
}
|
||||
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
|
||||
try {
|
||||
await this._peer.onGotDescription(offer.connectionId, desc);
|
||||
} catch (error) {
|
||||
Logger.warn(`Error on GotDescription: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _onAnswer(e) {
|
||||
const answer = e.detail;
|
||||
const participantId = answer.participantId;
|
||||
const desc = new RTCSessionDescription({ sdp: answer.sdp, type: "answer" });
|
||||
|
||||
if (this._isHost && participantId) {
|
||||
// host端:路由到对应participant的peer
|
||||
const peer = this._peers.get(participantId);
|
||||
if (peer) {
|
||||
try {
|
||||
await peer.onGotDescription(this._connectionId, desc);
|
||||
} catch (error) {
|
||||
Logger.warn(`Error on GotDescription answer for ${participantId}: ${error}`);
|
||||
}
|
||||
}
|
||||
} else if (this._peer) {
|
||||
// participant端
|
||||
try {
|
||||
await this._peer.onGotDescription(answer.connectionId, desc);
|
||||
} catch (error) {
|
||||
Logger.warn(`Error on GotDescription answer: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _onIceCandidate(e) {
|
||||
const candidate = e.detail;
|
||||
const participantId = candidate.participantId;
|
||||
const iceCandidate = new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex });
|
||||
|
||||
if (this._isHost && participantId) {
|
||||
// host端:路由到对应participant的peer
|
||||
const peer = this._peers.get(participantId);
|
||||
if (peer) {
|
||||
await peer.onGotCandidate(this._connectionId, iceCandidate);
|
||||
}
|
||||
} else if (this._peer) {
|
||||
// participant端
|
||||
await this._peer.onGotCandidate(candidate.connectionId, iceCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
async _onMessage(e) {
|
||||
const data = e.detail;
|
||||
this.onMessage(data);
|
||||
}
|
||||
|
||||
async _onParticipantLeft(e) {
|
||||
const data = e.detail;
|
||||
const participantId = data.participantId;
|
||||
Logger.log(`Participant left: ${participantId}`);
|
||||
|
||||
// 关闭该participant的peer
|
||||
if (this._peers.has(participantId)) {
|
||||
const peer = this._peers.get(participantId);
|
||||
peer.close();
|
||||
this._peers.delete(participantId);
|
||||
}
|
||||
|
||||
this.onParticipantLeft(participantId);
|
||||
}
|
||||
|
||||
async _onParticipantJoined(e) {
|
||||
const data = e.detail;
|
||||
const participantId = data.participantId;
|
||||
Logger.log(`Participant joined: ${participantId}`);
|
||||
|
||||
// host端:为新participant创建peer
|
||||
if (this._isHost && !this._peers.has(participantId)) {
|
||||
this._preparePeerConnection(this._connectionId, false, participantId);
|
||||
}
|
||||
|
||||
this.onParticipantJoined(participantId);
|
||||
}
|
||||
|
||||
async createConnection(connectionId) {
|
||||
this._connectionId = connectionId ? connectionId : uuid4();
|
||||
await this._signaling.createConnection(this._connectionId);
|
||||
}
|
||||
|
||||
async deleteConnection() {
|
||||
await this._signaling.deleteConnection(this._connectionId);
|
||||
}
|
||||
|
||||
_preparePeerConnection(connectionId, polite, participantId) {
|
||||
// host端多peer模式:participantId标识目标participant
|
||||
// participant端单peer模式:participantId为null
|
||||
|
||||
const peer = new Peer(connectionId, polite, this._config);
|
||||
|
||||
// 保存peer
|
||||
if (participantId) {
|
||||
if (this._peers.has(participantId)) {
|
||||
const oldPeer = this._peers.get(participantId);
|
||||
oldPeer.close();
|
||||
}
|
||||
this._peers.set(participantId, peer);
|
||||
} else {
|
||||
if (this._peer) {
|
||||
this._peer.close();
|
||||
}
|
||||
this._peer = peer;
|
||||
}
|
||||
|
||||
// 事件处理:附加participantId用于路由
|
||||
peer.addEventListener('trackevent', (e) => {
|
||||
const data = e.detail;
|
||||
data.participantId = participantId;
|
||||
this.onTrackEvent(data);
|
||||
});
|
||||
|
||||
peer.addEventListener('adddatachannel', (e) => {
|
||||
const data = e.detail;
|
||||
this.onAddChannel(data);
|
||||
});
|
||||
|
||||
peer.addEventListener('ongotoffer', (e) => {
|
||||
const id = e.detail.connectionId;
|
||||
this.onGotOffer(id);
|
||||
});
|
||||
|
||||
peer.addEventListener('ongotanswer', (e) => {
|
||||
const id = e.detail.connectionId;
|
||||
this.onGotAnswer(id);
|
||||
});
|
||||
|
||||
peer.addEventListener('sendoffer', (e) => {
|
||||
const offer = e.detail;
|
||||
this._signaling.sendOffer(offer.connectionId, offer.sdp, participantId);
|
||||
});
|
||||
|
||||
peer.addEventListener('sendanswer', (e) => {
|
||||
const answer = e.detail;
|
||||
this._signaling.sendAnswer(answer.connectionId, answer.sdp, participantId);
|
||||
});
|
||||
|
||||
peer.addEventListener('sendcandidate', (e) => {
|
||||
const candidate = e.detail;
|
||||
this._signaling.sendCandidate(candidate.connectionId, candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex, participantId);
|
||||
});
|
||||
|
||||
this.onNewPeer(participantId || connectionId);
|
||||
return peer;
|
||||
}
|
||||
|
||||
async getStats(participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? await peer.getStats(this._connectionId) : null;
|
||||
}
|
||||
return this._peer ? await this._peer.getStats(this._connectionId) : null;
|
||||
}
|
||||
|
||||
createDataChannel(label, participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? peer.createDataChannel(this._connectionId, label) : null;
|
||||
}
|
||||
return this._peer ? this._peer.createDataChannel(this._connectionId, label) : null;
|
||||
}
|
||||
|
||||
addTrack(track, participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? peer.addTrack(this._connectionId, track) : null;
|
||||
}
|
||||
return this._peer ? this._peer.addTrack(this._connectionId, track) : null;
|
||||
}
|
||||
|
||||
addTransceiver(trackOrKind, init, participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? peer.addTransceiver(this._connectionId, trackOrKind, init) : null;
|
||||
}
|
||||
return this._peer ? this._peer.addTransceiver(this._connectionId, trackOrKind, init) : null;
|
||||
}
|
||||
|
||||
getTransceivers(participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? peer.getTransceivers(this._connectionId) : null;
|
||||
}
|
||||
return this._peer ? this._peer.getTransceivers(this._connectionId) : null;
|
||||
}
|
||||
|
||||
sendMessage(message) {
|
||||
if (this._signaling && this._connectionId) {
|
||||
this._signaling.sendMessage(this._connectionId, message);
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this._signaling.start();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this._peer) {
|
||||
this._peer.close();
|
||||
this._peer = null;
|
||||
}
|
||||
this._peers.forEach((peer) => {
|
||||
peer.close();
|
||||
});
|
||||
this._peers.clear();
|
||||
|
||||
if (this._signaling) {
|
||||
await this._signaling.stop();
|
||||
this._signaling = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
208
client/src/sender.js
Normal file
208
client/src/sender.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import {
|
||||
Mouse,
|
||||
Keyboard,
|
||||
Gamepad,
|
||||
Touchscreen,
|
||||
StateEvent,
|
||||
TextEvent
|
||||
} from "./inputdevice.js";
|
||||
|
||||
import { LocalInputManager } from "./inputremoting.js";
|
||||
import { GamepadHandler } from "./gamepadhandler.js";
|
||||
import { PointerCorrector } from "./pointercorrect.js";
|
||||
|
||||
export class Sender extends LocalInputManager {
|
||||
constructor(elem) {
|
||||
super();
|
||||
this._devices = [];
|
||||
this._elem = elem;
|
||||
this._corrector = new PointerCorrector(
|
||||
this._elem.videoWidth,
|
||||
this._elem.videoHeight,
|
||||
this._elem
|
||||
);
|
||||
|
||||
//since line 27 cannot complete resize initialization but can only monitor div dimension changes, line 26 needs to be reserved
|
||||
this._elem.addEventListener('resize', this._onResizeEvent.bind(this), false);
|
||||
const observer = new ResizeObserver(this._onResizeEvent.bind(this));
|
||||
observer.observe(this._elem);
|
||||
}
|
||||
|
||||
addMouse() {
|
||||
const descriptionMouse = {
|
||||
m_InterfaceName: "RawInput",
|
||||
m_DeviceClass: "Mouse",
|
||||
m_Manufacturer: "",
|
||||
m_Product: "",
|
||||
m_Serial: "",
|
||||
m_Version: "",
|
||||
m_Capabilities: ""
|
||||
};
|
||||
this.mouse = new Mouse("Mouse", "Mouse", 1, null, descriptionMouse);
|
||||
this._devices.push(this.mouse);
|
||||
|
||||
this._elem.addEventListener('click', this._onMouseEvent.bind(this), false);
|
||||
this._elem.addEventListener('mousedown', this._onMouseEvent.bind(this), false);
|
||||
this._elem.addEventListener('mouseup', this._onMouseEvent.bind(this), false);
|
||||
this._elem.addEventListener('mousemove', this._onMouseEvent.bind(this), false);
|
||||
this._elem.addEventListener('wheel', this._onWheelEvent.bind(this), false);
|
||||
}
|
||||
|
||||
addKeyboard() {
|
||||
const descriptionKeyboard = {
|
||||
m_InterfaceName: "RawInput",
|
||||
m_DeviceClass: "Keyboard",
|
||||
m_Manufacturer: "",
|
||||
m_Product: "",
|
||||
m_Serial: "",
|
||||
m_Version: "",
|
||||
m_Capabilities: ""
|
||||
};
|
||||
this.keyboard = new Keyboard("Keyboard", "Keyboard", 2, null, descriptionKeyboard);
|
||||
this._devices.push(this.keyboard);
|
||||
|
||||
document.addEventListener('keyup', this._onKeyEvent.bind(this), false);
|
||||
document.addEventListener('keydown', this._onKeyEvent.bind(this), false);
|
||||
}
|
||||
|
||||
addGamepad() {
|
||||
const descriptionGamepad = {
|
||||
m_InterfaceName: "RawInput",
|
||||
m_DeviceClass: "Gamepad",
|
||||
m_Manufacturer: "",
|
||||
m_Product: "",
|
||||
m_Serial: "",
|
||||
m_Version: "",
|
||||
m_Capabilities: ""
|
||||
};
|
||||
this.gamepad = new Gamepad("Gamepad", "Gamepad", 3, null, descriptionGamepad);
|
||||
this._devices.push(this.gamepad);
|
||||
|
||||
window.addEventListener("gamepadconnected", this._onGamepadEvent.bind(this), false);
|
||||
window.addEventListener("gamepaddisconnected", this._onGamepadEvent.bind(this), false);
|
||||
this._gamepadHandler = new GamepadHandler();
|
||||
this._gamepadHandler.addEventListener("gamepadupdated", this._onGamepadEvent.bind(this), false);
|
||||
}
|
||||
|
||||
addTouchscreen() {
|
||||
const descriptionTouch = {
|
||||
m_InterfaceName: "RawInput",
|
||||
m_DeviceClass: "Touch",
|
||||
m_Manufacturer: "",
|
||||
m_Product: "",
|
||||
m_Serial: "",
|
||||
m_Version: "",
|
||||
m_Capabilities: ""
|
||||
};
|
||||
this.touchscreen = new Touchscreen("Touchscreen", "Touchscreen", 4, null, descriptionTouch);
|
||||
this._devices.push(this.touchscreen);
|
||||
|
||||
this._elem.addEventListener('touchend', this._onTouchEvent.bind(this), false);
|
||||
this._elem.addEventListener('touchstart', this._onTouchEvent.bind(this), false);
|
||||
this._elem.addEventListener('touchcancel', this._onTouchEvent.bind(this), false);
|
||||
this._elem.addEventListener('touchmove', this._onTouchEvent.bind(this), false);
|
||||
this._elem.addEventListener('click', this._onTouchEvent.bind(this), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {InputDevice[]}
|
||||
*/
|
||||
get devices() {
|
||||
return this._devices;
|
||||
}
|
||||
|
||||
_onResizeEvent() {
|
||||
this._corrector.reset(
|
||||
this._elem.videoWidth,
|
||||
this._elem.videoHeight,
|
||||
this._elem
|
||||
);
|
||||
}
|
||||
_onMouseEvent(event) {
|
||||
this.mouse.queueEvent(event);
|
||||
this.mouse.currentState.position = this._corrector.map(this.mouse.currentState.position);
|
||||
this._queueStateEvent(this.mouse.currentState, this.mouse);
|
||||
}
|
||||
_onWheelEvent(event) {
|
||||
this.mouse.queueEvent(event);
|
||||
this._queueStateEvent(this.mouse.currentState, this.mouse);
|
||||
}
|
||||
_onKeyEvent(event) {
|
||||
if(event.type == 'keydown') {
|
||||
if(!event.repeat) { // StateEvent
|
||||
this.keyboard.queueEvent(event);
|
||||
this._queueStateEvent(this.keyboard.currentState, this.keyboard);
|
||||
}
|
||||
// TextEvent
|
||||
this._queueTextEvent(this.keyboard, event);
|
||||
}
|
||||
else if(event.type == 'keyup') {
|
||||
this.keyboard.queueEvent(event);
|
||||
this._queueStateEvent(this.keyboard.currentState, this.keyboard);
|
||||
}
|
||||
}
|
||||
_onTouchEvent(event) {
|
||||
this.touchscreen.queueEvent(event, this.timeSinceStartup);
|
||||
for(let touch of this.touchscreen.currentState.touchData) {
|
||||
let clone = touch.copy();
|
||||
clone.position = this._corrector.map(clone.position);
|
||||
this._queueStateEvent(clone, this.touchscreen);
|
||||
}
|
||||
}
|
||||
_onGamepadEvent(event) {
|
||||
switch(event.type) {
|
||||
case 'gamepadconnected': {
|
||||
this._gamepadHandler.addGamepad(event.gamepad);
|
||||
break;
|
||||
}
|
||||
case 'gamepaddisconnected': {
|
||||
this._gamepadHandler.removeGamepad(event.gamepad);
|
||||
break;
|
||||
}
|
||||
case 'gamepadupdated': {
|
||||
this.gamepad.queueEvent(event);
|
||||
this._queueStateEvent(this.gamepad.currentState, this.gamepad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_queueStateEvent(state, device) {
|
||||
const stateEvent =
|
||||
StateEvent.fromState(state, device.deviceId, this.timeSinceStartup);
|
||||
const e = new CustomEvent(
|
||||
'event', {detail: { event: stateEvent, device: device}});
|
||||
super.onEvent.dispatchEvent(e);
|
||||
}
|
||||
_queueTextEvent(device, event) {
|
||||
const textEvent = TextEvent.create(device.deviceId, event, this.timeSinceStartup);
|
||||
const e = new CustomEvent(
|
||||
'event', {detail: { event: textEvent, device: device}});
|
||||
super.onEvent.dispatchEvent(e);
|
||||
}
|
||||
_queueDeviceChange(device, usage) {
|
||||
const e = new CustomEvent(
|
||||
'changedeviceusage', {detail: { device: device, usage: usage }});
|
||||
super.onEvent.dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
export class Observer {
|
||||
/**
|
||||
*
|
||||
* @param {RTCDataChannel} channel
|
||||
*/
|
||||
constructor(channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Message} message
|
||||
*/
|
||||
onNext(message) {
|
||||
if(this.channel == null || this.channel.readyState != 'open') {
|
||||
return;
|
||||
}
|
||||
this.channel.send(message.buffer);
|
||||
}
|
||||
}
|
||||
276
client/src/signaling.js
Normal file
276
client/src/signaling.js
Normal file
@@ -0,0 +1,276 @@
|
||||
import * as Logger from "./logger.js";
|
||||
|
||||
export class Signaling extends EventTarget {
|
||||
|
||||
constructor(interval = 1000) {
|
||||
super();
|
||||
this.running = false;
|
||||
this.interval = interval;
|
||||
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
|
||||
}
|
||||
|
||||
headers() {
|
||||
if (this.sessionId !== undefined) {
|
||||
return { 'Content-Type': 'application/json', 'Session-Id': this.sessionId };
|
||||
}
|
||||
else {
|
||||
return { 'Content-Type': 'application/json' };
|
||||
}
|
||||
}
|
||||
|
||||
url(method, parameter = '') {
|
||||
let ret = location.origin + '/signaling';
|
||||
if (method)
|
||||
ret += '/' + method;
|
||||
if (parameter)
|
||||
ret += '?' + parameter;
|
||||
return ret;
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.running = true;
|
||||
while (!this.sessionId) {
|
||||
const createResponse = await fetch(this.url(''), { method: 'PUT', headers: this.headers() });
|
||||
const session = await createResponse.json();
|
||||
this.sessionId = session.sessionId;
|
||||
|
||||
if (!this.sessionId) {
|
||||
await this.sleep(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
this.loopGetAll();
|
||||
}
|
||||
|
||||
async loopGetAll() {
|
||||
let lastTimeRequest = Date.now() - 30000;
|
||||
while (this.running) {
|
||||
const res = await this.getAll(lastTimeRequest);
|
||||
const data = await res.json();
|
||||
lastTimeRequest = data.datetime ? data.datetime : Date.now();
|
||||
|
||||
const messages = data.messages;
|
||||
|
||||
for (const msg of messages) {
|
||||
switch (msg.type) {
|
||||
case "connect":
|
||||
break;
|
||||
case "disconnect":
|
||||
this.dispatchEvent(new CustomEvent('disconnect', { detail: msg }));
|
||||
break;
|
||||
case "offer":
|
||||
this.dispatchEvent(new CustomEvent('offer', { detail: msg }));
|
||||
break;
|
||||
case "answer":
|
||||
this.dispatchEvent(new CustomEvent('answer', { detail: msg }));
|
||||
break;
|
||||
case "candidate":
|
||||
this.dispatchEvent(new CustomEvent('candidate', { detail: msg }));
|
||||
break;
|
||||
case "on-message":
|
||||
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.data }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this.sleep(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.running = false;
|
||||
await fetch(this.url(''), { method: 'DELETE', headers: this.headers() });
|
||||
this.sessionId = null;
|
||||
}
|
||||
|
||||
async createConnection(connectionId) {
|
||||
const data = { 'connectionId': connectionId };
|
||||
const res = await fetch(this.url('connection'), { method: 'PUT', headers: this.headers(), body: JSON.stringify(data) });
|
||||
const json = await res.json();
|
||||
Logger.log(`Signaling: HTTP create connection, connectionId: ${json.connectionId}, polite:${json.polite}`);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('connect', { detail: json }));
|
||||
return json;
|
||||
}
|
||||
|
||||
async deleteConnection(connectionId) {
|
||||
const data = { 'connectionId': connectionId };
|
||||
const res = await fetch(this.url('connection'), { method: 'DELETE', headers: this.headers(), body: JSON.stringify(data) });
|
||||
const json = await res.json();
|
||||
this.dispatchEvent(new CustomEvent('disconnect', { detail: json }));
|
||||
return json;
|
||||
}
|
||||
|
||||
async sendOffer(connectionId, sdp) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
Logger.log('sendOffer:' + data);
|
||||
await fetch(this.url('offer'), { method: 'POST', headers: this.headers(), body: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
async sendAnswer(connectionId, sdp) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
Logger.log('sendAnswer:' + data);
|
||||
await fetch(this.url('answer'), { method: 'POST', headers: this.headers(), body: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
async sendCandidate(connectionId, candidate, sdpMid, sdpMLineIndex) {
|
||||
const data = {
|
||||
'candidate': candidate,
|
||||
'sdpMLineIndex': sdpMLineIndex,
|
||||
'sdpMid': sdpMid,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
Logger.log('sendCandidate:' + data);
|
||||
await fetch(this.url('candidate'), { method: 'POST', headers: this.headers(), body: JSON.stringify(data) });
|
||||
}
|
||||
// 在 Signaling 类中添加
|
||||
async sendMessage(connectionId, message) {
|
||||
const data = {
|
||||
'message': message,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
await fetch(this.url('on-message'), { method: 'POST', headers: this.headers(), body: JSON.stringify(data) });
|
||||
}
|
||||
async getAll(fromTime = 0) {
|
||||
return await fetch(this.url(``, `fromtime=${fromTime}`), { method: 'GET', headers: this.headers() });
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSocketSignaling extends EventTarget {
|
||||
|
||||
constructor(interval = 1000) {
|
||||
super();
|
||||
this.interval = interval;
|
||||
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
|
||||
|
||||
let websocketUrl;
|
||||
if (location.protocol === "https:") {
|
||||
websocketUrl = "wss://" + location.host;
|
||||
} else {
|
||||
websocketUrl = "ws://" + location.host;
|
||||
}
|
||||
|
||||
this.websocket = new WebSocket(websocketUrl);
|
||||
this.connectionId = null;
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
this.isWsOpen = true;
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
this.isWsOpen = false;
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (!msg || !this) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.log(msg);
|
||||
|
||||
switch (msg.type) {
|
||||
case "connect":
|
||||
this.dispatchEvent(new CustomEvent('connect', { detail: msg }));
|
||||
break;
|
||||
case "disconnect":
|
||||
this.dispatchEvent(new CustomEvent('disconnect', { detail: msg }));
|
||||
break;
|
||||
case "offer":
|
||||
this.dispatchEvent(new CustomEvent('offer', { detail: { connectionId: msg.from, sdp: msg.data.sdp, polite: msg.data.polite, participantId: msg.participantId } }));
|
||||
break;
|
||||
case "answer":
|
||||
this.dispatchEvent(new CustomEvent('answer', { detail: { connectionId: msg.from, sdp: msg.data.sdp, participantId: msg.participantId } }));
|
||||
break;
|
||||
case "candidate":
|
||||
this.dispatchEvent(new CustomEvent('candidate', { detail: { connectionId: msg.from, candidate: msg.data.candidate, sdpMLineIndex: msg.data.sdpMLineIndex, sdpMid: msg.data.sdpMid, participantId: msg.participantId } }));
|
||||
break;
|
||||
case "on-message":
|
||||
// 将participantId附加到消息数据中,以便Host识别消息发送者
|
||||
if (msg.participantId) {
|
||||
msg.data.participantId = msg.participantId;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.data }));
|
||||
break;
|
||||
case "participant-left":
|
||||
this.dispatchEvent(new CustomEvent('participant-left', { detail: msg }));
|
||||
break;
|
||||
case "participant-joined":
|
||||
this.dispatchEvent(new CustomEvent('participant-joined', { detail: msg }));
|
||||
break;
|
||||
case "broadcast":
|
||||
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.message }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async start() {
|
||||
while (!this.isWsOpen) {
|
||||
await this.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.websocket.close();
|
||||
while (this.isWsOpen) {
|
||||
await this.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
createConnection(connectionId) {
|
||||
const sendJson = JSON.stringify({ type: "connect", connectionId: connectionId });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
deleteConnection(connectionId) {
|
||||
const sendJson = JSON.stringify({ type: "disconnect", connectionId: connectionId });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendOffer(connectionId, sdp, participantId) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
const sendJson = JSON.stringify({ type: "offer", from: connectionId, data: data, participantId: participantId || '' });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendAnswer(connectionId, sdp, participantId) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
const sendJson = JSON.stringify({ type: "answer", from: connectionId, data: data, participantId: participantId || '' });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendCandidate(connectionId, candidate, sdpMLineIndex, sdpMid, participantId) {
|
||||
const data = {
|
||||
'candidate': candidate,
|
||||
'sdpMLineIndex': sdpMLineIndex,
|
||||
'sdpMid': sdpMid,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
const sendJson = JSON.stringify({ type: "candidate", from: connectionId, data: data, participantId: participantId || '' });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
// 在 WebSocketSignaling 类中添加
|
||||
sendMessage(connectionId, message) {
|
||||
const data = {
|
||||
'message': message,
|
||||
'senderId': message.senderId,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
const sendJson = JSON.stringify({ type: "on-message", data: data });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
}
|
||||
7
client/src/touchflags.js
Normal file
7
client/src/touchflags.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const TouchFlags =
|
||||
{
|
||||
IndirectTouch: 1 << 0,
|
||||
PrimaryTouch: 1 << 4,
|
||||
Tap: 1 << 5,
|
||||
OrphanedPrimaryTouch: 1 << 6,
|
||||
};
|
||||
8
client/src/touchphase.js
Normal file
8
client/src/touchphase.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export const TouchPhase = {
|
||||
None: 0,
|
||||
Began: 1,
|
||||
Moved: 2,
|
||||
Ended: 3,
|
||||
Canceled: 4,
|
||||
Stationary: 5
|
||||
};
|
||||
Reference in New Issue
Block a user