mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-10 07:45:36 +02:00
Compare commits
8 Commits
6dd1f124c2
...
fa9c53eb35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa9c53eb35 | ||
|
|
d87a0b35dd | ||
|
|
695ccc4170 | ||
|
|
8d5895fc5e | ||
|
|
1d88a03b93 | ||
|
|
a54cf759fa | ||
|
|
67f9a7d0a0 | ||
|
|
32341574dc |
2
.github/workflows/runner.yml
vendored
2
.github/workflows/runner.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
variant:
|
||||
- { suffix: "", base: "docker.io/cachyos/cachyos:latest" }
|
||||
- { suffix: "-v3", base: "docker.io/cachyos/cachyos-v3:latest" }
|
||||
- { suffix: "-v4", base: "docker.io/cachyos/cachyos-v4:latest" }
|
||||
#- { suffix: "-v4", base: "docker.io/cachyos/cachyos-v4:latest" } # Disabled until GHA has this
|
||||
steps:
|
||||
-
|
||||
name: Checkout repo
|
||||
|
||||
@@ -41,7 +41,7 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm lib32-gcc-libs
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 --rev "9e8bfd0217eeab011c5afc368d3ea67a4c239e81" https://github.com/DatCaptainHorse/vimputti.git
|
||||
RUN git clone --depth 1 --rev "2fde5376b6b9a38cdbd94ccc6a80c9d29a81a417" https://github.com/DatCaptainHorse/vimputti.git
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM vimputti-manager-deps AS vimputti-manager-planner
|
||||
@@ -129,23 +129,8 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo install cargo-c
|
||||
|
||||
# Grab cudart from NVIDIA..
|
||||
RUN wget https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-13.0.96-archive.tar.xz -O cuda_cudart.tar.xz && \
|
||||
mkdir cuda_cudart && tar -xf cuda_cudart.tar.xz -C cuda_cudart --strip-components=1 && \
|
||||
cp cuda_cudart/lib/libcudart.so cuda_cudart/lib/libcudart.so.* /usr/lib/ && \
|
||||
rm -r cuda_cudart && \
|
||||
rm cuda_cudart.tar.xz
|
||||
|
||||
# Grab cuda lib from NVIDIA (it's in driver package of all things..)
|
||||
RUN wget https://developer.download.nvidia.com/compute/cuda/redist/nvidia_driver/linux-x86_64/nvidia_driver-linux-x86_64-580.95.05-archive.tar.xz -O nvidia_driver.tar.xz && \
|
||||
mkdir nvidia_driver && tar -xf nvidia_driver.tar.xz -C nvidia_driver --strip-components=1 && \
|
||||
cp nvidia_driver/lib/libcuda.so.* /usr/lib/libcuda.so && \
|
||||
ln -s /usr/lib/libcuda.so /usr/lib/libcuda.so.1 && \
|
||||
rm -r nvidia_driver && \
|
||||
rm nvidia_driver.tar.xz
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 --rev "afa853fa03e8403c83bbb3bc0cf39147ad46c266" https://github.com/games-on-whales/gst-wayland-display.git
|
||||
RUN git clone --depth 1 --rev "a4abcfe2cffe2d33b564d1308b58504a5e3012b1" https://github.com/games-on-whales/gst-wayland-display.git
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM gst-wayland-deps AS gst-wayland-planner
|
||||
@@ -214,5 +199,4 @@ COPY --from=gst-wayland-cached-builder /artifacts/include/ /artifacts/include/
|
||||
COPY --from=vimputti-manager-cached-builder /artifacts/vimputti-manager /artifacts/bin/
|
||||
COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_64.so /artifacts/lib64/libvimputti_shim.so
|
||||
COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_32.so /artifacts/lib32/libvimputti_shim.so
|
||||
COPY --from=gst-wayland-deps /usr/lib/libcuda.so /usr/lib/libcuda.so.* /artifacts/lib/
|
||||
COPY --from=bubblewrap-builder /artifacts/bin/bwrap /artifacts/bin/
|
||||
|
||||
@@ -72,6 +72,11 @@ RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config"
|
||||
|
||||
COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/"
|
||||
|
||||
## MangoHud Config ##
|
||||
RUN mkdir -p "${NESTRI_HOME}/.config/MangoHud"
|
||||
|
||||
COPY packages/configs/MangoHud/MangoHud.conf "${NESTRI_HOME}/.config/MangoHud/"
|
||||
|
||||
### Artifacts from Builder ###
|
||||
COPY --from=builder /artifacts/bin/nestri-server /usr/bin/
|
||||
COPY --from=builder /artifacts/bin/bwrap /usr/bin/
|
||||
|
||||
48
packages/configs/MangoHud/MangoHud.conf
Normal file
48
packages/configs/MangoHud/MangoHud.conf
Normal file
@@ -0,0 +1,48 @@
|
||||
legacy_layout=false
|
||||
|
||||
# common
|
||||
horizontal
|
||||
horizontal_stretch
|
||||
hud_no_margin
|
||||
no_small_font
|
||||
background_alpha=0.66
|
||||
round_corners=0
|
||||
background_color=000000
|
||||
font_size=24
|
||||
position=top-left
|
||||
engine_short_names
|
||||
|
||||
# colors
|
||||
text_color=DFDFDF
|
||||
gpu_color=FF4E00
|
||||
cpu_color=00AA00
|
||||
engine_color=00AA00
|
||||
vram_color=00AA00
|
||||
ram_color=00AA00
|
||||
frametime_color=FF4E00
|
||||
|
||||
# load colors
|
||||
cpu_load_color=DFDFDF,DF964D,DF3D3D
|
||||
gpu_load_color=DFDFDF,DF964D,DF3D3D
|
||||
|
||||
# GPU and VRAM
|
||||
gpu_text=NESTRI
|
||||
gpu_stats
|
||||
gpu_load_change
|
||||
gpu_load_value=70,90
|
||||
|
||||
vram
|
||||
|
||||
# CPU and RAM
|
||||
cpu_text=CPU
|
||||
cpu_stats
|
||||
cpu_load_change
|
||||
cpu_load_value=70,90
|
||||
|
||||
ram
|
||||
|
||||
# FPS and timing
|
||||
fps
|
||||
fps_metrics=0.01
|
||||
|
||||
frame_timing
|
||||
@@ -7,24 +7,22 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/buf": "^1.57.2",
|
||||
"@bufbuild/protoc-gen-es": "^2.9.0"
|
||||
"@bufbuild/buf": "^1.59.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.9.0",
|
||||
"@chainsafe/libp2p-noise": "^16.1.4",
|
||||
"@bufbuild/protobuf": "^2.10.0",
|
||||
"@chainsafe/libp2p-noise": "^17.0.0",
|
||||
"@chainsafe/libp2p-quic": "^1.1.3",
|
||||
"@chainsafe/libp2p-yamux": "^7.0.4",
|
||||
"@libp2p/identify": "^3.0.39",
|
||||
"@libp2p/interface": "^2.11.0",
|
||||
"@libp2p/ping": "^2.0.37",
|
||||
"@libp2p/websockets": "^9.2.19",
|
||||
"@libp2p/webtransport": "^5.0.51",
|
||||
"@multiformats/multiaddr": "^12.5.1",
|
||||
"it-length-prefixed": "^10.0.1",
|
||||
"it-pipe": "^3.0.1",
|
||||
"libp2p": "^2.10.0",
|
||||
"uint8arraylist": "^2.4.8",
|
||||
"uint8arrays": "^5.1.0"
|
||||
"@chainsafe/libp2p-yamux": "^8.0.1",
|
||||
"@libp2p/identify": "^4.0.5",
|
||||
"@libp2p/interface": "^3.0.2",
|
||||
"@libp2p/ping": "^3.0.5",
|
||||
"@libp2p/websockets": "^10.0.6",
|
||||
"@libp2p/webtransport": "^6.0.7",
|
||||
"@libp2p/utils": "^7.0.5",
|
||||
"@multiformats/multiaddr": "^13.0.1",
|
||||
"libp2p": "^3.0.6",
|
||||
"uint8arraylist": "^2.4.8"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
import { controllerButtonToLinuxEventCode } from "./codes";
|
||||
import { WebRTCStream } from "./webrtc-stream";
|
||||
import {
|
||||
ProtoMessageBase,
|
||||
ProtoMessageInput,
|
||||
ProtoMessageInputSchema,
|
||||
} from "./proto/messages_pb";
|
||||
import {
|
||||
ProtoInputSchema,
|
||||
ProtoControllerAttachSchema,
|
||||
ProtoControllerDetachSchema,
|
||||
ProtoControllerButtonSchema,
|
||||
ProtoControllerTriggerSchema,
|
||||
ProtoControllerAxisSchema,
|
||||
ProtoControllerStickSchema,
|
||||
ProtoControllerStateBatchSchema,
|
||||
ProtoControllerStateBatch,
|
||||
ProtoControllerRumble,
|
||||
} from "./proto/types_pb";
|
||||
import { create, toBinary, fromBinary } from "@bufbuild/protobuf";
|
||||
import { createMessage } from "./utils";
|
||||
import { ProtoMessageSchema } from "./proto/messages_pb";
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCStream;
|
||||
@@ -23,6 +17,7 @@ interface Props {
|
||||
}
|
||||
|
||||
interface GamepadState {
|
||||
previousButtonState: Map<number, boolean>;
|
||||
buttonState: Map<number, boolean>;
|
||||
leftTrigger: number;
|
||||
rightTrigger: number;
|
||||
@@ -34,12 +29,17 @@ interface GamepadState {
|
||||
dpadY: number;
|
||||
}
|
||||
|
||||
enum PollState {
|
||||
IDLE,
|
||||
RUNNING,
|
||||
}
|
||||
|
||||
export class Controller {
|
||||
protected wrtc: WebRTCStream;
|
||||
protected slot: number;
|
||||
protected connected: boolean = false;
|
||||
protected gamepad: Gamepad | null = null;
|
||||
protected lastState: GamepadState = {
|
||||
protected state: GamepadState = {
|
||||
previousButtonState: new Map<number, boolean>(),
|
||||
buttonState: new Map<number, boolean>(),
|
||||
leftTrigger: 0,
|
||||
rightTrigger: 0,
|
||||
@@ -53,17 +53,33 @@ export class Controller {
|
||||
// TODO: As user configurable, set quite low now for decent controllers (not Nintendo ones :P)
|
||||
protected stickDeadzone: number = 2048; // 2048 / 32768 = ~0.06 (6% of stick range)
|
||||
|
||||
private updateInterval = 10.0; // 100 updates per second
|
||||
private _dcRumbleHandler: ((data: ArrayBuffer) => void) | null = null;
|
||||
// Polling configuration
|
||||
private readonly FULL_RATE_MS = 10; // 100 UPS
|
||||
private readonly IDLE_THRESHOLD = 100; // ms before considering idle/hands off controller
|
||||
private readonly FULL_INTERVAL = 250; // ms before sending full state occassionally, to verify inputs are synced
|
||||
|
||||
// Polling state
|
||||
private pollingState: PollState = PollState.IDLE;
|
||||
private lastInputTime: number = Date.now();
|
||||
private lastFullTime: number = Date.now();
|
||||
private pollInterval: any = null;
|
||||
|
||||
// Controller batch vars
|
||||
private sequence: number = 0;
|
||||
private readonly CHANGED_BUTTONS_STATE = 1 << 0;
|
||||
private readonly CHANGED_LEFT_STICK_X = 1 << 1;
|
||||
private readonly CHANGED_LEFT_STICK_Y = 1 << 2;
|
||||
private readonly CHANGED_RIGHT_STICK_X = 1 << 3;
|
||||
private readonly CHANGED_RIGHT_STICK_Y = 1 << 4;
|
||||
private readonly CHANGED_LEFT_TRIGGER = 1 << 5;
|
||||
private readonly CHANGED_RIGHT_TRIGGER = 1 << 6;
|
||||
private readonly CHANGED_DPAD_X = 1 << 7;
|
||||
private readonly CHANGED_DPAD_Y = 1 << 8;
|
||||
|
||||
private _dcHandler: ((data: ArrayBuffer) => void) | null = null;
|
||||
|
||||
constructor({ webrtc, e }: Props) {
|
||||
this.wrtc = webrtc;
|
||||
this.slot = e.gamepad.index;
|
||||
|
||||
this.updateInterval = 1000 / webrtc.currentFrameRate;
|
||||
|
||||
// Gamepad connected
|
||||
this.gamepad = e.gamepad;
|
||||
|
||||
// Get vendor of gamepad from id string (i.e. "... Vendor: 054c Product: 09cc")
|
||||
const vendorMatch = e.gamepad.id.match(/Vendor:\s?([0-9a-fA-F]{4})/);
|
||||
@@ -72,34 +88,49 @@ export class Controller {
|
||||
const productMatch = e.gamepad.id.match(/Product:\s?([0-9a-fA-F]{4})/);
|
||||
const productId = productMatch ? productMatch[1].toLowerCase() : "unknown";
|
||||
|
||||
const attachMsg = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerAttach",
|
||||
value: create(ProtoControllerAttachSchema, {
|
||||
type: "ControllerAttach",
|
||||
id: this.vendor_id_to_controller(vendorId, productId),
|
||||
slot: this.slot,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: attachMsg,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
// Listen to datachannel events from server
|
||||
this._dcHandler = (data: ArrayBuffer) => {
|
||||
if (!this.connected) return;
|
||||
try {
|
||||
// First decode the wrapper message
|
||||
const uint8Data = new Uint8Array(data);
|
||||
const messageWrapper = fromBinary(ProtoMessageSchema, uint8Data);
|
||||
|
||||
// Listen to feedback rumble events from server
|
||||
this._dcRumbleHandler = (data: any) => this.rumbleCallback(data as ArrayBuffer);
|
||||
this.wrtc.addDataChannelCallback(this._dcRumbleHandler);
|
||||
if (messageWrapper.payload.case === "controllerRumble") {
|
||||
this.rumbleCallback(messageWrapper.payload.value);
|
||||
} else if (messageWrapper.payload.case === "controllerAttach") {
|
||||
if (this.gamepad) return; // already attached
|
||||
const attachMsg = messageWrapper.payload.value;
|
||||
// Gamepad connected succesfully
|
||||
this.gamepad = e.gamepad;
|
||||
console.log(
|
||||
`Gamepad connected: ${e.gamepad.id}, local slot ${e.gamepad.index}, msg: ${attachMsg.sessionSlot}`,
|
||||
);
|
||||
this.run();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error decoding datachannel message:", err);
|
||||
}
|
||||
};
|
||||
this.wrtc.addDataChannelCallback(this._dcHandler);
|
||||
|
||||
const attachMsg = createMessage(
|
||||
create(ProtoControllerAttachSchema, {
|
||||
id: this.vendor_id_to_controller(vendorId, productId),
|
||||
sessionSlot: e.gamepad.index,
|
||||
sessionId: this.wrtc.getSessionID(),
|
||||
}),
|
||||
"controllerInput",
|
||||
);
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, attachMsg));
|
||||
|
||||
this.run();
|
||||
}
|
||||
|
||||
public getSlot(): number {
|
||||
return this.gamepad.index;
|
||||
}
|
||||
|
||||
// Maps vendor id and product id to supported controller type
|
||||
// Currently supported: Sony (ps4, ps5), Microsoft (xbox360, xboxone), Nintendo (switchpro)
|
||||
// Default fallback to xbox360
|
||||
@@ -149,361 +180,352 @@ export class Controller {
|
||||
return ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin) + toMin;
|
||||
}
|
||||
|
||||
private pollGamepad() {
|
||||
const gamepads = navigator.getGamepads();
|
||||
if (this.slot < gamepads.length) {
|
||||
const gamepad = gamepads[this.slot];
|
||||
if (gamepad) {
|
||||
/* Button handling */
|
||||
gamepad.buttons.forEach((button, index) => {
|
||||
// Ignore d-pad buttons (12-15) as we handle those as axis
|
||||
if (index >= 12 && index <= 15) return;
|
||||
// ignore trigger buttons (6-7) as we handle those as axis
|
||||
if (index === 6 || index === 7) return;
|
||||
// If state differs, send
|
||||
if (button.pressed !== this.lastState.buttonState.get(index)) {
|
||||
const linuxCode = this.controllerButtonToVirtualKeyCode(index);
|
||||
if (linuxCode === undefined) {
|
||||
// Skip unmapped button index
|
||||
this.lastState.buttonState.set(index, button.pressed);
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerButton",
|
||||
value: create(ProtoControllerButtonSchema, {
|
||||
type: "ControllerButton",
|
||||
slot: this.slot,
|
||||
button: linuxCode,
|
||||
pressed: button.pressed,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const buttonMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: buttonProto,
|
||||
};
|
||||
this.wrtc.sendBinary(
|
||||
toBinary(ProtoMessageInputSchema, buttonMessage),
|
||||
);
|
||||
// Store button state
|
||||
this.lastState.buttonState.set(index, button.pressed);
|
||||
}
|
||||
});
|
||||
|
||||
/* Trigger handling */
|
||||
// map trigger value from 0.0 to 1.0 to -32768 to 32767
|
||||
const leftTrigger = Math.round(
|
||||
this.remapFromTo(gamepad.buttons[6]?.value ?? 0, 0, 1, -32768, 32767),
|
||||
);
|
||||
// If state differs, send
|
||||
if (leftTrigger !== this.lastState.leftTrigger) {
|
||||
const triggerProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerTrigger",
|
||||
value: create(ProtoControllerTriggerSchema, {
|
||||
type: "ControllerTrigger",
|
||||
slot: this.slot,
|
||||
trigger: 0, // 0 = left, 1 = right
|
||||
value: leftTrigger,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const triggerMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: triggerProto,
|
||||
};
|
||||
this.lastState.leftTrigger = leftTrigger;
|
||||
this.wrtc.sendBinary(
|
||||
toBinary(ProtoMessageInputSchema, triggerMessage),
|
||||
);
|
||||
}
|
||||
const rightTrigger = Math.round(
|
||||
this.remapFromTo(gamepad.buttons[7]?.value ?? 0, 0, 1, -32768, 32767),
|
||||
);
|
||||
// If state differs, send
|
||||
if (rightTrigger !== this.lastState.rightTrigger) {
|
||||
const triggerProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerTrigger",
|
||||
value: create(ProtoControllerTriggerSchema, {
|
||||
type: "ControllerTrigger",
|
||||
slot: this.slot,
|
||||
trigger: 1, // 0 = left, 1 = right
|
||||
value: rightTrigger,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const triggerMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: triggerProto,
|
||||
};
|
||||
this.lastState.rightTrigger = rightTrigger;
|
||||
this.wrtc.sendBinary(
|
||||
toBinary(ProtoMessageInputSchema, triggerMessage),
|
||||
);
|
||||
}
|
||||
|
||||
/* DPad handling */
|
||||
// We send dpad buttons as axis values -1 to 1 for left/up, right/down
|
||||
const dpadLeft = gamepad.buttons[14]?.pressed ? 1 : 0;
|
||||
const dpadRight = gamepad.buttons[15]?.pressed ? 1 : 0;
|
||||
const dpadX = dpadLeft ? -1 : dpadRight ? 1 : 0;
|
||||
if (dpadX !== this.lastState.dpadX) {
|
||||
const dpadProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerAxis",
|
||||
value: create(ProtoControllerAxisSchema, {
|
||||
type: "ControllerAxis",
|
||||
slot: this.slot,
|
||||
axis: 0, // 0 = dpadX, 1 = dpadY
|
||||
value: dpadX,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const dpadMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: dpadProto,
|
||||
};
|
||||
this.lastState.dpadX = dpadX;
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, dpadMessage));
|
||||
}
|
||||
|
||||
const dpadUp = gamepad.buttons[12]?.pressed ? 1 : 0;
|
||||
const dpadDown = gamepad.buttons[13]?.pressed ? 1 : 0;
|
||||
const dpadY = dpadUp ? -1 : dpadDown ? 1 : 0;
|
||||
if (dpadY !== this.lastState.dpadY) {
|
||||
const dpadProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerAxis",
|
||||
value: create(ProtoControllerAxisSchema, {
|
||||
type: "ControllerAxis",
|
||||
slot: this.slot,
|
||||
axis: 1, // 0 = dpadX, 1 = dpadY
|
||||
value: dpadY,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const dpadMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: dpadProto,
|
||||
};
|
||||
this.lastState.dpadY = dpadY;
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, dpadMessage));
|
||||
}
|
||||
|
||||
/* Stick handling */
|
||||
// stick values need to be mapped from -1.0 to 1.0 to -32768 to 32767
|
||||
const leftX = this.remapFromTo(gamepad.axes[0] ?? 0, -1, 1, -32768, 32767);
|
||||
const leftY = this.remapFromTo(gamepad.axes[1] ?? 0, -1, 1, -32768, 32767);
|
||||
// Apply deadzone
|
||||
const sendLeftX =
|
||||
Math.abs(leftX) > this.stickDeadzone ? Math.round(leftX) : 0;
|
||||
const sendLeftY =
|
||||
Math.abs(leftY) > this.stickDeadzone ? Math.round(leftY) : 0;
|
||||
// if outside deadzone, send normally if changed
|
||||
// if moves inside deadzone, zero it if not inside deadzone last time
|
||||
if (
|
||||
sendLeftX !== this.lastState.leftX ||
|
||||
sendLeftY !== this.lastState.leftY
|
||||
) {
|
||||
// console.log("Sticks: ", sendLeftX, sendLeftY, sendRightX, sendRightY);
|
||||
const stickProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerStick",
|
||||
value: create(ProtoControllerStickSchema, {
|
||||
type: "ControllerStick",
|
||||
slot: this.slot,
|
||||
stick: 0, // 0 = left, 1 = right
|
||||
x: sendLeftX,
|
||||
y: sendLeftY,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const stickMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: stickProto,
|
||||
};
|
||||
this.lastState.leftX = sendLeftX;
|
||||
this.lastState.leftY = sendLeftY;
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, stickMessage));
|
||||
}
|
||||
|
||||
const rightX = this.remapFromTo(gamepad.axes[2] ?? 0, -1, 1, -32768, 32767);
|
||||
const rightY = this.remapFromTo(gamepad.axes[3] ?? 0, -1, 1, -32768, 32767);
|
||||
// Apply deadzone
|
||||
const sendRightX =
|
||||
Math.abs(rightX) > this.stickDeadzone ? Math.round(rightX) : 0;
|
||||
const sendRightY =
|
||||
Math.abs(rightY) > this.stickDeadzone ? Math.round(rightY) : 0;
|
||||
if (
|
||||
sendRightX !== this.lastState.rightX ||
|
||||
sendRightY !== this.lastState.rightY
|
||||
) {
|
||||
const stickProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerStick",
|
||||
value: create(ProtoControllerStickSchema, {
|
||||
type: "ControllerStick",
|
||||
slot: this.slot,
|
||||
stick: 1, // 0 = left, 1 = right
|
||||
x: sendRightX,
|
||||
y: sendRightY,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const stickMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: stickProto,
|
||||
};
|
||||
this.lastState.rightX = sendRightX;
|
||||
this.lastState.rightY = sendRightY;
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, stickMessage));
|
||||
}
|
||||
}
|
||||
private restartPolling() {
|
||||
// Clear existing interval
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
|
||||
// Restart with active polling
|
||||
this.pollingState = PollState.RUNNING;
|
||||
this.lastInputTime = Date.now();
|
||||
|
||||
// Start interval
|
||||
this.pollInterval = setInterval(
|
||||
() => this.pollGamepad(),
|
||||
this.FULL_RATE_MS,
|
||||
);
|
||||
}
|
||||
|
||||
private loopInterval: any = null;
|
||||
private pollGamepad() {
|
||||
if (!this.connected || !this.gamepad) return;
|
||||
|
||||
const gamepads = navigator.getGamepads();
|
||||
if (!gamepads[this.gamepad.index]) return;
|
||||
|
||||
this.gamepad = gamepads[this.gamepad.index];
|
||||
|
||||
// Collect state changes
|
||||
const changedFields = this.collectStateChanges();
|
||||
|
||||
// Send batched changes update if there's changes
|
||||
if (changedFields > 0) {
|
||||
let send_type = 1;
|
||||
const timeSinceFull = Date.now() - this.lastFullTime;
|
||||
if (timeSinceFull > this.FULL_INTERVAL) {
|
||||
send_type = 0;
|
||||
this.lastFullTime = Date.now();
|
||||
}
|
||||
|
||||
this.sendBatchedState(changedFields, send_type);
|
||||
this.lastInputTime = Date.now();
|
||||
if (this.pollingState !== PollState.RUNNING) {
|
||||
this.pollingState = PollState.RUNNING;
|
||||
}
|
||||
}
|
||||
|
||||
const timeSinceInput = Date.now() - this.lastInputTime;
|
||||
if (timeSinceInput > this.IDLE_THRESHOLD) {
|
||||
// Changing from running to idle..
|
||||
if (this.pollingState === PollState.RUNNING) {
|
||||
// Send full state on idle assumption
|
||||
this.sendBatchedState(0xff, 0);
|
||||
this.pollingState = PollState.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
this.state.buttonState.forEach((b, i) =>
|
||||
this.state.previousButtonState.set(i, b),
|
||||
);
|
||||
}
|
||||
|
||||
private collectStateChanges(): number {
|
||||
let changedFields = 0;
|
||||
|
||||
// Collect analog values
|
||||
const leftTrigger = Math.round(
|
||||
this.remapFromTo(
|
||||
this.gamepad.buttons[6]?.value ?? 0,
|
||||
0,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
),
|
||||
);
|
||||
const rightTrigger = Math.round(
|
||||
this.remapFromTo(
|
||||
this.gamepad.buttons[7]?.value ?? 0,
|
||||
0,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
),
|
||||
);
|
||||
|
||||
const leftX = this.remapFromTo(
|
||||
this.gamepad.axes[0] ?? 0,
|
||||
-1,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
);
|
||||
const leftY = this.remapFromTo(
|
||||
this.gamepad.axes[1] ?? 0,
|
||||
-1,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
);
|
||||
const sendLeftX =
|
||||
Math.abs(leftX) > this.stickDeadzone ? Math.round(leftX) : 0;
|
||||
const sendLeftY =
|
||||
Math.abs(leftY) > this.stickDeadzone ? Math.round(leftY) : 0;
|
||||
|
||||
const rightX = this.remapFromTo(
|
||||
this.gamepad.axes[2] ?? 0,
|
||||
-1,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
);
|
||||
const rightY = this.remapFromTo(
|
||||
this.gamepad.axes[3] ?? 0,
|
||||
-1,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
);
|
||||
const sendRightX =
|
||||
Math.abs(rightX) > this.stickDeadzone ? Math.round(rightX) : 0;
|
||||
const sendRightY =
|
||||
Math.abs(rightY) > this.stickDeadzone ? Math.round(rightY) : 0;
|
||||
|
||||
const dpadX =
|
||||
(this.gamepad.buttons[14]?.pressed ? -1 : 0) +
|
||||
(this.gamepad.buttons[15]?.pressed ? 1 : 0);
|
||||
const dpadY =
|
||||
(this.gamepad.buttons[12]?.pressed ? -1 : 0) +
|
||||
(this.gamepad.buttons[13]?.pressed ? 1 : 0);
|
||||
|
||||
// Check what changed
|
||||
for (let i = 0; i < this.gamepad.buttons.length; i++) {
|
||||
if (i >= 6 && i <= 7) continue; // Skip triggers
|
||||
if (i >= 12 && i <= 15) continue; // Skip d-pad
|
||||
if (this.state.buttonState.get(i) !== this.gamepad.buttons[i].pressed) {
|
||||
changedFields |= this.CHANGED_BUTTONS_STATE;
|
||||
}
|
||||
this.state.buttonState.set(i, this.gamepad.buttons[i].pressed);
|
||||
}
|
||||
if (leftTrigger !== this.state.leftTrigger) {
|
||||
changedFields |= this.CHANGED_LEFT_TRIGGER;
|
||||
}
|
||||
this.state.leftTrigger = leftTrigger;
|
||||
if (rightTrigger !== this.state.rightTrigger) {
|
||||
changedFields |= this.CHANGED_RIGHT_TRIGGER;
|
||||
}
|
||||
this.state.rightTrigger = rightTrigger;
|
||||
if (sendLeftX !== this.state.leftX) {
|
||||
changedFields |= this.CHANGED_LEFT_STICK_X;
|
||||
}
|
||||
this.state.leftX = sendLeftX;
|
||||
if (sendLeftY !== this.state.leftY) {
|
||||
changedFields |= this.CHANGED_LEFT_STICK_Y;
|
||||
}
|
||||
this.state.leftY = sendLeftY;
|
||||
if (sendRightX !== this.state.rightX) {
|
||||
changedFields |= this.CHANGED_RIGHT_STICK_X;
|
||||
}
|
||||
this.state.rightX = sendRightX;
|
||||
if (sendRightY !== this.state.rightY) {
|
||||
changedFields |= this.CHANGED_RIGHT_STICK_Y;
|
||||
}
|
||||
this.state.rightY = sendRightY;
|
||||
if (dpadX !== this.state.dpadX) {
|
||||
changedFields |= this.CHANGED_DPAD_X;
|
||||
}
|
||||
this.state.dpadX = dpadX;
|
||||
if (dpadY !== this.state.dpadY) {
|
||||
changedFields |= this.CHANGED_DPAD_Y;
|
||||
}
|
||||
this.state.dpadY = dpadY;
|
||||
|
||||
return changedFields;
|
||||
}
|
||||
|
||||
private sendBatchedState(changedFields: number, updateType: number) {
|
||||
// @ts-ignore
|
||||
let message: ProtoControllerStateBatch = {
|
||||
sessionSlot: this.gamepad.index,
|
||||
sessionId: this.wrtc.getSessionID(),
|
||||
updateType: updateType,
|
||||
sequence: this.sequence++,
|
||||
};
|
||||
|
||||
// For FULL_STATE, include everything
|
||||
if (updateType === 0) {
|
||||
message.changedFields = 0xff;
|
||||
|
||||
message.buttonChangedMask = Object.fromEntries(
|
||||
Array.from(this.state.buttonState)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
[this.controllerButtonToVirtualKeyCode(key), value] as const,
|
||||
)
|
||||
.filter(([code]) => code !== undefined),
|
||||
);
|
||||
message.leftStickX = this.state.leftX;
|
||||
message.leftStickY = this.state.leftY;
|
||||
message.rightStickX = this.state.rightX;
|
||||
message.rightStickY = this.state.rightY;
|
||||
message.leftTrigger = this.state.leftTrigger;
|
||||
message.rightTrigger = this.state.rightTrigger;
|
||||
message.dpadX = this.state.dpadX;
|
||||
message.dpadY = this.state.dpadY;
|
||||
}
|
||||
// For DELTA, only include changed fields
|
||||
else {
|
||||
message.changedFields = changedFields;
|
||||
|
||||
if (changedFields & this.CHANGED_BUTTONS_STATE) {
|
||||
const currentStateMap = this.state.buttonState;
|
||||
const previousStateMap = this.state.previousButtonState;
|
||||
const allKeys = new Set([
|
||||
// @ts-ignore
|
||||
...currentStateMap.keys(),
|
||||
// @ts-ignore
|
||||
...previousStateMap.keys(),
|
||||
]);
|
||||
message.buttonChangedMask = Object.fromEntries(
|
||||
Array.from(allKeys)
|
||||
.filter((key) => {
|
||||
const newState = currentStateMap.get(key);
|
||||
const oldState = previousStateMap.get(key);
|
||||
return newState !== oldState;
|
||||
})
|
||||
.map((key) => {
|
||||
const newValue = currentStateMap.get(key) ?? false;
|
||||
return [
|
||||
this.controllerButtonToVirtualKeyCode(key),
|
||||
newValue,
|
||||
] as const;
|
||||
})
|
||||
.filter(([code]) => code !== undefined),
|
||||
);
|
||||
}
|
||||
if (changedFields & this.CHANGED_LEFT_STICK_X) {
|
||||
message.leftStickX = this.state.leftX;
|
||||
}
|
||||
if (changedFields & this.CHANGED_LEFT_STICK_Y) {
|
||||
message.leftStickY = this.state.leftY;
|
||||
}
|
||||
if (changedFields & this.CHANGED_RIGHT_STICK_X) {
|
||||
message.rightStickX = this.state.rightX;
|
||||
}
|
||||
if (changedFields & this.CHANGED_RIGHT_STICK_Y) {
|
||||
message.rightStickY = this.state.rightY;
|
||||
}
|
||||
if (changedFields & this.CHANGED_LEFT_TRIGGER) {
|
||||
message.leftTrigger = this.state.leftTrigger;
|
||||
}
|
||||
if (changedFields & this.CHANGED_RIGHT_TRIGGER) {
|
||||
message.rightTrigger = this.state.rightTrigger;
|
||||
}
|
||||
if (changedFields & this.CHANGED_DPAD_X) {
|
||||
message.dpadX = this.state.dpadX;
|
||||
}
|
||||
if (changedFields & this.CHANGED_DPAD_Y) {
|
||||
message.dpadY = this.state.dpadY;
|
||||
}
|
||||
}
|
||||
|
||||
// Send message
|
||||
const batchMessage = createMessage(
|
||||
create(
|
||||
ProtoControllerStateBatchSchema,
|
||||
message as ProtoControllerStateBatch,
|
||||
),
|
||||
"controllerInput",
|
||||
);
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, batchMessage));
|
||||
}
|
||||
|
||||
public run() {
|
||||
if (this.connected)
|
||||
this.stop();
|
||||
if (this.connected) this.stop();
|
||||
|
||||
this.connected = true;
|
||||
// Poll gamepads in setInterval loop
|
||||
this.loopInterval = setInterval(() => {
|
||||
if (this.connected) this.pollGamepad();
|
||||
}, this.updateInterval);
|
||||
|
||||
// Start with active polling
|
||||
this.restartPolling();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.loopInterval) {
|
||||
clearInterval(this.loopInterval);
|
||||
this.loopInterval = null;
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
public getSlot() {
|
||||
return this.slot;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.stop();
|
||||
// Remove callback
|
||||
if (this._dcRumbleHandler !== null) {
|
||||
this.wrtc.removeDataChannelCallback(this._dcRumbleHandler);
|
||||
this._dcRumbleHandler = null;
|
||||
if (this._dcHandler !== null) {
|
||||
this.wrtc.removeDataChannelCallback(this._dcHandler);
|
||||
this._dcHandler = null;
|
||||
}
|
||||
// Gamepad disconnected
|
||||
const detachMsg = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerDetach",
|
||||
value: create(ProtoControllerDetachSchema, {
|
||||
type: "ControllerDetach",
|
||||
slot: this.slot,
|
||||
if (this.gamepad) {
|
||||
// Gamepad disconnected
|
||||
const detachMsg = createMessage(
|
||||
create(ProtoControllerDetachSchema, {
|
||||
sessionSlot: this.gamepad.index,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: detachMsg,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
"controllerInput",
|
||||
);
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, detachMsg));
|
||||
}
|
||||
}
|
||||
|
||||
private controllerButtonToVirtualKeyCode(code: number) {
|
||||
private controllerButtonToVirtualKeyCode(code: number): number | undefined {
|
||||
return controllerButtonToLinuxEventCode[code] || undefined;
|
||||
}
|
||||
|
||||
private rumbleCallback(data: ArrayBuffer) {
|
||||
// If not connected, ignore
|
||||
if (!this.connected) return;
|
||||
try {
|
||||
// First decode the wrapper message
|
||||
const uint8Data = new Uint8Array(data);
|
||||
const messageWrapper = fromBinary(ProtoMessageInputSchema, uint8Data);
|
||||
private rumbleCallback(rumbleMsg: ProtoControllerRumble) {
|
||||
if (!this.connected || !this.gamepad) return;
|
||||
|
||||
// Check if it contains controller rumble data
|
||||
if (messageWrapper.data?.inputType?.case === "controllerRumble") {
|
||||
const rumbleMsg = messageWrapper.data.inputType.value as ProtoControllerRumble;
|
||||
// Check if this rumble is for us
|
||||
if (
|
||||
rumbleMsg.sessionId !== this.wrtc.getSessionID() ||
|
||||
rumbleMsg.sessionSlot !== this.gamepad.index
|
||||
)
|
||||
return;
|
||||
|
||||
// Check if aimed at this controller slot
|
||||
if (rumbleMsg.slot !== this.slot) return;
|
||||
|
||||
// Trigger actual rumble
|
||||
// Need to remap from 0-65535 to 0.0-1.0 ranges
|
||||
const clampedLowFreq = Math.max(0, Math.min(65535, rumbleMsg.lowFrequency));
|
||||
const rumbleLowFreq = this.remapFromTo(
|
||||
clampedLowFreq,
|
||||
0,
|
||||
65535,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
const clampedHighFreq = Math.max(0, Math.min(65535, rumbleMsg.highFrequency));
|
||||
const rumbleHighFreq = this.remapFromTo(
|
||||
clampedHighFreq,
|
||||
0,
|
||||
65535,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
// Cap to valid range (max 5000)
|
||||
const rumbleDuration = Math.max(0, Math.min(5000, rumbleMsg.duration));
|
||||
if (this.gamepad.vibrationActuator) {
|
||||
this.gamepad.vibrationActuator.playEffect("dual-rumble", {
|
||||
startDelay: 0,
|
||||
duration: rumbleDuration,
|
||||
weakMagnitude: rumbleLowFreq,
|
||||
strongMagnitude: rumbleHighFreq,
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to decode rumble message:", error);
|
||||
// Trigger actual rumble
|
||||
// Need to remap from 0-65535 to 0.0-1.0 ranges
|
||||
const clampedLowFreq = Math.max(0, Math.min(65535, rumbleMsg.lowFrequency));
|
||||
const rumbleLowFreq = this.remapFromTo(clampedLowFreq, 0, 65535, 0.0, 1.0);
|
||||
const clampedHighFreq = Math.max(
|
||||
0,
|
||||
Math.min(65535, rumbleMsg.highFrequency),
|
||||
);
|
||||
const rumbleHighFreq = this.remapFromTo(
|
||||
clampedHighFreq,
|
||||
0,
|
||||
65535,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
// Cap to valid range (max 5000)
|
||||
const rumbleDuration = Math.max(0, Math.min(5000, rumbleMsg.duration));
|
||||
if (this.gamepad.vibrationActuator) {
|
||||
this.gamepad.vibrationActuator
|
||||
.playEffect("dual-rumble", {
|
||||
startDelay: 0,
|
||||
duration: rumbleDuration,
|
||||
weakMagnitude: rumbleLowFreq,
|
||||
strongMagnitude: rumbleHighFreq,
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import {keyCodeToLinuxEventCode} from "./codes"
|
||||
import {WebRTCStream} from "./webrtc-stream";
|
||||
import {LatencyTracker} from "./latency";
|
||||
import {ProtoLatencyTracker, ProtoTimestampEntry} from "./proto/latency_tracker_pb";
|
||||
import {timestampFromDate} from "@bufbuild/protobuf/wkt";
|
||||
import {ProtoMessageBase, ProtoMessageInput, ProtoMessageInputSchema} from "./proto/messages_pb";
|
||||
import {
|
||||
ProtoInput,
|
||||
ProtoInputSchema,
|
||||
ProtoKeyDownSchema,
|
||||
ProtoKeyUpSchema,
|
||||
} from "./proto/types_pb";
|
||||
import {create, toBinary} from "@bufbuild/protobuf";
|
||||
import { keyCodeToLinuxEventCode } from "./codes";
|
||||
import { WebRTCStream } from "./webrtc-stream";
|
||||
import { ProtoKeyDownSchema, ProtoKeyUpSchema } from "./proto/types_pb";
|
||||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { createMessage } from "./utils";
|
||||
import { ProtoMessageSchema } from "./proto/messages_pb";
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCStream;
|
||||
@@ -24,38 +17,29 @@ export class Keyboard {
|
||||
private readonly keydownListener: (e: KeyboardEvent) => void;
|
||||
private readonly keyupListener: (e: KeyboardEvent) => void;
|
||||
|
||||
constructor({webrtc}: Props) {
|
||||
constructor({ webrtc }: Props) {
|
||||
this.wrtc = webrtc;
|
||||
this.keydownListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "keyDown",
|
||||
value: create(ProtoKeyDownSchema, {
|
||||
type: "KeyDown",
|
||||
key: this.keyToVirtualKeyCode(e.code)
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.keyupListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "keyUp",
|
||||
value: create(ProtoKeyUpSchema, {
|
||||
type: "KeyUp",
|
||||
key: this.keyToVirtualKeyCode(e.code)
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.run()
|
||||
this.keydownListener = this.createKeyboardListener((e: any) =>
|
||||
create(ProtoKeyDownSchema, {
|
||||
key: this.keyToVirtualKeyCode(e.code),
|
||||
}),
|
||||
);
|
||||
this.keyupListener = this.createKeyboardListener((e: any) =>
|
||||
create(ProtoKeyUpSchema, {
|
||||
key: this.keyToVirtualKeyCode(e.code),
|
||||
}),
|
||||
);
|
||||
this.run();
|
||||
}
|
||||
|
||||
private run() {
|
||||
if (this.connected)
|
||||
this.stop()
|
||||
if (this.connected) this.stop();
|
||||
|
||||
this.connected = true
|
||||
document.addEventListener("keydown", this.keydownListener, {passive: false});
|
||||
document.addEventListener("keyup", this.keyupListener, {passive: false});
|
||||
this.connected = true;
|
||||
document.addEventListener("keydown", this.keydownListener, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("keyup", this.keyupListener, { passive: false });
|
||||
}
|
||||
|
||||
private stop() {
|
||||
@@ -65,42 +49,19 @@ export class Keyboard {
|
||||
}
|
||||
|
||||
// Helper function to create and return mouse listeners
|
||||
private createKeyboardListener(dataCreator: (e: Event) => ProtoInput): (e: Event) => void {
|
||||
private createKeyboardListener(
|
||||
dataCreator: (e: Event) => any,
|
||||
): (e: Event) => void {
|
||||
return (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Prevent repeated key events from being sent (important for games)
|
||||
if ((e as any).repeat)
|
||||
return;
|
||||
if ((e as any).repeat) return;
|
||||
|
||||
const data = dataCreator(e as any);
|
||||
|
||||
// Latency tracking
|
||||
const tracker = new LatencyTracker("input-keyboard");
|
||||
tracker.addTimestamp("client_send");
|
||||
const protoTracker: ProtoLatencyTracker = {
|
||||
$typeName: "proto.ProtoLatencyTracker",
|
||||
sequenceId: tracker.sequence_id,
|
||||
timestamps: [],
|
||||
};
|
||||
for (const t of tracker.timestamps) {
|
||||
protoTracker.timestamps.push({
|
||||
$typeName: "proto.ProtoTimestampEntry",
|
||||
stage: t.stage,
|
||||
time: timestampFromDate(t.time),
|
||||
} as ProtoTimestampEntry);
|
||||
}
|
||||
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "input",
|
||||
latency: protoTracker,
|
||||
} as ProtoMessageBase,
|
||||
data: data,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
const message = createMessage(data, "input");
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,4 +75,4 @@ export class Keyboard {
|
||||
if (code === "Home") return 1;
|
||||
return keyCodeToLinuxEventCode[code] || undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
import { LatencyTracker } from "./latency";
|
||||
import { Uint8ArrayList } from "uint8arraylist";
|
||||
import { allocUnsafe } from "uint8arrays/alloc";
|
||||
import { pipe } from "it-pipe";
|
||||
import { decode, encode } from "it-length-prefixed";
|
||||
import { Stream } from "@libp2p/interface";
|
||||
|
||||
export interface MessageBase {
|
||||
payload_type: string;
|
||||
latency?: LatencyTracker;
|
||||
}
|
||||
|
||||
export interface MessageRaw extends MessageBase {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export function NewMessageRaw(type: string, data: any): Uint8Array {
|
||||
const msg = {
|
||||
payload_type: type,
|
||||
data: data,
|
||||
};
|
||||
return new TextEncoder().encode(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
export interface MessageICE extends MessageBase {
|
||||
candidate: RTCIceCandidateInit;
|
||||
}
|
||||
|
||||
export function NewMessageICE(
|
||||
type: string,
|
||||
candidate: RTCIceCandidateInit,
|
||||
): Uint8Array {
|
||||
const msg = {
|
||||
payload_type: type,
|
||||
candidate: candidate,
|
||||
};
|
||||
return new TextEncoder().encode(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
export interface MessageSDP extends MessageBase {
|
||||
sdp: RTCSessionDescriptionInit;
|
||||
}
|
||||
|
||||
export function NewMessageSDP(
|
||||
type: string,
|
||||
sdp: RTCSessionDescriptionInit,
|
||||
): Uint8Array {
|
||||
const msg = {
|
||||
payload_type: type,
|
||||
sdp: sdp,
|
||||
};
|
||||
return new TextEncoder().encode(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
const MAX_SIZE = 1024 * 1024; // 1MB
|
||||
const MAX_QUEUE_SIZE = 1000; // Maximum number of messages in the queue
|
||||
|
||||
// Custom 4-byte length encoder
|
||||
export const length4ByteEncoder = (length: number) => {
|
||||
const buf = allocUnsafe(4);
|
||||
|
||||
// Write the length as a 32-bit unsigned integer (4 bytes)
|
||||
buf[0] = length >>> 24;
|
||||
buf[1] = (length >>> 16) & 0xff;
|
||||
buf[2] = (length >>> 8) & 0xff;
|
||||
buf[3] = length & 0xff;
|
||||
|
||||
// Set the bytes property to 4
|
||||
length4ByteEncoder.bytes = 4;
|
||||
|
||||
return buf;
|
||||
};
|
||||
length4ByteEncoder.bytes = 4;
|
||||
|
||||
// Custom 4-byte length decoder
|
||||
export const length4ByteDecoder = (data: Uint8ArrayList) => {
|
||||
if (data.byteLength < 4) {
|
||||
// Not enough bytes to read the length
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the length from the first 4 bytes
|
||||
let length = 0;
|
||||
length =
|
||||
(data.subarray(0, 1)[0] >>> 0) * 0x1000000 +
|
||||
(data.subarray(1, 2)[0] >>> 0) * 0x10000 +
|
||||
(data.subarray(2, 3)[0] >>> 0) * 0x100 +
|
||||
(data.subarray(3, 4)[0] >>> 0);
|
||||
|
||||
// Set bytes read to 4
|
||||
length4ByteDecoder.bytes = 4;
|
||||
|
||||
return length;
|
||||
};
|
||||
length4ByteDecoder.bytes = 4;
|
||||
|
||||
interface PromiseMessage {
|
||||
data: Uint8Array;
|
||||
resolve: () => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
export class SafeStream {
|
||||
private stream: Stream;
|
||||
private callbacks: Map<string, ((data: any) => void)[]> = new Map();
|
||||
private isReading: boolean = false;
|
||||
private isWriting: boolean = false;
|
||||
private closed: boolean = false;
|
||||
private messageQueue: PromiseMessage[] = [];
|
||||
private writeLock = false;
|
||||
private readRetries = 0;
|
||||
private writeRetries = 0;
|
||||
private readonly MAX_RETRIES = 5;
|
||||
|
||||
constructor(stream: Stream) {
|
||||
this.stream = stream;
|
||||
this.startReading();
|
||||
this.startWriting();
|
||||
}
|
||||
|
||||
private async startReading(): Promise<void> {
|
||||
if (this.isReading || this.closed) return;
|
||||
|
||||
this.isReading = true;
|
||||
|
||||
try {
|
||||
const source = this.stream.source;
|
||||
const decodedSource = decode(source, {
|
||||
maxDataLength: MAX_SIZE,
|
||||
lengthDecoder: length4ByteDecoder,
|
||||
});
|
||||
|
||||
for await (const chunk of decodedSource) {
|
||||
if (this.closed) break;
|
||||
|
||||
this.readRetries = 0;
|
||||
|
||||
try {
|
||||
const data = chunk.slice();
|
||||
const message = JSON.parse(
|
||||
new TextDecoder().decode(data),
|
||||
) as MessageBase;
|
||||
const msgType = message.payload_type;
|
||||
|
||||
if (this.callbacks.has(msgType)) {
|
||||
const handlers = this.callbacks.get(msgType)!;
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
handler(message);
|
||||
} catch (err) {
|
||||
console.error(`Error in message handler for ${msgType}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error processing message:", err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Stream reading error:", err);
|
||||
} finally {
|
||||
this.isReading = false;
|
||||
this.readRetries++;
|
||||
|
||||
// If not closed, try to restart reading
|
||||
if (!this.closed && this.readRetries < this.MAX_RETRIES)
|
||||
setTimeout(() => this.startReading(), 100);
|
||||
else if (this.readRetries >= this.MAX_RETRIES)
|
||||
console.error(
|
||||
"Max retries reached for reading stream, stopping attempts",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public registerCallback(
|
||||
msgType: string,
|
||||
callback: (data: any) => void,
|
||||
): void {
|
||||
if (!this.callbacks.has(msgType)) {
|
||||
this.callbacks.set(msgType, []);
|
||||
}
|
||||
|
||||
this.callbacks.get(msgType)!.push(callback);
|
||||
}
|
||||
|
||||
public removeCallback(msgType: string, callback: (data: any) => void): void {
|
||||
if (this.callbacks.has(msgType)) {
|
||||
const callbacks = this.callbacks.get(msgType)!;
|
||||
const index = callbacks.indexOf(callback);
|
||||
|
||||
if (index !== -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
if (callbacks.length === 0) {
|
||||
this.callbacks.delete(msgType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async startWriting(): Promise<void> {
|
||||
if (this.isWriting || this.closed) return;
|
||||
|
||||
this.isWriting = true;
|
||||
|
||||
try {
|
||||
// Create an async generator for real-time message processing
|
||||
const messageSource = async function* (this: SafeStream) {
|
||||
while (!this.closed) {
|
||||
// Check if we have messages to send
|
||||
if (this.messageQueue.length > 0) {
|
||||
this.writeLock = true;
|
||||
|
||||
try {
|
||||
const message = this.messageQueue[0];
|
||||
|
||||
// Encode the message
|
||||
const encoded = encode([message.data], {
|
||||
maxDataLength: MAX_SIZE,
|
||||
lengthEncoder: length4ByteEncoder,
|
||||
});
|
||||
|
||||
for await (const chunk of encoded) {
|
||||
yield chunk;
|
||||
}
|
||||
|
||||
// Remove message after successful sending
|
||||
this.writeRetries = 0;
|
||||
const sentMessage = this.messageQueue.shift();
|
||||
if (sentMessage)
|
||||
sentMessage.resolve();
|
||||
} catch (err) {
|
||||
console.error("Error encoding or sending message:", err);
|
||||
const failedMessage = this.messageQueue.shift();
|
||||
if (failedMessage)
|
||||
failedMessage.reject(new Error(`Failed to send message: ${err}`));
|
||||
} finally {
|
||||
this.writeLock = false;
|
||||
}
|
||||
} else {
|
||||
// No messages to send, wait for a short period
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
await pipe(messageSource(), this.stream.sink).catch((err) => {
|
||||
console.error("Sink error:", err);
|
||||
this.isWriting = false;
|
||||
this.writeRetries++;
|
||||
|
||||
// Try to restart if not closed
|
||||
if (!this.closed && this.writeRetries < this.MAX_RETRIES) {
|
||||
setTimeout(() => this.startWriting(), 1000);
|
||||
} else if (this.writeRetries >= this.MAX_RETRIES) {
|
||||
console.error("Max retries reached for writing to stream sink, stopping attempts");
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Stream writing error:", err);
|
||||
this.isWriting = false;
|
||||
this.writeRetries++;
|
||||
|
||||
// Try to restart if not closed
|
||||
if (!this.closed && this.writeRetries < this.MAX_RETRIES) {
|
||||
setTimeout(() => this.startWriting(), 1000);
|
||||
} else if (this.writeRetries >= this.MAX_RETRIES) {
|
||||
console.error("Max retries reached for writing stream, stopping attempts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async writeMessage(message: Uint8Array): Promise<void> {
|
||||
if (this.closed) {
|
||||
throw new Error("Cannot write to closed stream");
|
||||
}
|
||||
|
||||
// Validate message size before queuing
|
||||
if (message.length > MAX_SIZE) {
|
||||
throw new Error("Message size exceeds maximum size limit");
|
||||
}
|
||||
|
||||
// Check if the message queue is too large
|
||||
if (this.messageQueue.length >= MAX_QUEUE_SIZE) {
|
||||
throw new Error("Message queue is full, cannot write message");
|
||||
}
|
||||
|
||||
// Create a promise to resolve when the message is sent
|
||||
return new Promise((resolve, reject) => {
|
||||
this.messageQueue.push({ data: message, resolve, reject } as PromiseMessage);
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.closed = true;
|
||||
this.callbacks.clear();
|
||||
// Reject pending messages
|
||||
for (const msg of this.messageQueue)
|
||||
msg.reject(new Error("Stream closed"));
|
||||
|
||||
this.messageQueue = [];
|
||||
this.readRetries = 0;
|
||||
this.writeRetries = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
import {WebRTCStream} from "./webrtc-stream";
|
||||
import {LatencyTracker} from "./latency";
|
||||
import {ProtoMessageInput, ProtoMessageBase, ProtoMessageInputSchema} from "./proto/messages_pb";
|
||||
import { WebRTCStream } from "./webrtc-stream";
|
||||
import {
|
||||
ProtoInput, ProtoInputSchema,
|
||||
ProtoMouseKeyDown, ProtoMouseKeyDownSchema,
|
||||
ProtoMouseKeyUp, ProtoMouseKeyUpSchema,
|
||||
ProtoMouseMove,
|
||||
ProtoMouseKeyDownSchema,
|
||||
ProtoMouseKeyUpSchema,
|
||||
ProtoMouseMoveSchema,
|
||||
ProtoMouseWheel, ProtoMouseWheelSchema
|
||||
ProtoMouseWheelSchema,
|
||||
} from "./proto/types_pb";
|
||||
import {mouseButtonToLinuxEventCode} from "./codes";
|
||||
import {ProtoLatencyTracker, ProtoTimestampEntry} from "./proto/latency_tracker_pb";
|
||||
import {create, toBinary} from "@bufbuild/protobuf";
|
||||
import {timestampFromDate} from "@bufbuild/protobuf/wkt";
|
||||
import { mouseButtonToLinuxEventCode } from "./codes";
|
||||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { createMessage } from "./utils";
|
||||
import { ProtoMessageSchema } from "./proto/messages_pb";
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCStream;
|
||||
@@ -24,7 +20,7 @@ export class Mouse {
|
||||
protected canvas: HTMLCanvasElement;
|
||||
protected connected!: boolean;
|
||||
|
||||
private sendInterval = 10 // 100 updates per second
|
||||
private sendInterval = 10; // 100 updates per second
|
||||
|
||||
// Store references to event listeners
|
||||
private readonly mousemoveListener: (e: MouseEvent) => void;
|
||||
@@ -35,7 +31,7 @@ export class Mouse {
|
||||
private readonly mouseupListener: (e: MouseEvent) => void;
|
||||
private readonly mousewheelListener: (e: WheelEvent) => void;
|
||||
|
||||
constructor({webrtc, canvas}: Props) {
|
||||
constructor({ webrtc, canvas }: Props) {
|
||||
this.wrtc = webrtc;
|
||||
this.canvas = canvas;
|
||||
|
||||
@@ -48,65 +44,56 @@ export class Mouse {
|
||||
this.movementY += e.movementY;
|
||||
};
|
||||
|
||||
this.mousedownListener = this.createMouseListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "mouseKeyDown",
|
||||
value: create(ProtoMouseKeyDownSchema, {
|
||||
type: "MouseKeyDown",
|
||||
key: this.keyToVirtualKeyCode(e.button)
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.mouseupListener = this.createMouseListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "mouseKeyUp",
|
||||
value: create(ProtoMouseKeyUpSchema, {
|
||||
type: "MouseKeyUp",
|
||||
key: this.keyToVirtualKeyCode(e.button)
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.mousewheelListener = this.createMouseListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "mouseWheel",
|
||||
value: create(ProtoMouseWheelSchema, {
|
||||
type: "MouseWheel",
|
||||
x: Math.round(e.deltaX),
|
||||
y: Math.round(e.deltaY),
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.mousedownListener = this.createMouseListener((e: any) =>
|
||||
create(ProtoMouseKeyDownSchema, {
|
||||
key: this.keyToVirtualKeyCode(e.button),
|
||||
}),
|
||||
);
|
||||
this.mouseupListener = this.createMouseListener((e: any) =>
|
||||
create(ProtoMouseKeyUpSchema, {
|
||||
key: this.keyToVirtualKeyCode(e.button),
|
||||
}),
|
||||
);
|
||||
this.mousewheelListener = this.createMouseListener((e: any) =>
|
||||
create(ProtoMouseWheelSchema, {
|
||||
x: Math.round(e.deltaX),
|
||||
y: Math.round(e.deltaY),
|
||||
}),
|
||||
);
|
||||
|
||||
this.run()
|
||||
this.run();
|
||||
this.startProcessing();
|
||||
}
|
||||
|
||||
private run() {
|
||||
//calls all the other functions
|
||||
if (!document.pointerLockElement) {
|
||||
console.log("no pointerlock")
|
||||
console.log("no pointerlock");
|
||||
if (this.connected) {
|
||||
this.stop()
|
||||
this.stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.pointerLockElement == this.canvas) {
|
||||
this.connected = true
|
||||
this.canvas.addEventListener("mousemove", this.mousemoveListener, {passive: false});
|
||||
this.canvas.addEventListener("mousedown", this.mousedownListener, {passive: false});
|
||||
this.canvas.addEventListener("mouseup", this.mouseupListener, {passive: false});
|
||||
this.canvas.addEventListener("wheel", this.mousewheelListener, {passive: false});
|
||||
|
||||
this.connected = true;
|
||||
this.canvas.addEventListener("mousemove", this.mousemoveListener, {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("mousedown", this.mousedownListener, {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("mouseup", this.mouseupListener, {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("wheel", this.mousewheelListener, {
|
||||
passive: false,
|
||||
});
|
||||
} else {
|
||||
if (this.connected) {
|
||||
this.stop()
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private stop() {
|
||||
@@ -128,79 +115,26 @@ export class Mouse {
|
||||
}
|
||||
|
||||
private sendAggregatedMouseMove() {
|
||||
const data = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "mouseMove",
|
||||
value: create(ProtoMouseMoveSchema, {
|
||||
type: "MouseMove",
|
||||
x: Math.round(this.movementX),
|
||||
y: Math.round(this.movementY),
|
||||
}),
|
||||
},
|
||||
const data = create(ProtoMouseMoveSchema, {
|
||||
x: Math.round(this.movementX),
|
||||
y: Math.round(this.movementY),
|
||||
});
|
||||
|
||||
// Latency tracking
|
||||
const tracker = new LatencyTracker("input-mouse");
|
||||
tracker.addTimestamp("client_send");
|
||||
const protoTracker: ProtoLatencyTracker = {
|
||||
$typeName: "proto.ProtoLatencyTracker",
|
||||
sequenceId: tracker.sequence_id,
|
||||
timestamps: [],
|
||||
};
|
||||
for (const t of tracker.timestamps) {
|
||||
protoTracker.timestamps.push({
|
||||
$typeName: "proto.ProtoTimestampEntry",
|
||||
stage: t.stage,
|
||||
time: timestampFromDate(t.time),
|
||||
} as ProtoTimestampEntry);
|
||||
}
|
||||
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "input",
|
||||
latency: protoTracker,
|
||||
} as ProtoMessageBase,
|
||||
data: data,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
const message = createMessage(data, "input");
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
|
||||
}
|
||||
|
||||
// Helper function to create and return mouse listeners
|
||||
private createMouseListener(dataCreator: (e: Event) => ProtoInput): (e: Event) => void {
|
||||
private createMouseListener(
|
||||
dataCreator: (e: Event) => any,
|
||||
): (e: Event) => void {
|
||||
return (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const data = dataCreator(e as any);
|
||||
|
||||
// Latency tracking
|
||||
const tracker = new LatencyTracker("input-mouse");
|
||||
tracker.addTimestamp("client_send");
|
||||
const protoTracker: ProtoLatencyTracker = {
|
||||
$typeName: "proto.ProtoLatencyTracker",
|
||||
sequenceId: tracker.sequence_id,
|
||||
timestamps: [],
|
||||
};
|
||||
for (const t of tracker.timestamps) {
|
||||
protoTracker.timestamps.push({
|
||||
$typeName: "proto.ProtoTimestampEntry",
|
||||
stage: t.stage,
|
||||
time: timestampFromDate(t.time),
|
||||
} as ProtoTimestampEntry);
|
||||
}
|
||||
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "input",
|
||||
latency: protoTracker,
|
||||
} as ProtoMessageBase,
|
||||
data: data,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
const message = createMessage(data, "input");
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,4 +147,4 @@ export class Mouse {
|
||||
private keyToVirtualKeyCode(code: number) {
|
||||
return mouseButtonToLinuxEventCode[code] || undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated from file latency_tracker.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated from file messages.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { ProtoInput } from "./types_pb";
|
||||
import type { ProtoClientDisconnected, ProtoClientRequestRoomStream, ProtoControllerAttach, ProtoControllerDetach, ProtoControllerRumble, ProtoControllerStateBatch, ProtoICE, ProtoKeyDown, ProtoKeyUp, ProtoMouseKeyDown, ProtoMouseKeyUp, ProtoMouseMove, ProtoMouseMoveAbs, ProtoMouseWheel, ProtoRaw, ProtoSDP, ProtoServerPushStream } from "./types_pb";
|
||||
import { file_types } from "./types_pb";
|
||||
import type { ProtoLatencyTracker } from "./latency_tracker_pb";
|
||||
import { file_latency_tracker } from "./latency_tracker_pb";
|
||||
@@ -14,7 +14,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file messages.proto.
|
||||
*/
|
||||
export const file_messages: GenFile = /*@__PURE__*/
|
||||
fileDesc("Cg5tZXNzYWdlcy5wcm90bxIFcHJvdG8iVQoQUHJvdG9NZXNzYWdlQmFzZRIUCgxwYXlsb2FkX3R5cGUYASABKAkSKwoHbGF0ZW5jeRgCIAEoCzIaLnByb3RvLlByb3RvTGF0ZW5jeVRyYWNrZXIiYwoRUHJvdG9NZXNzYWdlSW5wdXQSLQoMbWVzc2FnZV9iYXNlGAEgASgLMhcucHJvdG8uUHJvdG9NZXNzYWdlQmFzZRIfCgRkYXRhGAIgASgLMhEucHJvdG8uUHJvdG9JbnB1dEIWWhRyZWxheS9pbnRlcm5hbC9wcm90b2IGcHJvdG8z", [file_types, file_latency_tracker]);
|
||||
fileDesc("Cg5tZXNzYWdlcy5wcm90bxIFcHJvdG8iVQoQUHJvdG9NZXNzYWdlQmFzZRIUCgxwYXlsb2FkX3R5cGUYASABKAkSKwoHbGF0ZW5jeRgCIAEoCzIaLnByb3RvLlByb3RvTGF0ZW5jeVRyYWNrZXIipQcKDFByb3RvTWVzc2FnZRItCgxtZXNzYWdlX2Jhc2UYASABKAsyFy5wcm90by5Qcm90b01lc3NhZ2VCYXNlEisKCm1vdXNlX21vdmUYAiABKAsyFS5wcm90by5Qcm90b01vdXNlTW92ZUgAEjIKDm1vdXNlX21vdmVfYWJzGAMgASgLMhgucHJvdG8uUHJvdG9Nb3VzZU1vdmVBYnNIABItCgttb3VzZV93aGVlbBgEIAEoCzIWLnByb3RvLlByb3RvTW91c2VXaGVlbEgAEjIKDm1vdXNlX2tleV9kb3duGAUgASgLMhgucHJvdG8uUHJvdG9Nb3VzZUtleURvd25IABIuCgxtb3VzZV9rZXlfdXAYBiABKAsyFi5wcm90by5Qcm90b01vdXNlS2V5VXBIABInCghrZXlfZG93bhgHIAEoCzITLnByb3RvLlByb3RvS2V5RG93bkgAEiMKBmtleV91cBgIIAEoCzIRLnByb3RvLlByb3RvS2V5VXBIABI5ChFjb250cm9sbGVyX2F0dGFjaBgJIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckF0dGFjaEgAEjkKEWNvbnRyb2xsZXJfZGV0YWNoGAogASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyRGV0YWNoSAASOQoRY29udHJvbGxlcl9ydW1ibGUYCyABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIABJCChZjb250cm9sbGVyX3N0YXRlX2JhdGNoGAwgASgLMiAucHJvdG8uUHJvdG9Db250cm9sbGVyU3RhdGVCYXRjaEgAEh4KA2ljZRgUIAEoCzIPLnByb3RvLlByb3RvSUNFSAASHgoDc2RwGBUgASgLMg8ucHJvdG8uUHJvdG9TRFBIABIeCgNyYXcYFiABKAsyDy5wcm90by5Qcm90b1Jhd0gAEkkKGmNsaWVudF9yZXF1ZXN0X3Jvb21fc3RyZWFtGBcgASgLMiMucHJvdG8uUHJvdG9DbGllbnRSZXF1ZXN0Um9vbVN0cmVhbUgAEj0KE2NsaWVudF9kaXNjb25uZWN0ZWQYGCABKAsyHi5wcm90by5Qcm90b0NsaWVudERpc2Nvbm5lY3RlZEgAEjoKEnNlcnZlcl9wdXNoX3N0cmVhbRgZIAEoCzIcLnByb3RvLlByb3RvU2VydmVyUHVzaFN0cmVhbUgAQgkKB3BheWxvYWRCFloUcmVsYXkvaW50ZXJuYWwvcHJvdG9iBnByb3RvMw", [file_types, file_latency_tracker]);
|
||||
|
||||
/**
|
||||
* @generated from message proto.ProtoMessageBase
|
||||
@@ -39,24 +39,132 @@ export const ProtoMessageBaseSchema: GenMessage<ProtoMessageBase> = /*@__PURE__*
|
||||
messageDesc(file_messages, 0);
|
||||
|
||||
/**
|
||||
* @generated from message proto.ProtoMessageInput
|
||||
* @generated from message proto.ProtoMessage
|
||||
*/
|
||||
export type ProtoMessageInput = Message<"proto.ProtoMessageInput"> & {
|
||||
export type ProtoMessage = Message<"proto.ProtoMessage"> & {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMessageBase message_base = 1;
|
||||
*/
|
||||
messageBase?: ProtoMessageBase;
|
||||
|
||||
/**
|
||||
* @generated from field: proto.ProtoInput data = 2;
|
||||
* @generated from oneof proto.ProtoMessage.payload
|
||||
*/
|
||||
data?: ProtoInput;
|
||||
payload: {
|
||||
/**
|
||||
* Input types
|
||||
*
|
||||
* @generated from field: proto.ProtoMouseMove mouse_move = 2;
|
||||
*/
|
||||
value: ProtoMouseMove;
|
||||
case: "mouseMove";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseMoveAbs mouse_move_abs = 3;
|
||||
*/
|
||||
value: ProtoMouseMoveAbs;
|
||||
case: "mouseMoveAbs";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseWheel mouse_wheel = 4;
|
||||
*/
|
||||
value: ProtoMouseWheel;
|
||||
case: "mouseWheel";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseKeyDown mouse_key_down = 5;
|
||||
*/
|
||||
value: ProtoMouseKeyDown;
|
||||
case: "mouseKeyDown";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseKeyUp mouse_key_up = 6;
|
||||
*/
|
||||
value: ProtoMouseKeyUp;
|
||||
case: "mouseKeyUp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoKeyDown key_down = 7;
|
||||
*/
|
||||
value: ProtoKeyDown;
|
||||
case: "keyDown";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoKeyUp key_up = 8;
|
||||
*/
|
||||
value: ProtoKeyUp;
|
||||
case: "keyUp";
|
||||
} | {
|
||||
/**
|
||||
* Controller input types
|
||||
*
|
||||
* @generated from field: proto.ProtoControllerAttach controller_attach = 9;
|
||||
*/
|
||||
value: ProtoControllerAttach;
|
||||
case: "controllerAttach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerDetach controller_detach = 10;
|
||||
*/
|
||||
value: ProtoControllerDetach;
|
||||
case: "controllerDetach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerRumble controller_rumble = 11;
|
||||
*/
|
||||
value: ProtoControllerRumble;
|
||||
case: "controllerRumble";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerStateBatch controller_state_batch = 12;
|
||||
*/
|
||||
value: ProtoControllerStateBatch;
|
||||
case: "controllerStateBatch";
|
||||
} | {
|
||||
/**
|
||||
* Signaling types
|
||||
*
|
||||
* @generated from field: proto.ProtoICE ice = 20;
|
||||
*/
|
||||
value: ProtoICE;
|
||||
case: "ice";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoSDP sdp = 21;
|
||||
*/
|
||||
value: ProtoSDP;
|
||||
case: "sdp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoRaw raw = 22;
|
||||
*/
|
||||
value: ProtoRaw;
|
||||
case: "raw";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoClientRequestRoomStream client_request_room_stream = 23;
|
||||
*/
|
||||
value: ProtoClientRequestRoomStream;
|
||||
case: "clientRequestRoomStream";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoClientDisconnected client_disconnected = 24;
|
||||
*/
|
||||
value: ProtoClientDisconnected;
|
||||
case: "clientDisconnected";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoServerPushStream server_push_stream = 25;
|
||||
*/
|
||||
value: ProtoServerPushStream;
|
||||
case: "serverPushStream";
|
||||
} | { case: undefined; value?: undefined };
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoMessageInput.
|
||||
* Use `create(ProtoMessageInputSchema)` to create a new message.
|
||||
* Describes the message proto.ProtoMessage.
|
||||
* Use `create(ProtoMessageSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoMessageInputSchema: GenMessage<ProtoMessageInput> = /*@__PURE__*/
|
||||
export const ProtoMessageSchema: GenMessage<ProtoMessage> = /*@__PURE__*/
|
||||
messageDesc(file_messages, 1);
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated from file types.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file types.proto.
|
||||
*/
|
||||
export const file_types: GenFile = /*@__PURE__*/
|
||||
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSI/ChVQcm90b0NvbnRyb2xsZXJBdHRhY2gSDAoEdHlwZRgBIAEoCRIKCgJpZBgCIAEoCRIMCgRzbG90GAMgASgFIjMKFVByb3RvQ29udHJvbGxlckRldGFjaBIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUiVAoVUHJvdG9Db250cm9sbGVyQnV0dG9uEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIOCgZidXR0b24YAyABKAUSDwoHcHJlc3NlZBgEIAEoCCJUChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIPCgd0cmlnZ2VyGAMgASgFEg0KBXZhbHVlGAQgASgFIlcKFFByb3RvQ29udHJvbGxlclN0aWNrEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRINCgVzdGljaxgDIAEoBRIJCgF4GAQgASgFEgkKAXkYBSABKAUiTgoTUHJvdG9Db250cm9sbGVyQXhpcxIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUSDAoEYXhpcxgDIAEoBRINCgV2YWx1ZRgEIAEoBSJ0ChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEdHlwZRgBIAEoCRIMCgRzbG90GAIgASgFEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi9QUKClByb3RvSW5wdXQSKwoKbW91c2VfbW92ZRgBIAEoCzIVLnByb3RvLlByb3RvTW91c2VNb3ZlSAASMgoObW91c2VfbW92ZV9hYnMYAiABKAsyGC5wcm90by5Qcm90b01vdXNlTW92ZUFic0gAEi0KC21vdXNlX3doZWVsGAMgASgLMhYucHJvdG8uUHJvdG9Nb3VzZVdoZWVsSAASMgoObW91c2Vfa2V5X2Rvd24YBCABKAsyGC5wcm90by5Qcm90b01vdXNlS2V5RG93bkgAEi4KDG1vdXNlX2tleV91cBgFIAEoCzIWLnByb3RvLlByb3RvTW91c2VLZXlVcEgAEicKCGtleV9kb3duGAYgASgLMhMucHJvdG8uUHJvdG9LZXlEb3duSAASIwoGa2V5X3VwGAcgASgLMhEucHJvdG8uUHJvdG9LZXlVcEgAEjkKEWNvbnRyb2xsZXJfYXR0YWNoGAggASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyQXR0YWNoSAASOQoRY29udHJvbGxlcl9kZXRhY2gYCSABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJEZXRhY2hIABI5ChFjb250cm9sbGVyX2J1dHRvbhgKIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckJ1dHRvbkgAEjsKEmNvbnRyb2xsZXJfdHJpZ2dlchgLIAEoCzIdLnByb3RvLlByb3RvQ29udHJvbGxlclRyaWdnZXJIABI3ChBjb250cm9sbGVyX3N0aWNrGAwgASgLMhsucHJvdG8uUHJvdG9Db250cm9sbGVyU3RpY2tIABI1Cg9jb250cm9sbGVyX2F4aXMYDSABKAsyGi5wcm90by5Qcm90b0NvbnRyb2xsZXJBeGlzSAASOQoRY29udHJvbGxlcl9ydW1ibGUYDiABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIAEIMCgppbnB1dF90eXBlQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM");
|
||||
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iJgoOUHJvdG9Nb3VzZU1vdmUSCQoBeBgBIAEoBRIJCgF5GAIgASgFIikKEVByb3RvTW91c2VNb3ZlQWJzEgkKAXgYASABKAUSCQoBeRgCIAEoBSInCg9Qcm90b01vdXNlV2hlZWwSCQoBeBgBIAEoBRIJCgF5GAIgASgFIiAKEVByb3RvTW91c2VLZXlEb3duEgsKA2tleRgBIAEoBSIeCg9Qcm90b01vdXNlS2V5VXASCwoDa2V5GAEgASgFIhsKDFByb3RvS2V5RG93bhILCgNrZXkYASABKAUiGQoKUHJvdG9LZXlVcBILCgNrZXkYASABKAUiTQoVUHJvdG9Db250cm9sbGVyQXR0YWNoEgoKAmlkGAEgASgJEhQKDHNlc3Npb25fc2xvdBgCIAEoBRISCgpzZXNzaW9uX2lkGAMgASgJIkEKFVByb3RvQ29udHJvbGxlckRldGFjaBIUCgxzZXNzaW9uX3Nsb3QYASABKAUSEgoKc2Vzc2lvbl9pZBgCIAEoCSKCAQoVUHJvdG9Db250cm9sbGVyUnVtYmxlEhQKDHNlc3Npb25fc2xvdBgBIAEoBRISCgpzZXNzaW9uX2lkGAIgASgJEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi0AUKGVByb3RvQ29udHJvbGxlclN0YXRlQmF0Y2gSFAoMc2Vzc2lvbl9zbG90GAEgASgFEhIKCnNlc3Npb25faWQYAiABKAkSQAoLdXBkYXRlX3R5cGUYAyABKA4yKy5wcm90by5Qcm90b0NvbnRyb2xsZXJTdGF0ZUJhdGNoLlVwZGF0ZVR5cGUSEAoIc2VxdWVuY2UYBCABKA0SVAoTYnV0dG9uX2NoYW5nZWRfbWFzaxgFIAMoCzI3LnByb3RvLlByb3RvQ29udHJvbGxlclN0YXRlQmF0Y2guQnV0dG9uQ2hhbmdlZE1hc2tFbnRyeRIZCgxsZWZ0X3N0aWNrX3gYBiABKAVIAIgBARIZCgxsZWZ0X3N0aWNrX3kYByABKAVIAYgBARIaCg1yaWdodF9zdGlja194GAggASgFSAKIAQESGgoNcmlnaHRfc3RpY2tfeRgJIAEoBUgDiAEBEhkKDGxlZnRfdHJpZ2dlchgKIAEoBUgEiAEBEhoKDXJpZ2h0X3RyaWdnZXIYCyABKAVIBYgBARITCgZkcGFkX3gYDCABKAVIBogBARITCgZkcGFkX3kYDSABKAVIB4gBARIbCg5jaGFuZ2VkX2ZpZWxkcxgOIAEoDUgIiAEBGjgKFkJ1dHRvbkNoYW5nZWRNYXNrRW50cnkSCwoDa2V5GAEgASgFEg0KBXZhbHVlGAIgASgIOgI4ASInCgpVcGRhdGVUeXBlEg4KCkZVTExfU1RBVEUQABIJCgVERUxUQRABQg8KDV9sZWZ0X3N0aWNrX3hCDwoNX2xlZnRfc3RpY2tfeUIQCg5fcmlnaHRfc3RpY2tfeEIQCg5fcmlnaHRfc3RpY2tfeUIPCg1fbGVmdF90cmlnZ2VyQhAKDl9yaWdodF90cmlnZ2VyQgkKB19kcGFkX3hCCQoHX2RwYWRfeUIRCg9fY2hhbmdlZF9maWVsZHMiqgEKE1JUQ0ljZUNhbmRpZGF0ZUluaXQSEQoJY2FuZGlkYXRlGAEgASgJEhoKDXNkcE1MaW5lSW5kZXgYAiABKA1IAIgBARITCgZzZHBNaWQYAyABKAlIAYgBARIdChB1c2VybmFtZUZyYWdtZW50GAQgASgJSAKIAQFCEAoOX3NkcE1MaW5lSW5kZXhCCQoHX3NkcE1pZEITChFfdXNlcm5hbWVGcmFnbWVudCI2ChlSVENTZXNzaW9uRGVzY3JpcHRpb25Jbml0EgsKA3NkcBgBIAEoCRIMCgR0eXBlGAIgASgJIjkKCFByb3RvSUNFEi0KCWNhbmRpZGF0ZRgBIAEoCzIaLnByb3RvLlJUQ0ljZUNhbmRpZGF0ZUluaXQiOQoIUHJvdG9TRFASLQoDc2RwGAEgASgLMiAucHJvdG8uUlRDU2Vzc2lvbkRlc2NyaXB0aW9uSW5pdCIYCghQcm90b1JhdxIMCgRkYXRhGAEgASgJIkUKHFByb3RvQ2xpZW50UmVxdWVzdFJvb21TdHJlYW0SEQoJcm9vbV9uYW1lGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkiRwoXUHJvdG9DbGllbnREaXNjb25uZWN0ZWQSEgoKc2Vzc2lvbl9pZBgBIAEoCRIYChBjb250cm9sbGVyX3Nsb3RzGAIgAygFIioKFVByb3RvU2VydmVyUHVzaFN0cmVhbRIRCglyb29tX25hbWUYASABKAlCFloUcmVsYXkvaW50ZXJuYWwvcHJvdG9iBnByb3RvMw");
|
||||
|
||||
/**
|
||||
* MouseMove message
|
||||
@@ -19,19 +19,12 @@ export const file_types: GenFile = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoMouseMove = Message<"proto.ProtoMouseMove"> & {
|
||||
/**
|
||||
* Fixed value "MouseMove"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 x = 2;
|
||||
* @generated from field: int32 x = 1;
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 y = 3;
|
||||
* @generated from field: int32 y = 2;
|
||||
*/
|
||||
y: number;
|
||||
};
|
||||
@@ -50,19 +43,12 @@ export const ProtoMouseMoveSchema: GenMessage<ProtoMouseMove> = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoMouseMoveAbs = Message<"proto.ProtoMouseMoveAbs"> & {
|
||||
/**
|
||||
* Fixed value "MouseMoveAbs"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 x = 2;
|
||||
* @generated from field: int32 x = 1;
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 y = 3;
|
||||
* @generated from field: int32 y = 2;
|
||||
*/
|
||||
y: number;
|
||||
};
|
||||
@@ -81,19 +67,12 @@ export const ProtoMouseMoveAbsSchema: GenMessage<ProtoMouseMoveAbs> = /*@__PURE_
|
||||
*/
|
||||
export type ProtoMouseWheel = Message<"proto.ProtoMouseWheel"> & {
|
||||
/**
|
||||
* Fixed value "MouseWheel"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 x = 2;
|
||||
* @generated from field: int32 x = 1;
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 y = 3;
|
||||
* @generated from field: int32 y = 2;
|
||||
*/
|
||||
y: number;
|
||||
};
|
||||
@@ -112,14 +91,7 @@ export const ProtoMouseWheelSchema: GenMessage<ProtoMouseWheel> = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoMouseKeyDown = Message<"proto.ProtoMouseKeyDown"> & {
|
||||
/**
|
||||
* Fixed value "MouseKeyDown"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 key = 2;
|
||||
* @generated from field: int32 key = 1;
|
||||
*/
|
||||
key: number;
|
||||
};
|
||||
@@ -138,14 +110,7 @@ export const ProtoMouseKeyDownSchema: GenMessage<ProtoMouseKeyDown> = /*@__PURE_
|
||||
*/
|
||||
export type ProtoMouseKeyUp = Message<"proto.ProtoMouseKeyUp"> & {
|
||||
/**
|
||||
* Fixed value "MouseKeyUp"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 key = 2;
|
||||
* @generated from field: int32 key = 1;
|
||||
*/
|
||||
key: number;
|
||||
};
|
||||
@@ -164,14 +129,7 @@ export const ProtoMouseKeyUpSchema: GenMessage<ProtoMouseKeyUp> = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoKeyDown = Message<"proto.ProtoKeyDown"> & {
|
||||
/**
|
||||
* Fixed value "KeyDown"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 key = 2;
|
||||
* @generated from field: int32 key = 1;
|
||||
*/
|
||||
key: number;
|
||||
};
|
||||
@@ -190,14 +148,7 @@ export const ProtoKeyDownSchema: GenMessage<ProtoKeyDown> = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoKeyUp = Message<"proto.ProtoKeyUp"> & {
|
||||
/**
|
||||
* Fixed value "KeyUp"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 key = 2;
|
||||
* @generated from field: int32 key = 1;
|
||||
*/
|
||||
key: number;
|
||||
};
|
||||
@@ -215,26 +166,26 @@ export const ProtoKeyUpSchema: GenMessage<ProtoKeyUp> = /*@__PURE__*/
|
||||
* @generated from message proto.ProtoControllerAttach
|
||||
*/
|
||||
export type ProtoControllerAttach = Message<"proto.ProtoControllerAttach"> & {
|
||||
/**
|
||||
* Fixed value "ControllerAttach"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* One of the following enums: "ps", "xbox" or "switch"
|
||||
*
|
||||
* @generated from field: string id = 2;
|
||||
* @generated from field: string id = 1;
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
* Session specific slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 3;
|
||||
* @generated from field: int32 session_slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
sessionSlot: number;
|
||||
|
||||
/**
|
||||
* Session ID of the client
|
||||
*
|
||||
* @generated from field: string session_id = 3;
|
||||
*/
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -251,18 +202,18 @@ export const ProtoControllerAttachSchema: GenMessage<ProtoControllerAttach> = /*
|
||||
*/
|
||||
export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & {
|
||||
/**
|
||||
* Fixed value "ControllerDetach"
|
||||
* Session specific slot number (0-3)
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
* @generated from field: int32 session_slot = 1;
|
||||
*/
|
||||
type: string;
|
||||
sessionSlot: number;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
* Session ID of the client
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
* @generated from field: string session_id = 2;
|
||||
*/
|
||||
slot: number;
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -272,181 +223,6 @@ export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & {
|
||||
export const ProtoControllerDetachSchema: GenMessage<ProtoControllerDetach> = /*@__PURE__*/
|
||||
messageDesc(file_types, 8);
|
||||
|
||||
/**
|
||||
* ControllerButton message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerButton
|
||||
*/
|
||||
export type ProtoControllerButton = Message<"proto.ProtoControllerButton"> & {
|
||||
/**
|
||||
* Fixed value "ControllerButtons"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Button code (linux input event code)
|
||||
*
|
||||
* @generated from field: int32 button = 3;
|
||||
*/
|
||||
button: number;
|
||||
|
||||
/**
|
||||
* true if pressed, false if released
|
||||
*
|
||||
* @generated from field: bool pressed = 4;
|
||||
*/
|
||||
pressed: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerButton.
|
||||
* Use `create(ProtoControllerButtonSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerButtonSchema: GenMessage<ProtoControllerButton> = /*@__PURE__*/
|
||||
messageDesc(file_types, 9);
|
||||
|
||||
/**
|
||||
* ControllerTriggers message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerTrigger
|
||||
*/
|
||||
export type ProtoControllerTrigger = Message<"proto.ProtoControllerTrigger"> & {
|
||||
/**
|
||||
* Fixed value "ControllerTriggers"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Trigger number (0 for left, 1 for right)
|
||||
*
|
||||
* @generated from field: int32 trigger = 3;
|
||||
*/
|
||||
trigger: number;
|
||||
|
||||
/**
|
||||
* trigger value (-32768 to 32767)
|
||||
*
|
||||
* @generated from field: int32 value = 4;
|
||||
*/
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerTrigger.
|
||||
* Use `create(ProtoControllerTriggerSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerTriggerSchema: GenMessage<ProtoControllerTrigger> = /*@__PURE__*/
|
||||
messageDesc(file_types, 10);
|
||||
|
||||
/**
|
||||
* ControllerSticks message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerStick
|
||||
*/
|
||||
export type ProtoControllerStick = Message<"proto.ProtoControllerStick"> & {
|
||||
/**
|
||||
* Fixed value "ControllerStick"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Stick number (0 for left, 1 for right)
|
||||
*
|
||||
* @generated from field: int32 stick = 3;
|
||||
*/
|
||||
stick: number;
|
||||
|
||||
/**
|
||||
* X axis value (-32768 to 32767)
|
||||
*
|
||||
* @generated from field: int32 x = 4;
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* Y axis value (-32768 to 32767)
|
||||
*
|
||||
* @generated from field: int32 y = 5;
|
||||
*/
|
||||
y: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerStick.
|
||||
* Use `create(ProtoControllerStickSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerStickSchema: GenMessage<ProtoControllerStick> = /*@__PURE__*/
|
||||
messageDesc(file_types, 11);
|
||||
|
||||
/**
|
||||
* ControllerAxis message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerAxis
|
||||
*/
|
||||
export type ProtoControllerAxis = Message<"proto.ProtoControllerAxis"> & {
|
||||
/**
|
||||
* Fixed value "ControllerAxis"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
|
||||
*
|
||||
* @generated from field: int32 axis = 3;
|
||||
*/
|
||||
axis: number;
|
||||
|
||||
/**
|
||||
* axis value (-1 to 1)
|
||||
*
|
||||
* @generated from field: int32 value = 4;
|
||||
*/
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerAxis.
|
||||
* Use `create(ProtoControllerAxisSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerAxisSchema: GenMessage<ProtoControllerAxis> = /*@__PURE__*/
|
||||
messageDesc(file_types, 12);
|
||||
|
||||
/**
|
||||
* ControllerRumble message
|
||||
*
|
||||
@@ -454,18 +230,18 @@ export const ProtoControllerAxisSchema: GenMessage<ProtoControllerAxis> = /*@__P
|
||||
*/
|
||||
export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & {
|
||||
/**
|
||||
* Fixed value "ControllerRumble"
|
||||
* Session specific slot number (0-3)
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
* @generated from field: int32 session_slot = 1;
|
||||
*/
|
||||
type: string;
|
||||
sessionSlot: number;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
* Session ID of the client
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
* @generated from field: string session_id = 2;
|
||||
*/
|
||||
slot: number;
|
||||
sessionId: string;
|
||||
|
||||
/**
|
||||
* Low frequency rumble (0-65535)
|
||||
@@ -494,108 +270,321 @@ export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & {
|
||||
* Use `create(ProtoControllerRumbleSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerRumbleSchema: GenMessage<ProtoControllerRumble> = /*@__PURE__*/
|
||||
messageDesc(file_types, 13);
|
||||
messageDesc(file_types, 9);
|
||||
|
||||
/**
|
||||
* Union of all Input types
|
||||
* ControllerStateBatch - single message containing full or partial controller state
|
||||
*
|
||||
* @generated from message proto.ProtoInput
|
||||
* @generated from message proto.ProtoControllerStateBatch
|
||||
*/
|
||||
export type ProtoInput = Message<"proto.ProtoInput"> & {
|
||||
export type ProtoControllerStateBatch = Message<"proto.ProtoControllerStateBatch"> & {
|
||||
/**
|
||||
* @generated from oneof proto.ProtoInput.input_type
|
||||
* Session specific slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 session_slot = 1;
|
||||
*/
|
||||
inputType: {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseMove mouse_move = 1;
|
||||
*/
|
||||
value: ProtoMouseMove;
|
||||
case: "mouseMove";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseMoveAbs mouse_move_abs = 2;
|
||||
*/
|
||||
value: ProtoMouseMoveAbs;
|
||||
case: "mouseMoveAbs";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseWheel mouse_wheel = 3;
|
||||
*/
|
||||
value: ProtoMouseWheel;
|
||||
case: "mouseWheel";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseKeyDown mouse_key_down = 4;
|
||||
*/
|
||||
value: ProtoMouseKeyDown;
|
||||
case: "mouseKeyDown";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseKeyUp mouse_key_up = 5;
|
||||
*/
|
||||
value: ProtoMouseKeyUp;
|
||||
case: "mouseKeyUp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoKeyDown key_down = 6;
|
||||
*/
|
||||
value: ProtoKeyDown;
|
||||
case: "keyDown";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoKeyUp key_up = 7;
|
||||
*/
|
||||
value: ProtoKeyUp;
|
||||
case: "keyUp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerAttach controller_attach = 8;
|
||||
*/
|
||||
value: ProtoControllerAttach;
|
||||
case: "controllerAttach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerDetach controller_detach = 9;
|
||||
*/
|
||||
value: ProtoControllerDetach;
|
||||
case: "controllerDetach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerButton controller_button = 10;
|
||||
*/
|
||||
value: ProtoControllerButton;
|
||||
case: "controllerButton";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerTrigger controller_trigger = 11;
|
||||
*/
|
||||
value: ProtoControllerTrigger;
|
||||
case: "controllerTrigger";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerStick controller_stick = 12;
|
||||
*/
|
||||
value: ProtoControllerStick;
|
||||
case: "controllerStick";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerAxis controller_axis = 13;
|
||||
*/
|
||||
value: ProtoControllerAxis;
|
||||
case: "controllerAxis";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerRumble controller_rumble = 14;
|
||||
*/
|
||||
value: ProtoControllerRumble;
|
||||
case: "controllerRumble";
|
||||
} | { case: undefined; value?: undefined };
|
||||
sessionSlot: number;
|
||||
|
||||
/**
|
||||
* Session ID of the client
|
||||
*
|
||||
* @generated from field: string session_id = 2;
|
||||
*/
|
||||
sessionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerStateBatch.UpdateType update_type = 3;
|
||||
*/
|
||||
updateType: ProtoControllerStateBatch_UpdateType;
|
||||
|
||||
/**
|
||||
* Sequence number for packet loss detection
|
||||
*
|
||||
* @generated from field: uint32 sequence = 4;
|
||||
*/
|
||||
sequence: number;
|
||||
|
||||
/**
|
||||
* Button state map (Linux event codes)
|
||||
*
|
||||
* @generated from field: map<int32, bool> button_changed_mask = 5;
|
||||
*/
|
||||
buttonChangedMask: { [key: number]: boolean };
|
||||
|
||||
/**
|
||||
* Analog inputs
|
||||
*
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 left_stick_x = 6;
|
||||
*/
|
||||
leftStickX?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 left_stick_y = 7;
|
||||
*/
|
||||
leftStickY?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 right_stick_x = 8;
|
||||
*/
|
||||
rightStickX?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 right_stick_y = 9;
|
||||
*/
|
||||
rightStickY?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 left_trigger = 10;
|
||||
*/
|
||||
leftTrigger?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 right_trigger = 11;
|
||||
*/
|
||||
rightTrigger?: number;
|
||||
|
||||
/**
|
||||
* -1, 0, or 1
|
||||
*
|
||||
* @generated from field: optional int32 dpad_x = 12;
|
||||
*/
|
||||
dpadX?: number;
|
||||
|
||||
/**
|
||||
* -1, 0, or 1
|
||||
*
|
||||
* @generated from field: optional int32 dpad_y = 13;
|
||||
*/
|
||||
dpadY?: number;
|
||||
|
||||
/**
|
||||
* Bitmask indicating which fields have changed
|
||||
* Bit 0: button_changed_mask, Bit 1: left_stick_x, Bit 2: left_stick_y, etc.
|
||||
*
|
||||
* @generated from field: optional uint32 changed_fields = 14;
|
||||
*/
|
||||
changedFields?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoInput.
|
||||
* Use `create(ProtoInputSchema)` to create a new message.
|
||||
* Describes the message proto.ProtoControllerStateBatch.
|
||||
* Use `create(ProtoControllerStateBatchSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoInputSchema: GenMessage<ProtoInput> = /*@__PURE__*/
|
||||
export const ProtoControllerStateBatchSchema: GenMessage<ProtoControllerStateBatch> = /*@__PURE__*/
|
||||
messageDesc(file_types, 10);
|
||||
|
||||
/**
|
||||
* @generated from enum proto.ProtoControllerStateBatch.UpdateType
|
||||
*/
|
||||
export enum ProtoControllerStateBatch_UpdateType {
|
||||
/**
|
||||
* Complete controller state
|
||||
*
|
||||
* @generated from enum value: FULL_STATE = 0;
|
||||
*/
|
||||
FULL_STATE = 0,
|
||||
|
||||
/**
|
||||
* Only changed fields
|
||||
*
|
||||
* @generated from enum value: DELTA = 1;
|
||||
*/
|
||||
DELTA = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum proto.ProtoControllerStateBatch.UpdateType.
|
||||
*/
|
||||
export const ProtoControllerStateBatch_UpdateTypeSchema: GenEnum<ProtoControllerStateBatch_UpdateType> = /*@__PURE__*/
|
||||
enumDesc(file_types, 10, 0);
|
||||
|
||||
/**
|
||||
* @generated from message proto.RTCIceCandidateInit
|
||||
*/
|
||||
export type RTCIceCandidateInit = Message<"proto.RTCIceCandidateInit"> & {
|
||||
/**
|
||||
* @generated from field: string candidate = 1;
|
||||
*/
|
||||
candidate: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional uint32 sdpMLineIndex = 2;
|
||||
*/
|
||||
sdpMLineIndex?: number;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string sdpMid = 3;
|
||||
*/
|
||||
sdpMid?: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string usernameFragment = 4;
|
||||
*/
|
||||
usernameFragment?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.RTCIceCandidateInit.
|
||||
* Use `create(RTCIceCandidateInitSchema)` to create a new message.
|
||||
*/
|
||||
export const RTCIceCandidateInitSchema: GenMessage<RTCIceCandidateInit> = /*@__PURE__*/
|
||||
messageDesc(file_types, 11);
|
||||
|
||||
/**
|
||||
* @generated from message proto.RTCSessionDescriptionInit
|
||||
*/
|
||||
export type RTCSessionDescriptionInit = Message<"proto.RTCSessionDescriptionInit"> & {
|
||||
/**
|
||||
* @generated from field: string sdp = 1;
|
||||
*/
|
||||
sdp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string type = 2;
|
||||
*/
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.RTCSessionDescriptionInit.
|
||||
* Use `create(RTCSessionDescriptionInitSchema)` to create a new message.
|
||||
*/
|
||||
export const RTCSessionDescriptionInitSchema: GenMessage<RTCSessionDescriptionInit> = /*@__PURE__*/
|
||||
messageDesc(file_types, 12);
|
||||
|
||||
/**
|
||||
* ProtoICE message
|
||||
*
|
||||
* @generated from message proto.ProtoICE
|
||||
*/
|
||||
export type ProtoICE = Message<"proto.ProtoICE"> & {
|
||||
/**
|
||||
* @generated from field: proto.RTCIceCandidateInit candidate = 1;
|
||||
*/
|
||||
candidate?: RTCIceCandidateInit;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoICE.
|
||||
* Use `create(ProtoICESchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoICESchema: GenMessage<ProtoICE> = /*@__PURE__*/
|
||||
messageDesc(file_types, 13);
|
||||
|
||||
/**
|
||||
* ProtoSDP message
|
||||
*
|
||||
* @generated from message proto.ProtoSDP
|
||||
*/
|
||||
export type ProtoSDP = Message<"proto.ProtoSDP"> & {
|
||||
/**
|
||||
* @generated from field: proto.RTCSessionDescriptionInit sdp = 1;
|
||||
*/
|
||||
sdp?: RTCSessionDescriptionInit;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoSDP.
|
||||
* Use `create(ProtoSDPSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoSDPSchema: GenMessage<ProtoSDP> = /*@__PURE__*/
|
||||
messageDesc(file_types, 14);
|
||||
|
||||
/**
|
||||
* ProtoRaw message
|
||||
*
|
||||
* @generated from message proto.ProtoRaw
|
||||
*/
|
||||
export type ProtoRaw = Message<"proto.ProtoRaw"> & {
|
||||
/**
|
||||
* @generated from field: string data = 1;
|
||||
*/
|
||||
data: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoRaw.
|
||||
* Use `create(ProtoRawSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoRawSchema: GenMessage<ProtoRaw> = /*@__PURE__*/
|
||||
messageDesc(file_types, 15);
|
||||
|
||||
/**
|
||||
* ProtoClientRequestRoomStream message
|
||||
*
|
||||
* @generated from message proto.ProtoClientRequestRoomStream
|
||||
*/
|
||||
export type ProtoClientRequestRoomStream = Message<"proto.ProtoClientRequestRoomStream"> & {
|
||||
/**
|
||||
* @generated from field: string room_name = 1;
|
||||
*/
|
||||
roomName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string session_id = 2;
|
||||
*/
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoClientRequestRoomStream.
|
||||
* Use `create(ProtoClientRequestRoomStreamSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoClientRequestRoomStreamSchema: GenMessage<ProtoClientRequestRoomStream> = /*@__PURE__*/
|
||||
messageDesc(file_types, 16);
|
||||
|
||||
/**
|
||||
* ProtoClientDisconnected message
|
||||
*
|
||||
* @generated from message proto.ProtoClientDisconnected
|
||||
*/
|
||||
export type ProtoClientDisconnected = Message<"proto.ProtoClientDisconnected"> & {
|
||||
/**
|
||||
* @generated from field: string session_id = 1;
|
||||
*/
|
||||
sessionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated int32 controller_slots = 2;
|
||||
*/
|
||||
controllerSlots: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoClientDisconnected.
|
||||
* Use `create(ProtoClientDisconnectedSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoClientDisconnectedSchema: GenMessage<ProtoClientDisconnected> = /*@__PURE__*/
|
||||
messageDesc(file_types, 17);
|
||||
|
||||
/**
|
||||
* ProtoServerPushStream message
|
||||
*
|
||||
* @generated from message proto.ProtoServerPushStream
|
||||
*/
|
||||
export type ProtoServerPushStream = Message<"proto.ProtoServerPushStream"> & {
|
||||
/**
|
||||
* @generated from field: string room_name = 1;
|
||||
*/
|
||||
roomName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoServerPushStream.
|
||||
* Use `create(ProtoServerPushStreamSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoServerPushStreamSchema: GenMessage<ProtoServerPushStream> = /*@__PURE__*/
|
||||
messageDesc(file_types, 18);
|
||||
|
||||
|
||||
81
packages/input/src/streamwrapper.ts
Normal file
81
packages/input/src/streamwrapper.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { pbStream, type ProtobufStream } from "@libp2p/utils";
|
||||
import type { Stream } from "@libp2p/interface";
|
||||
import { bufbuildAdapter } from "./utils";
|
||||
import {
|
||||
ProtoMessage,
|
||||
ProtoMessageSchema,
|
||||
ProtoMessageBase,
|
||||
} from "./proto/messages_pb";
|
||||
|
||||
type MessageHandler = (
|
||||
data: any,
|
||||
base: ProtoMessageBase,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export class P2PMessageStream {
|
||||
private pb: ProtobufStream;
|
||||
private handlers = new Map<string, MessageHandler[]>();
|
||||
private closed = false;
|
||||
private readLoopRunning = false;
|
||||
|
||||
constructor(stream: Stream) {
|
||||
this.pb = pbStream(stream);
|
||||
}
|
||||
|
||||
public on(payloadType: string, handler: MessageHandler): void {
|
||||
if (!this.handlers.has(payloadType)) {
|
||||
this.handlers.set(payloadType, []);
|
||||
}
|
||||
this.handlers.get(payloadType)!.push(handler);
|
||||
|
||||
if (!this.readLoopRunning) this.startReading().catch(console.error);
|
||||
}
|
||||
|
||||
private async startReading(): Promise<void> {
|
||||
if (this.readLoopRunning || this.closed) return;
|
||||
this.readLoopRunning = true;
|
||||
|
||||
while (!this.closed) {
|
||||
try {
|
||||
const msg: ProtoMessage = await this.pb.read(
|
||||
bufbuildAdapter(ProtoMessageSchema),
|
||||
);
|
||||
|
||||
const payloadType = msg.messageBase?.payloadType;
|
||||
if (payloadType && this.handlers.has(payloadType)) {
|
||||
const handlers = this.handlers.get(payloadType)!;
|
||||
if (msg.payload.value) {
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
await handler(msg.payload.value, msg.messageBase);
|
||||
} catch (err) {
|
||||
console.error(`Error in handler for ${payloadType}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.closed) break;
|
||||
console.error("Stream read error:", err);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
this.readLoopRunning = false;
|
||||
}
|
||||
|
||||
public async write(
|
||||
message: ProtoMessage,
|
||||
options?: { signal?: AbortSignal },
|
||||
): Promise<void> {
|
||||
if (this.closed)
|
||||
throw new Error("Cannot write to closed stream");
|
||||
|
||||
await this.pb.write(message, bufbuildAdapter(ProtoMessageSchema), options);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.closed = true;
|
||||
this.handlers.clear();
|
||||
}
|
||||
}
|
||||
95
packages/input/src/utils.ts
Normal file
95
packages/input/src/utils.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { create, toBinary, fromBinary } from "@bufbuild/protobuf";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
import { Uint8ArrayList } from "uint8arraylist";
|
||||
import type { GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
||||
import {
|
||||
ProtoLatencyTracker,
|
||||
ProtoLatencyTrackerSchema,
|
||||
ProtoTimestampEntrySchema,
|
||||
} from "./proto/latency_tracker_pb";
|
||||
import {
|
||||
ProtoMessage,
|
||||
ProtoMessageSchema,
|
||||
ProtoMessageBaseSchema,
|
||||
} from "./proto/messages_pb";
|
||||
|
||||
export function bufbuildAdapter<T extends Message>(schema: GenMessage<T>) {
|
||||
return {
|
||||
encode: (data: T): Uint8Array => {
|
||||
return toBinary(schema, data);
|
||||
},
|
||||
decode: (data: Uint8Array | Uint8ArrayList): T => {
|
||||
// Convert Uint8ArrayList to Uint8Array if needed
|
||||
const bytes = data instanceof Uint8ArrayList ? data.subarray() : data;
|
||||
return fromBinary(schema, bytes);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Latency tracker helpers
|
||||
export function createLatencyTracker(sequenceId?: string): ProtoLatencyTracker {
|
||||
return create(ProtoLatencyTrackerSchema, {
|
||||
sequenceId: sequenceId || crypto.randomUUID(),
|
||||
timestamps: [],
|
||||
});
|
||||
}
|
||||
|
||||
export function addLatencyTimestamp(
|
||||
tracker: ProtoLatencyTracker,
|
||||
stage: string,
|
||||
): ProtoLatencyTracker {
|
||||
const entry = create(ProtoTimestampEntrySchema, {
|
||||
stage,
|
||||
time: timestampFromDate(new Date()),
|
||||
});
|
||||
|
||||
return {
|
||||
...tracker,
|
||||
timestamps: [...tracker.timestamps, entry],
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateMessageOptions {
|
||||
sequenceId?: string;
|
||||
}
|
||||
|
||||
function derivePayloadCase(data: Message): string {
|
||||
// Extract case from $typeName: "proto.ProtoICE" -> "ice"
|
||||
// "proto.ProtoControllerAttach" -> "controllerAttach"
|
||||
const typeName = data.$typeName;
|
||||
if (!typeName)
|
||||
throw new Error("Message has no $typeName");
|
||||
|
||||
// Remove "proto.Proto" prefix and convert first char to lowercase
|
||||
const caseName = typeName.replace(/^proto\.Proto/, "");
|
||||
|
||||
// Convert PascalCase to camelCase
|
||||
// If it's all caps (like SDP, ICE), lowercase everything
|
||||
// Otherwise, just lowercase the first character
|
||||
if (caseName === caseName.toUpperCase()) {
|
||||
return caseName.toLowerCase();
|
||||
}
|
||||
return caseName.charAt(0).toLowerCase() + caseName.slice(1);
|
||||
}
|
||||
|
||||
export function createMessage(
|
||||
data: Message,
|
||||
payloadType: string,
|
||||
options?: CreateMessageOptions,
|
||||
): ProtoMessage {
|
||||
const payloadCase = derivePayloadCase(data);
|
||||
|
||||
return create(ProtoMessageSchema, {
|
||||
messageBase: create(ProtoMessageBaseSchema, {
|
||||
payloadType,
|
||||
latency: options?.sequenceId
|
||||
? createLatencyTracker(options.sequenceId)
|
||||
: undefined,
|
||||
}),
|
||||
payload: {
|
||||
case: payloadCase,
|
||||
value: data,
|
||||
} as any, // Type assertion needed for dynamic case
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
import {
|
||||
NewMessageRaw,
|
||||
NewMessageSDP,
|
||||
NewMessageICE,
|
||||
SafeStream,
|
||||
} from "./messages";
|
||||
import { webSockets } from "@libp2p/websockets";
|
||||
import { webTransport } from "@libp2p/webtransport";
|
||||
import { createLibp2p, Libp2p } from "libp2p";
|
||||
@@ -13,19 +7,33 @@ import { identify } from "@libp2p/identify";
|
||||
import { multiaddr } from "@multiformats/multiaddr";
|
||||
import { Connection } from "@libp2p/interface";
|
||||
import { ping } from "@libp2p/ping";
|
||||
import { createMessage } from "./utils";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import {
|
||||
ProtoClientRequestRoomStream,
|
||||
ProtoClientRequestRoomStreamSchema,
|
||||
ProtoICE,
|
||||
ProtoICESchema, ProtoRaw,
|
||||
ProtoSDP,
|
||||
ProtoSDPSchema
|
||||
} from "./proto/types_pb";
|
||||
import { P2PMessageStream } from "./streamwrapper";
|
||||
|
||||
const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0";
|
||||
|
||||
export class WebRTCStream {
|
||||
private _sessionId: string | null = null;
|
||||
private _p2p: Libp2p | undefined = undefined;
|
||||
private _p2pConn: Connection | undefined = undefined;
|
||||
private _p2pSafeStream: SafeStream | undefined = undefined;
|
||||
private _msgStream: P2PMessageStream | undefined = undefined;
|
||||
private _pc: RTCPeerConnection | undefined = undefined;
|
||||
private _audioTrack: MediaStreamTrack | undefined = undefined;
|
||||
private _videoTrack: MediaStreamTrack | undefined = undefined;
|
||||
private _dataChannel: RTCDataChannel | undefined = undefined;
|
||||
private _onConnected: ((stream: MediaStream | null) => void) | undefined = undefined;
|
||||
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined;
|
||||
private _onConnected: ((stream: MediaStream | null) => void) | undefined =
|
||||
undefined;
|
||||
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined =
|
||||
undefined;
|
||||
private _serverURL: string | undefined = undefined;
|
||||
private _roomName: string | undefined = undefined;
|
||||
private _isConnected: boolean = false;
|
||||
@@ -89,14 +97,20 @@ export class WebRTCStream {
|
||||
.newStream(NESTRI_PROTOCOL_STREAM_REQUEST)
|
||||
.catch(console.error);
|
||||
if (stream) {
|
||||
this._p2pSafeStream = new SafeStream(stream);
|
||||
this._msgStream = new P2PMessageStream(stream);
|
||||
console.log("Stream opened with peer");
|
||||
|
||||
let iceHolder: RTCIceCandidateInit[] = [];
|
||||
this._p2pSafeStream.registerCallback("ice-candidate", (data) => {
|
||||
this._msgStream.on("ice-candidate", (data: ProtoICE) => {
|
||||
const cand: RTCIceCandidateInit = {
|
||||
candidate: data.candidate.candidate,
|
||||
sdpMLineIndex: data.candidate.sdpMLineIndex,
|
||||
sdpMid: data.candidate.sdpMid,
|
||||
usernameFragment: data.candidate.usernameFragment,
|
||||
};
|
||||
if (this._pc) {
|
||||
if (this._pc.remoteDescription) {
|
||||
this._pc.addIceCandidate(data.candidate).catch((err) => {
|
||||
this._pc.addIceCandidate(cand).catch((err) => {
|
||||
console.error("Error adding ICE candidate:", err);
|
||||
});
|
||||
// Add held candidates
|
||||
@@ -107,45 +121,78 @@ export class WebRTCStream {
|
||||
});
|
||||
iceHolder = [];
|
||||
} else {
|
||||
iceHolder.push(data.candidate);
|
||||
iceHolder.push(cand);
|
||||
}
|
||||
} else {
|
||||
iceHolder.push(data.candidate);
|
||||
}
|
||||
});
|
||||
|
||||
this._p2pSafeStream.registerCallback("offer", async (data) => {
|
||||
this._msgStream.on("session-assigned", (data: ProtoClientRequestRoomStream) => {
|
||||
this._sessionId = data.sessionId;
|
||||
localStorage.setItem("nestri-session-id", this._sessionId);
|
||||
console.log("Session ID assigned:", this._sessionId, "for room:", data.roomName);
|
||||
});
|
||||
|
||||
this._msgStream.on("offer", async (data: ProtoSDP) => {
|
||||
if (!this._pc) {
|
||||
// Setup peer connection now
|
||||
this._setupPeerConnection();
|
||||
}
|
||||
await this._pc!.setRemoteDescription(data.sdp);
|
||||
await this._pc!.setRemoteDescription({
|
||||
sdp: data.sdp.sdp,
|
||||
type: data.sdp.type as RTCSdpType,
|
||||
});
|
||||
// Add held candidates
|
||||
iceHolder.forEach((candidate) => {
|
||||
this._pc!.addIceCandidate(candidate).catch((err) => {
|
||||
console.error("Error adding held ICE candidate:", err);
|
||||
});
|
||||
});
|
||||
iceHolder = [];
|
||||
|
||||
// Create our answer
|
||||
const answer = await this._pc!.createAnswer();
|
||||
// Force stereo in Chromium browsers
|
||||
answer.sdp = this.forceOpusStereo(answer.sdp!);
|
||||
await this._pc!.setLocalDescription(answer);
|
||||
// Send answer back
|
||||
const answerMsg = NewMessageSDP("answer", answer);
|
||||
await this._p2pSafeStream?.writeMessage(answerMsg);
|
||||
const answerMsg = createMessage(
|
||||
create(ProtoSDPSchema, {
|
||||
sdp: answer,
|
||||
}),
|
||||
"answer",
|
||||
);
|
||||
await this._msgStream?.write(answerMsg);
|
||||
});
|
||||
|
||||
this._p2pSafeStream.registerCallback("request-stream-offline", (data) => {
|
||||
console.warn("Stream is offline for room:", data.roomName);
|
||||
this._msgStream.on("request-stream-offline", (msg: ProtoRaw) => {
|
||||
console.warn("Stream is offline for room:", msg.data);
|
||||
this._onConnected?.(null);
|
||||
});
|
||||
|
||||
const clientId = this.getSessionID();
|
||||
if (clientId) {
|
||||
console.debug("Using existing session ID:", clientId);
|
||||
}
|
||||
|
||||
// Send stream request
|
||||
// marshal room name into json
|
||||
const request = NewMessageRaw(
|
||||
const requestMsg = createMessage(
|
||||
create(ProtoClientRequestRoomStreamSchema, {
|
||||
roomName: roomName,
|
||||
sessionId: clientId ?? "",
|
||||
}),
|
||||
"request-stream-room",
|
||||
roomName,
|
||||
);
|
||||
await this._p2pSafeStream.writeMessage(request);
|
||||
await this._msgStream.write(requestMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getSessionID(): string | null {
|
||||
if (this._sessionId === null)
|
||||
this._sessionId = localStorage.getItem("nestri-session-id");
|
||||
return this._sessionId;
|
||||
}
|
||||
|
||||
// Forces opus to stereo in Chromium browsers, because of course
|
||||
private forceOpusStereo(SDP: string): string {
|
||||
// Look for "minptime=10;useinbandfec=1" and replace with "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1;"
|
||||
@@ -200,11 +247,16 @@ export class WebRTCStream {
|
||||
|
||||
this._pc.onicecandidate = (e) => {
|
||||
if (e.candidate) {
|
||||
const iceMsg = NewMessageICE("ice-candidate", e.candidate);
|
||||
if (this._p2pSafeStream) {
|
||||
this._p2pSafeStream.writeMessage(iceMsg).catch((err) =>
|
||||
console.error("Error sending ICE candidate:", err),
|
||||
);
|
||||
const iceMsg = createMessage(
|
||||
create(ProtoICESchema, {
|
||||
candidate: e.candidate,
|
||||
}),
|
||||
"ice-candidate",
|
||||
);
|
||||
if (this._msgStream) {
|
||||
this._msgStream
|
||||
.write(iceMsg)
|
||||
.catch((err) => console.error("Error sending ICE candidate:", err));
|
||||
} else {
|
||||
console.warn("P2P stream not established, cannot send ICE candidate");
|
||||
}
|
||||
@@ -218,8 +270,7 @@ export class WebRTCStream {
|
||||
}
|
||||
|
||||
private _checkConnectionState() {
|
||||
if (!this._pc || !this._p2p || !this._p2pConn)
|
||||
return;
|
||||
if (!this._pc || !this._p2p || !this._p2pConn) return;
|
||||
|
||||
console.debug("Checking connection state:", {
|
||||
connectionState: this._pc.connectionState,
|
||||
@@ -256,7 +307,7 @@ export class WebRTCStream {
|
||||
// @ts-ignore
|
||||
receiver.jitterBufferTarget = receiver.jitterBufferDelayHint = receiver.playoutDelayHint = 0;
|
||||
}
|
||||
}, 15);
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -286,7 +337,9 @@ export class WebRTCStream {
|
||||
|
||||
// Attempt to reconnect only if not already connected
|
||||
if (!this._isConnected && this._serverURL && this._roomName) {
|
||||
this._setup(this._serverURL, this._roomName).catch((err) => console.error("Reconnection failed:", err));
|
||||
this._setup(this._serverURL, this._roomName).catch((err) =>
|
||||
console.error("Reconnection failed:", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +388,9 @@ export class WebRTCStream {
|
||||
}
|
||||
|
||||
public removeDataChannelCallback(callback: (data: any) => void) {
|
||||
this._dataChannelCallbacks = this._dataChannelCallbacks.filter(cb => cb !== callback);
|
||||
this._dataChannelCallbacks = this._dataChannelCallbacks.filter(
|
||||
(cb) => cb !== callback,
|
||||
);
|
||||
}
|
||||
|
||||
private _setupDataChannelEvents() {
|
||||
@@ -343,7 +398,7 @@ export class WebRTCStream {
|
||||
|
||||
this._dataChannel.onclose = () => console.log("sendChannel has closed");
|
||||
this._dataChannel.onopen = () => console.log("sendChannel has opened");
|
||||
this._dataChannel.onmessage = (event => {
|
||||
this._dataChannel.onmessage = (event) => {
|
||||
// Parse as ProtoBuf message
|
||||
const data = event.data;
|
||||
// Call registered callback if exists
|
||||
@@ -354,7 +409,7 @@ export class WebRTCStream {
|
||||
console.error("Error in data channel callback:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _gatherFrameRate() {
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
"sync:android": "npm run build && npx cap sync android"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.4.2",
|
||||
"@capacitor/android": "^7.4.3",
|
||||
"@capacitor/cli": "^7.4.3",
|
||||
"@capacitor/core": "^7.4.3",
|
||||
"@capacitor/ios": "^7.4.3",
|
||||
"@astrojs/node": "9.5.0",
|
||||
"@nestri/input": "*",
|
||||
"astro": "5.14.5"
|
||||
"astro": "5.15.1"
|
||||
}
|
||||
}
|
||||
@@ -89,11 +89,7 @@ if (envs_map.size > 0) {
|
||||
let nestriControllers: Controller[] = [];
|
||||
|
||||
window.addEventListener("gamepadconnected", (e) => {
|
||||
// Ignore gamepads with id including "nestri"
|
||||
console.log("Gamepad connected:", e.gamepad);
|
||||
if (e.gamepad.id.toLowerCase().includes("nestri"))
|
||||
return;
|
||||
|
||||
const controller = new Controller({
|
||||
webrtc: stream,
|
||||
e: e,
|
||||
|
||||
@@ -10,7 +10,7 @@ require (
|
||||
github.com/oklog/ulid/v2 v2.1.1
|
||||
github.com/pion/ice/v4 v4.0.10
|
||||
github.com/pion/interceptor v0.1.41
|
||||
github.com/pion/rtp v1.8.24
|
||||
github.com/pion/rtp v1.8.25
|
||||
github.com/pion/webrtc/v4 v4.1.6
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
google.golang.org/protobuf v1.36.10
|
||||
@@ -30,17 +30,17 @@ require (
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/ipfs/go-cid v0.5.0 // indirect
|
||||
github.com/ipfs/go-cid v0.6.0 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/koron/go-ssdp v0.1.0 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.3.0 // indirect
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
|
||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.3.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.4.0 // indirect
|
||||
github.com/libp2p/go-yamux/v5 v5.1.0 // indirect
|
||||
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
@@ -71,13 +71,13 @@ require (
|
||||
github.com/pion/sdp/v3 v3.0.16 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.8 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/stun/v3 v3.0.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.8 // indirect
|
||||
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||
github.com/pion/turn/v4 v4.1.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/common v0.67.2 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
github.com/quic-go/webtransport-go v0.9.0 // indirect
|
||||
@@ -91,12 +91,12 @@ require (
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef // indirect
|
||||
golang.org/x/telemetry v0.0.0-20251028164327-d7a2859f34e8 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
|
||||
@@ -71,8 +71,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||
github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30=
|
||||
github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
@@ -82,8 +82,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y=
|
||||
@@ -113,8 +113,8 @@ github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUI
|
||||
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
|
||||
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
|
||||
github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
|
||||
github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc=
|
||||
github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA=
|
||||
github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q=
|
||||
github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA=
|
||||
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
|
||||
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||
github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE=
|
||||
@@ -199,8 +199,8 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.8.24 h1:+ICyZXUQDv95EsHN70RrA4XKJf5MGWyC6QQc1u6/ynI=
|
||||
github.com/pion/rtp v1.8.24/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/rtp v1.8.25 h1:b8+y44GNbwOJTYWuVan7SglX/hMlicVCAtL50ztyZHw=
|
||||
github.com/pion/rtp v1.8.25/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8=
|
||||
github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo=
|
||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||
@@ -209,16 +209,16 @@ github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM=
|
||||
github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/stun/v3 v3.0.1 h1:jx1uUq6BdPihF0yF33Jj2mh+C9p0atY94IkdnW174kA=
|
||||
github.com/pion/stun/v3 v3.0.1/go.mod h1:RHnvlKFg+qHgoKIqtQWMOJF52wsImCAf/Jh5GjX+4Tw=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
|
||||
github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||
github.com/pion/turn/v4 v4.1.2 h1:Em2svpl6aBFa88dLhxypMUzaLjC79kWZWx8FIov01cc=
|
||||
github.com/pion/turn/v4 v4.1.2/go.mod h1:ISYWfZYy0Z3tXzRpyYZHTL+U23yFQIspfxogdQ8pn9Y=
|
||||
github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw=
|
||||
github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -231,11 +231,11 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
|
||||
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
||||
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
|
||||
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
@@ -323,8 +323,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -396,8 +396,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef h1:5xFtU4tmJMJSxSeDlr1dgBff2tDXrq0laLdS1EA3LYw=
|
||||
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
|
||||
golang.org/x/telemetry v0.0.0-20251028164327-d7a2859f34e8 h1:DwMAzqwLj2rVin75cRFh1kfhwQY3hyHrU1oCEDZXPmQ=
|
||||
golang.org/x/telemetry v0.0.0-20251028164327-d7a2859f34e8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
||||
@@ -26,7 +26,7 @@ func InitWebRTCAPI() error {
|
||||
mediaEngine := &webrtc.MediaEngine{}
|
||||
|
||||
// Register our extensions
|
||||
if err := RegisterExtensions(mediaEngine); err != nil {
|
||||
if err = RegisterExtensions(mediaEngine); err != nil {
|
||||
return fmt.Errorf("failed to register extensions: %w", err)
|
||||
}
|
||||
|
||||
|
||||
53
packages/relay/internal/common/ice_helper.go
Normal file
53
packages/relay/internal/common/ice_helper.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// ICEHelper holds webrtc.ICECandidateInit(s) until remote candidate is set for given webrtc.PeerConnection
|
||||
// Held candidates should be flushed at the end of negotiation to ensure all are available for connection
|
||||
type ICEHelper struct {
|
||||
candidates []webrtc.ICECandidateInit
|
||||
pc *webrtc.PeerConnection
|
||||
}
|
||||
|
||||
func NewICEHelper(pc *webrtc.PeerConnection) *ICEHelper {
|
||||
return &ICEHelper{
|
||||
pc: pc,
|
||||
candidates: make([]webrtc.ICECandidateInit, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (ice *ICEHelper) SetPeerConnection(pc *webrtc.PeerConnection) {
|
||||
ice.pc = pc
|
||||
}
|
||||
|
||||
func (ice *ICEHelper) AddCandidate(c webrtc.ICECandidateInit) {
|
||||
if ice.pc != nil {
|
||||
if ice.pc.RemoteDescription() != nil {
|
||||
// Add immediately if remote is set
|
||||
if err := ice.pc.AddICECandidate(c); err != nil {
|
||||
slog.Error("Failed to add ICE candidate", "err", err)
|
||||
}
|
||||
// Also flush held candidates automatically
|
||||
ice.FlushHeldCandidates()
|
||||
} else {
|
||||
// Hold in slice until remote is set
|
||||
ice.candidates = append(ice.candidates, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ice *ICEHelper) FlushHeldCandidates() {
|
||||
if ice.pc != nil && len(ice.candidates) > 0 {
|
||||
for _, heldCandidate := range ice.candidates {
|
||||
if err := ice.pc.AddICECandidate(heldCandidate); err != nil {
|
||||
slog.Error("Failed to add held ICE candidate", "err", err)
|
||||
}
|
||||
}
|
||||
// Clear the held candidates
|
||||
ice.candidates = make([]webrtc.ICECandidateInit, 0)
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,28 @@ package common
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
gen "relay/internal/proto"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// MaxSize is the maximum allowed data size (1MB)
|
||||
const MaxSize = 1024 * 1024
|
||||
// readUvarint reads an unsigned varint from the reader
|
||||
func readUvarint(r io.ByteReader) (uint64, error) {
|
||||
return binary.ReadUvarint(r)
|
||||
}
|
||||
|
||||
// writeUvarint writes an unsigned varint to the writer
|
||||
func writeUvarint(w io.Writer, x uint64) error {
|
||||
buf := make([]byte, binary.MaxVarintLen64)
|
||||
n := binary.PutUvarint(buf, x)
|
||||
_, err := w.Write(buf[:n])
|
||||
return err
|
||||
}
|
||||
|
||||
// SafeBufioRW wraps a bufio.ReadWriter for sending and receiving JSON and protobufs safely
|
||||
type SafeBufioRW struct {
|
||||
@@ -24,83 +36,6 @@ func NewSafeBufioRW(brw *bufio.ReadWriter) *SafeBufioRW {
|
||||
return &SafeBufioRW{brw: brw}
|
||||
}
|
||||
|
||||
// SendJSON serializes the given data as JSON and sends it with a 4-byte length prefix
|
||||
func (bu *SafeBufioRW) SendJSON(data interface{}) error {
|
||||
bu.mutex.Lock()
|
||||
defer bu.mutex.Unlock()
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(jsonData) > MaxSize {
|
||||
return errors.New("JSON data exceeds maximum size")
|
||||
}
|
||||
|
||||
// Write the 4-byte length prefix
|
||||
if err = binary.Write(bu.brw, binary.BigEndian, uint32(len(jsonData))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the JSON data
|
||||
if _, err = bu.brw.Write(jsonData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Flush the writer to ensure data is sent
|
||||
return bu.brw.Flush()
|
||||
}
|
||||
|
||||
// ReceiveJSON reads a 4-byte length prefix, then reads and unmarshals the JSON
|
||||
func (bu *SafeBufioRW) ReceiveJSON(dest interface{}) error {
|
||||
bu.mutex.RLock()
|
||||
defer bu.mutex.RUnlock()
|
||||
|
||||
// Read the 4-byte length prefix
|
||||
var length uint32
|
||||
if err := binary.Read(bu.brw, binary.BigEndian, &length); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if length > MaxSize {
|
||||
return errors.New("received JSON data exceeds maximum size")
|
||||
}
|
||||
|
||||
// Read the JSON data
|
||||
data := make([]byte, length)
|
||||
if _, err := io.ReadFull(bu.brw, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, dest)
|
||||
}
|
||||
|
||||
// Receive reads a 4-byte length prefix, then reads the raw data
|
||||
func (bu *SafeBufioRW) Receive() ([]byte, error) {
|
||||
bu.mutex.RLock()
|
||||
defer bu.mutex.RUnlock()
|
||||
|
||||
// Read the 4-byte length prefix
|
||||
var length uint32
|
||||
if err := binary.Read(bu.brw, binary.BigEndian, &length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if length > MaxSize {
|
||||
return nil, errors.New("received data exceeds maximum size")
|
||||
}
|
||||
|
||||
// Read the raw data
|
||||
data := make([]byte, length)
|
||||
if _, err := io.ReadFull(bu.brw, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// SendProto serializes the given protobuf message and sends it with a 4-byte length prefix
|
||||
func (bu *SafeBufioRW) SendProto(msg proto.Message) error {
|
||||
bu.mutex.Lock()
|
||||
defer bu.mutex.Unlock()
|
||||
@@ -110,12 +45,8 @@ func (bu *SafeBufioRW) SendProto(msg proto.Message) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(protoData) > MaxSize {
|
||||
return errors.New("protobuf data exceeds maximum size")
|
||||
}
|
||||
|
||||
// Write the 4-byte length prefix
|
||||
if err = binary.Write(bu.brw, binary.BigEndian, uint32(len(protoData))); err != nil {
|
||||
// Write varint length prefix
|
||||
if err := writeUvarint(bu.brw, uint64(len(protoData))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -124,25 +55,19 @@ func (bu *SafeBufioRW) SendProto(msg proto.Message) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Flush the writer to ensure data is sent
|
||||
return bu.brw.Flush()
|
||||
}
|
||||
|
||||
// ReceiveProto reads a 4-byte length prefix, then reads and unmarshals the protobuf
|
||||
func (bu *SafeBufioRW) ReceiveProto(msg proto.Message) error {
|
||||
bu.mutex.RLock()
|
||||
defer bu.mutex.RUnlock()
|
||||
|
||||
// Read the 4-byte length prefix
|
||||
var length uint32
|
||||
if err := binary.Read(bu.brw, binary.BigEndian, &length); err != nil {
|
||||
// Read varint length prefix
|
||||
length, err := readUvarint(bu.brw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if length > MaxSize {
|
||||
return errors.New("received Protobuf data exceeds maximum size")
|
||||
}
|
||||
|
||||
// Read the Protobuf data
|
||||
data := make([]byte, length)
|
||||
if _, err := io.ReadFull(bu.brw, data); err != nil {
|
||||
@@ -152,24 +77,51 @@ func (bu *SafeBufioRW) ReceiveProto(msg proto.Message) error {
|
||||
return proto.Unmarshal(data, msg)
|
||||
}
|
||||
|
||||
// Write writes raw data to the underlying buffer
|
||||
func (bu *SafeBufioRW) Write(data []byte) (int, error) {
|
||||
bu.mutex.Lock()
|
||||
defer bu.mutex.Unlock()
|
||||
|
||||
if len(data) > MaxSize {
|
||||
return 0, errors.New("data exceeds maximum size")
|
||||
}
|
||||
|
||||
n, err := bu.brw.Write(data)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Flush the writer to ensure data is sent
|
||||
if err = bu.brw.Flush(); err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
return n, nil
|
||||
type CreateMessageOptions struct {
|
||||
SequenceID string
|
||||
Latency *gen.ProtoLatencyTracker
|
||||
}
|
||||
|
||||
func CreateMessage(payload proto.Message, payloadType string, opts *CreateMessageOptions) (*gen.ProtoMessage, error) {
|
||||
msg := &gen.ProtoMessage{
|
||||
MessageBase: &gen.ProtoMessageBase{
|
||||
PayloadType: payloadType,
|
||||
},
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
if opts.Latency != nil {
|
||||
msg.MessageBase.Latency = opts.Latency
|
||||
} else if opts.SequenceID != "" {
|
||||
msg.MessageBase.Latency = &gen.ProtoLatencyTracker{
|
||||
SequenceId: opts.SequenceID,
|
||||
Timestamps: []*gen.ProtoTimestampEntry{
|
||||
{
|
||||
Stage: "created",
|
||||
Time: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use reflection to set the oneof field automatically
|
||||
msgReflect := msg.ProtoReflect()
|
||||
payloadReflect := payload.ProtoReflect()
|
||||
|
||||
oneofDesc := msgReflect.Descriptor().Oneofs().ByName("payload")
|
||||
if oneofDesc == nil {
|
||||
return nil, errors.New("payload oneof not found")
|
||||
}
|
||||
|
||||
fields := oneofDesc.Fields()
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
field := fields.Get(i)
|
||||
if field.Message() != nil && field.Message().FullName() == payloadReflect.Descriptor().FullName() {
|
||||
msgReflect.Set(field, protoreflect.ValueOfMessage(payloadReflect))
|
||||
return msg, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("payload type not found in oneof")
|
||||
}
|
||||
|
||||
@@ -31,16 +31,18 @@ func NewNestriDataChannel(dc *webrtc.DataChannel) *NestriDataChannel {
|
||||
}
|
||||
|
||||
// Decode message
|
||||
var base gen.ProtoMessageInput
|
||||
var base gen.ProtoMessage
|
||||
if err := proto.Unmarshal(msg.Data, &base); err != nil {
|
||||
slog.Error("failed to decode binary DataChannel message", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle message type callback
|
||||
if callback, ok := ndc.callbacks["input"]; ok {
|
||||
go callback(msg.Data)
|
||||
} // We don't care about unhandled messages
|
||||
// Route based on PayloadType
|
||||
if base.MessageBase != nil && len(base.MessageBase.PayloadType) > 0 {
|
||||
if callback, ok := ndc.callbacks[base.MessageBase.PayloadType]; ok {
|
||||
go callback(msg.Data)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return ndc
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package connections
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"relay/internal/common"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// MessageBase is the base type for any JSON message
|
||||
type MessageBase struct {
|
||||
Type string `json:"payload_type"`
|
||||
Latency *common.LatencyTracker `json:"latency,omitempty"`
|
||||
}
|
||||
|
||||
type MessageRaw struct {
|
||||
MessageBase
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
func NewMessageRaw(t string, data json.RawMessage) *MessageRaw {
|
||||
return &MessageRaw{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageLog struct {
|
||||
MessageBase
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
func NewMessageLog(t string, level, message, time string) *MessageLog {
|
||||
return &MessageLog{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
Level: level,
|
||||
Message: message,
|
||||
Time: time,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageMetrics struct {
|
||||
MessageBase
|
||||
UsageCPU float64 `json:"usage_cpu"`
|
||||
UsageMemory float64 `json:"usage_memory"`
|
||||
Uptime uint64 `json:"uptime"`
|
||||
PipelineLatency float64 `json:"pipeline_latency"`
|
||||
}
|
||||
|
||||
func NewMessageMetrics(t string, usageCPU, usageMemory float64, uptime uint64, pipelineLatency float64) *MessageMetrics {
|
||||
return &MessageMetrics{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
UsageCPU: usageCPU,
|
||||
UsageMemory: usageMemory,
|
||||
Uptime: uptime,
|
||||
PipelineLatency: pipelineLatency,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageICE struct {
|
||||
MessageBase
|
||||
Candidate webrtc.ICECandidateInit `json:"candidate"`
|
||||
}
|
||||
|
||||
func NewMessageICE(t string, candidate webrtc.ICECandidateInit) *MessageICE {
|
||||
return &MessageICE{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
Candidate: candidate,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageSDP struct {
|
||||
MessageBase
|
||||
SDP webrtc.SessionDescription `json:"sdp"`
|
||||
}
|
||||
|
||||
func NewMessageSDP(t string, sdp webrtc.SessionDescription) *MessageSDP {
|
||||
return &MessageSDP{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
SDP: sdp,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@ func (r *Relay) DeleteRoomIfEmpty(room *shared.Room) {
|
||||
if room == nil {
|
||||
return
|
||||
}
|
||||
if room.Participants.Len() == 0 && r.LocalRooms.Has(room.ID) {
|
||||
if len(room.Participants) <= 0 && r.LocalRooms.Has(room.ID) {
|
||||
slog.Debug("Deleting empty room without participants", "room", room.Name)
|
||||
r.LocalRooms.Delete(room.ID)
|
||||
err := room.PeerConnection.Close()
|
||||
|
||||
@@ -129,12 +129,11 @@ func (r *Relay) onPeerConnected(peerID peer.ID) {
|
||||
|
||||
// onPeerDisconnected marks a peer as disconnected in our status view and removes latency info
|
||||
func (r *Relay) onPeerDisconnected(peerID peer.ID) {
|
||||
// Relay peer disconnect handling
|
||||
slog.Info("Mesh peer disconnected, deleting from local peer map", "peer", peerID)
|
||||
// Remove peer from local mesh peers
|
||||
if r.Peers.Has(peerID) {
|
||||
r.Peers.Delete(peerID)
|
||||
}
|
||||
// Remove any rooms associated with this peer
|
||||
if r.Rooms.Has(peerID.String()) {
|
||||
r.Rooms.Delete(peerID.String())
|
||||
}
|
||||
@@ -151,18 +150,18 @@ func (r *Relay) updateMeshRoomStates(peerID peer.ID, states []shared.RoomInfo) {
|
||||
}
|
||||
|
||||
// If previously did not exist, but does now, request a connection if participants exist for our room
|
||||
existed := r.Rooms.Has(state.ID.String())
|
||||
/*existed := r.Rooms.Has(state.ID.String())
|
||||
if !existed {
|
||||
// Request connection to this peer if we have participants in our local room
|
||||
if room, ok := r.LocalRooms.Get(state.ID); ok {
|
||||
if room.Participants.Len() > 0 {
|
||||
if len(room.Participants) > 0 {
|
||||
slog.Debug("Got new remote room state, we locally have participants for, requesting stream", "room_name", room.Name, "peer", peerID)
|
||||
if err := r.StreamProtocol.RequestStream(context.Background(), room, peerID); err != nil {
|
||||
slog.Error("Failed to request stream for new remote room state", "room_name", room.Name, "peer", peerID, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
r.Rooms.Set(state.ID.String(), state)
|
||||
}
|
||||
|
||||
@@ -73,28 +73,47 @@ func (x *ProtoMessageBase) GetLatency() *ProtoLatencyTracker {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ProtoMessageInput struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
MessageBase *ProtoMessageBase `protobuf:"bytes,1,opt,name=message_base,json=messageBase,proto3" json:"message_base,omitempty"`
|
||||
Data *ProtoInput `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
|
||||
type ProtoMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
MessageBase *ProtoMessageBase `protobuf:"bytes,1,opt,name=message_base,json=messageBase,proto3" json:"message_base,omitempty"`
|
||||
// Types that are valid to be assigned to Payload:
|
||||
//
|
||||
// *ProtoMessage_MouseMove
|
||||
// *ProtoMessage_MouseMoveAbs
|
||||
// *ProtoMessage_MouseWheel
|
||||
// *ProtoMessage_MouseKeyDown
|
||||
// *ProtoMessage_MouseKeyUp
|
||||
// *ProtoMessage_KeyDown
|
||||
// *ProtoMessage_KeyUp
|
||||
// *ProtoMessage_ControllerAttach
|
||||
// *ProtoMessage_ControllerDetach
|
||||
// *ProtoMessage_ControllerRumble
|
||||
// *ProtoMessage_ControllerStateBatch
|
||||
// *ProtoMessage_Ice
|
||||
// *ProtoMessage_Sdp
|
||||
// *ProtoMessage_Raw
|
||||
// *ProtoMessage_ClientRequestRoomStream
|
||||
// *ProtoMessage_ClientDisconnected
|
||||
// *ProtoMessage_ServerPushStream
|
||||
Payload isProtoMessage_Payload `protobuf_oneof:"payload"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProtoMessageInput) Reset() {
|
||||
*x = ProtoMessageInput{}
|
||||
func (x *ProtoMessage) Reset() {
|
||||
*x = ProtoMessage{}
|
||||
mi := &file_messages_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProtoMessageInput) String() string {
|
||||
func (x *ProtoMessage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProtoMessageInput) ProtoMessage() {}
|
||||
func (*ProtoMessage) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoMessageInput) ProtoReflect() protoreflect.Message {
|
||||
func (x *ProtoMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_messages_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
@@ -106,25 +125,287 @@ func (x *ProtoMessageInput) ProtoReflect() protoreflect.Message {
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ProtoMessageInput.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoMessageInput) Descriptor() ([]byte, []int) {
|
||||
// Deprecated: Use ProtoMessage.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoMessage) Descriptor() ([]byte, []int) {
|
||||
return file_messages_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ProtoMessageInput) GetMessageBase() *ProtoMessageBase {
|
||||
func (x *ProtoMessage) GetMessageBase() *ProtoMessageBase {
|
||||
if x != nil {
|
||||
return x.MessageBase
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessageInput) GetData() *ProtoInput {
|
||||
func (x *ProtoMessage) GetPayload() isProtoMessage_Payload {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
return x.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetMouseMove() *ProtoMouseMove {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_MouseMove); ok {
|
||||
return x.MouseMove
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetMouseMoveAbs() *ProtoMouseMoveAbs {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_MouseMoveAbs); ok {
|
||||
return x.MouseMoveAbs
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetMouseWheel() *ProtoMouseWheel {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_MouseWheel); ok {
|
||||
return x.MouseWheel
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetMouseKeyDown() *ProtoMouseKeyDown {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_MouseKeyDown); ok {
|
||||
return x.MouseKeyDown
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetMouseKeyUp() *ProtoMouseKeyUp {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_MouseKeyUp); ok {
|
||||
return x.MouseKeyUp
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetKeyDown() *ProtoKeyDown {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_KeyDown); ok {
|
||||
return x.KeyDown
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetKeyUp() *ProtoKeyUp {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_KeyUp); ok {
|
||||
return x.KeyUp
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetControllerAttach() *ProtoControllerAttach {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_ControllerAttach); ok {
|
||||
return x.ControllerAttach
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetControllerDetach() *ProtoControllerDetach {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_ControllerDetach); ok {
|
||||
return x.ControllerDetach
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetControllerRumble() *ProtoControllerRumble {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_ControllerRumble); ok {
|
||||
return x.ControllerRumble
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetControllerStateBatch() *ProtoControllerStateBatch {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_ControllerStateBatch); ok {
|
||||
return x.ControllerStateBatch
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetIce() *ProtoICE {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_Ice); ok {
|
||||
return x.Ice
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetSdp() *ProtoSDP {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_Sdp); ok {
|
||||
return x.Sdp
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetRaw() *ProtoRaw {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_Raw); ok {
|
||||
return x.Raw
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetClientRequestRoomStream() *ProtoClientRequestRoomStream {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_ClientRequestRoomStream); ok {
|
||||
return x.ClientRequestRoomStream
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetClientDisconnected() *ProtoClientDisconnected {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_ClientDisconnected); ok {
|
||||
return x.ClientDisconnected
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoMessage) GetServerPushStream() *ProtoServerPushStream {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ProtoMessage_ServerPushStream); ok {
|
||||
return x.ServerPushStream
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isProtoMessage_Payload interface {
|
||||
isProtoMessage_Payload()
|
||||
}
|
||||
|
||||
type ProtoMessage_MouseMove struct {
|
||||
// Input types
|
||||
MouseMove *ProtoMouseMove `protobuf:"bytes,2,opt,name=mouse_move,json=mouseMove,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_MouseMoveAbs struct {
|
||||
MouseMoveAbs *ProtoMouseMoveAbs `protobuf:"bytes,3,opt,name=mouse_move_abs,json=mouseMoveAbs,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_MouseWheel struct {
|
||||
MouseWheel *ProtoMouseWheel `protobuf:"bytes,4,opt,name=mouse_wheel,json=mouseWheel,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_MouseKeyDown struct {
|
||||
MouseKeyDown *ProtoMouseKeyDown `protobuf:"bytes,5,opt,name=mouse_key_down,json=mouseKeyDown,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_MouseKeyUp struct {
|
||||
MouseKeyUp *ProtoMouseKeyUp `protobuf:"bytes,6,opt,name=mouse_key_up,json=mouseKeyUp,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_KeyDown struct {
|
||||
KeyDown *ProtoKeyDown `protobuf:"bytes,7,opt,name=key_down,json=keyDown,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_KeyUp struct {
|
||||
KeyUp *ProtoKeyUp `protobuf:"bytes,8,opt,name=key_up,json=keyUp,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_ControllerAttach struct {
|
||||
// Controller input types
|
||||
ControllerAttach *ProtoControllerAttach `protobuf:"bytes,9,opt,name=controller_attach,json=controllerAttach,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_ControllerDetach struct {
|
||||
ControllerDetach *ProtoControllerDetach `protobuf:"bytes,10,opt,name=controller_detach,json=controllerDetach,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_ControllerRumble struct {
|
||||
ControllerRumble *ProtoControllerRumble `protobuf:"bytes,11,opt,name=controller_rumble,json=controllerRumble,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_ControllerStateBatch struct {
|
||||
ControllerStateBatch *ProtoControllerStateBatch `protobuf:"bytes,12,opt,name=controller_state_batch,json=controllerStateBatch,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_Ice struct {
|
||||
// Signaling types
|
||||
Ice *ProtoICE `protobuf:"bytes,20,opt,name=ice,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_Sdp struct {
|
||||
Sdp *ProtoSDP `protobuf:"bytes,21,opt,name=sdp,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_Raw struct {
|
||||
Raw *ProtoRaw `protobuf:"bytes,22,opt,name=raw,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_ClientRequestRoomStream struct {
|
||||
ClientRequestRoomStream *ProtoClientRequestRoomStream `protobuf:"bytes,23,opt,name=client_request_room_stream,json=clientRequestRoomStream,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_ClientDisconnected struct {
|
||||
ClientDisconnected *ProtoClientDisconnected `protobuf:"bytes,24,opt,name=client_disconnected,json=clientDisconnected,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoMessage_ServerPushStream struct {
|
||||
ServerPushStream *ProtoServerPushStream `protobuf:"bytes,25,opt,name=server_push_stream,json=serverPushStream,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*ProtoMessage_MouseMove) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_MouseMoveAbs) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_MouseWheel) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_MouseKeyDown) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_MouseKeyUp) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_KeyDown) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_KeyUp) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_ControllerAttach) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_ControllerDetach) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_ControllerRumble) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_ControllerStateBatch) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_Ice) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_Sdp) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_Raw) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_ClientRequestRoomStream) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_ClientDisconnected) isProtoMessage_Payload() {}
|
||||
|
||||
func (*ProtoMessage_ServerPushStream) isProtoMessage_Payload() {}
|
||||
|
||||
var File_messages_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_messages_proto_rawDesc = "" +
|
||||
@@ -132,10 +413,31 @@ const file_messages_proto_rawDesc = "" +
|
||||
"\x0emessages.proto\x12\x05proto\x1a\vtypes.proto\x1a\x15latency_tracker.proto\"k\n" +
|
||||
"\x10ProtoMessageBase\x12!\n" +
|
||||
"\fpayload_type\x18\x01 \x01(\tR\vpayloadType\x124\n" +
|
||||
"\alatency\x18\x02 \x01(\v2\x1a.proto.ProtoLatencyTrackerR\alatency\"v\n" +
|
||||
"\x11ProtoMessageInput\x12:\n" +
|
||||
"\fmessage_base\x18\x01 \x01(\v2\x17.proto.ProtoMessageBaseR\vmessageBase\x12%\n" +
|
||||
"\x04data\x18\x02 \x01(\v2\x11.proto.ProtoInputR\x04dataB\x16Z\x14relay/internal/protob\x06proto3"
|
||||
"\alatency\x18\x02 \x01(\v2\x1a.proto.ProtoLatencyTrackerR\alatency\"\x9b\t\n" +
|
||||
"\fProtoMessage\x12:\n" +
|
||||
"\fmessage_base\x18\x01 \x01(\v2\x17.proto.ProtoMessageBaseR\vmessageBase\x126\n" +
|
||||
"\n" +
|
||||
"mouse_move\x18\x02 \x01(\v2\x15.proto.ProtoMouseMoveH\x00R\tmouseMove\x12@\n" +
|
||||
"\x0emouse_move_abs\x18\x03 \x01(\v2\x18.proto.ProtoMouseMoveAbsH\x00R\fmouseMoveAbs\x129\n" +
|
||||
"\vmouse_wheel\x18\x04 \x01(\v2\x16.proto.ProtoMouseWheelH\x00R\n" +
|
||||
"mouseWheel\x12@\n" +
|
||||
"\x0emouse_key_down\x18\x05 \x01(\v2\x18.proto.ProtoMouseKeyDownH\x00R\fmouseKeyDown\x12:\n" +
|
||||
"\fmouse_key_up\x18\x06 \x01(\v2\x16.proto.ProtoMouseKeyUpH\x00R\n" +
|
||||
"mouseKeyUp\x120\n" +
|
||||
"\bkey_down\x18\a \x01(\v2\x13.proto.ProtoKeyDownH\x00R\akeyDown\x12*\n" +
|
||||
"\x06key_up\x18\b \x01(\v2\x11.proto.ProtoKeyUpH\x00R\x05keyUp\x12K\n" +
|
||||
"\x11controller_attach\x18\t \x01(\v2\x1c.proto.ProtoControllerAttachH\x00R\x10controllerAttach\x12K\n" +
|
||||
"\x11controller_detach\x18\n" +
|
||||
" \x01(\v2\x1c.proto.ProtoControllerDetachH\x00R\x10controllerDetach\x12K\n" +
|
||||
"\x11controller_rumble\x18\v \x01(\v2\x1c.proto.ProtoControllerRumbleH\x00R\x10controllerRumble\x12X\n" +
|
||||
"\x16controller_state_batch\x18\f \x01(\v2 .proto.ProtoControllerStateBatchH\x00R\x14controllerStateBatch\x12#\n" +
|
||||
"\x03ice\x18\x14 \x01(\v2\x0f.proto.ProtoICEH\x00R\x03ice\x12#\n" +
|
||||
"\x03sdp\x18\x15 \x01(\v2\x0f.proto.ProtoSDPH\x00R\x03sdp\x12#\n" +
|
||||
"\x03raw\x18\x16 \x01(\v2\x0f.proto.ProtoRawH\x00R\x03raw\x12b\n" +
|
||||
"\x1aclient_request_room_stream\x18\x17 \x01(\v2#.proto.ProtoClientRequestRoomStreamH\x00R\x17clientRequestRoomStream\x12Q\n" +
|
||||
"\x13client_disconnected\x18\x18 \x01(\v2\x1e.proto.ProtoClientDisconnectedH\x00R\x12clientDisconnected\x12L\n" +
|
||||
"\x12server_push_stream\x18\x19 \x01(\v2\x1c.proto.ProtoServerPushStreamH\x00R\x10serverPushStreamB\t\n" +
|
||||
"\apayloadB\x16Z\x14relay/internal/protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_messages_proto_rawDescOnce sync.Once
|
||||
@@ -151,20 +453,52 @@ func file_messages_proto_rawDescGZIP() []byte {
|
||||
|
||||
var file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_messages_proto_goTypes = []any{
|
||||
(*ProtoMessageBase)(nil), // 0: proto.ProtoMessageBase
|
||||
(*ProtoMessageInput)(nil), // 1: proto.ProtoMessageInput
|
||||
(*ProtoLatencyTracker)(nil), // 2: proto.ProtoLatencyTracker
|
||||
(*ProtoInput)(nil), // 3: proto.ProtoInput
|
||||
(*ProtoMessageBase)(nil), // 0: proto.ProtoMessageBase
|
||||
(*ProtoMessage)(nil), // 1: proto.ProtoMessage
|
||||
(*ProtoLatencyTracker)(nil), // 2: proto.ProtoLatencyTracker
|
||||
(*ProtoMouseMove)(nil), // 3: proto.ProtoMouseMove
|
||||
(*ProtoMouseMoveAbs)(nil), // 4: proto.ProtoMouseMoveAbs
|
||||
(*ProtoMouseWheel)(nil), // 5: proto.ProtoMouseWheel
|
||||
(*ProtoMouseKeyDown)(nil), // 6: proto.ProtoMouseKeyDown
|
||||
(*ProtoMouseKeyUp)(nil), // 7: proto.ProtoMouseKeyUp
|
||||
(*ProtoKeyDown)(nil), // 8: proto.ProtoKeyDown
|
||||
(*ProtoKeyUp)(nil), // 9: proto.ProtoKeyUp
|
||||
(*ProtoControllerAttach)(nil), // 10: proto.ProtoControllerAttach
|
||||
(*ProtoControllerDetach)(nil), // 11: proto.ProtoControllerDetach
|
||||
(*ProtoControllerRumble)(nil), // 12: proto.ProtoControllerRumble
|
||||
(*ProtoControllerStateBatch)(nil), // 13: proto.ProtoControllerStateBatch
|
||||
(*ProtoICE)(nil), // 14: proto.ProtoICE
|
||||
(*ProtoSDP)(nil), // 15: proto.ProtoSDP
|
||||
(*ProtoRaw)(nil), // 16: proto.ProtoRaw
|
||||
(*ProtoClientRequestRoomStream)(nil), // 17: proto.ProtoClientRequestRoomStream
|
||||
(*ProtoClientDisconnected)(nil), // 18: proto.ProtoClientDisconnected
|
||||
(*ProtoServerPushStream)(nil), // 19: proto.ProtoServerPushStream
|
||||
}
|
||||
var file_messages_proto_depIdxs = []int32{
|
||||
2, // 0: proto.ProtoMessageBase.latency:type_name -> proto.ProtoLatencyTracker
|
||||
0, // 1: proto.ProtoMessageInput.message_base:type_name -> proto.ProtoMessageBase
|
||||
3, // 2: proto.ProtoMessageInput.data:type_name -> proto.ProtoInput
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
2, // 0: proto.ProtoMessageBase.latency:type_name -> proto.ProtoLatencyTracker
|
||||
0, // 1: proto.ProtoMessage.message_base:type_name -> proto.ProtoMessageBase
|
||||
3, // 2: proto.ProtoMessage.mouse_move:type_name -> proto.ProtoMouseMove
|
||||
4, // 3: proto.ProtoMessage.mouse_move_abs:type_name -> proto.ProtoMouseMoveAbs
|
||||
5, // 4: proto.ProtoMessage.mouse_wheel:type_name -> proto.ProtoMouseWheel
|
||||
6, // 5: proto.ProtoMessage.mouse_key_down:type_name -> proto.ProtoMouseKeyDown
|
||||
7, // 6: proto.ProtoMessage.mouse_key_up:type_name -> proto.ProtoMouseKeyUp
|
||||
8, // 7: proto.ProtoMessage.key_down:type_name -> proto.ProtoKeyDown
|
||||
9, // 8: proto.ProtoMessage.key_up:type_name -> proto.ProtoKeyUp
|
||||
10, // 9: proto.ProtoMessage.controller_attach:type_name -> proto.ProtoControllerAttach
|
||||
11, // 10: proto.ProtoMessage.controller_detach:type_name -> proto.ProtoControllerDetach
|
||||
12, // 11: proto.ProtoMessage.controller_rumble:type_name -> proto.ProtoControllerRumble
|
||||
13, // 12: proto.ProtoMessage.controller_state_batch:type_name -> proto.ProtoControllerStateBatch
|
||||
14, // 13: proto.ProtoMessage.ice:type_name -> proto.ProtoICE
|
||||
15, // 14: proto.ProtoMessage.sdp:type_name -> proto.ProtoSDP
|
||||
16, // 15: proto.ProtoMessage.raw:type_name -> proto.ProtoRaw
|
||||
17, // 16: proto.ProtoMessage.client_request_room_stream:type_name -> proto.ProtoClientRequestRoomStream
|
||||
18, // 17: proto.ProtoMessage.client_disconnected:type_name -> proto.ProtoClientDisconnected
|
||||
19, // 18: proto.ProtoMessage.server_push_stream:type_name -> proto.ProtoServerPushStream
|
||||
19, // [19:19] is the sub-list for method output_type
|
||||
19, // [19:19] is the sub-list for method input_type
|
||||
19, // [19:19] is the sub-list for extension type_name
|
||||
19, // [19:19] is the sub-list for extension extendee
|
||||
0, // [0:19] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_messages_proto_init() }
|
||||
@@ -174,6 +508,25 @@ func file_messages_proto_init() {
|
||||
}
|
||||
file_types_proto_init()
|
||||
file_latency_tracker_proto_init()
|
||||
file_messages_proto_msgTypes[1].OneofWrappers = []any{
|
||||
(*ProtoMessage_MouseMove)(nil),
|
||||
(*ProtoMessage_MouseMoveAbs)(nil),
|
||||
(*ProtoMessage_MouseWheel)(nil),
|
||||
(*ProtoMessage_MouseKeyDown)(nil),
|
||||
(*ProtoMessage_MouseKeyUp)(nil),
|
||||
(*ProtoMessage_KeyDown)(nil),
|
||||
(*ProtoMessage_KeyUp)(nil),
|
||||
(*ProtoMessage_ControllerAttach)(nil),
|
||||
(*ProtoMessage_ControllerDetach)(nil),
|
||||
(*ProtoMessage_ControllerRumble)(nil),
|
||||
(*ProtoMessage_ControllerStateBatch)(nil),
|
||||
(*ProtoMessage_Ice)(nil),
|
||||
(*ProtoMessage_Sdp)(nil),
|
||||
(*ProtoMessage_Raw)(nil),
|
||||
(*ProtoMessage_ClientRequestRoomStream)(nil),
|
||||
(*ProtoMessage_ClientDisconnected)(nil),
|
||||
(*ProtoMessage_ServerPushStream)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,139 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"relay/internal/common"
|
||||
"relay/internal/connections"
|
||||
"sync"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Participant struct {
|
||||
ID ulid.ULID
|
||||
SessionID string // Track session for reconnection
|
||||
PeerID peer.ID // libp2p peer ID
|
||||
PeerConnection *webrtc.PeerConnection
|
||||
DataChannel *connections.NestriDataChannel
|
||||
|
||||
// Per-viewer tracks and channels
|
||||
VideoTrack *webrtc.TrackLocalStaticRTP
|
||||
AudioTrack *webrtc.TrackLocalStaticRTP
|
||||
|
||||
// Per-viewer RTP state for retiming
|
||||
VideoSequenceNumber uint16
|
||||
VideoTimestamp uint32
|
||||
AudioSequenceNumber uint16
|
||||
AudioTimestamp uint32
|
||||
|
||||
packetQueue chan *participantPacket
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func NewParticipant() (*Participant, error) {
|
||||
func NewParticipant(sessionID string, peerID peer.ID) (*Participant, error) {
|
||||
id, err := common.NewULID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create ULID for Participant: %w", err)
|
||||
}
|
||||
return &Participant{
|
||||
ID: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Participant) addTrack(trackLocal *webrtc.TrackLocalStaticRTP) error {
|
||||
rtpSender, err := p.PeerConnection.AddTrack(trackLocal)
|
||||
if err != nil {
|
||||
return err
|
||||
p := &Participant{
|
||||
ID: id,
|
||||
SessionID: sessionID,
|
||||
PeerID: peerID,
|
||||
VideoSequenceNumber: 0,
|
||||
VideoTimestamp: 0,
|
||||
AudioSequenceNumber: 0,
|
||||
AudioTimestamp: 0,
|
||||
packetQueue: make(chan *participantPacket, 1000),
|
||||
}
|
||||
|
||||
go func() {
|
||||
rtcpBuffer := make([]byte, 1400)
|
||||
for {
|
||||
if _, _, rtcpErr := rtpSender.Read(rtcpBuffer); rtcpErr != nil {
|
||||
break
|
||||
go p.packetWriter()
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// SetTrack sets audio/video track for Participant
|
||||
func (p *Participant) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
|
||||
switch trackType {
|
||||
case webrtc.RTPCodecTypeAudio:
|
||||
p.AudioTrack = track
|
||||
_, err := p.PeerConnection.AddTrack(track)
|
||||
if err != nil {
|
||||
slog.Error("Failed to add audio track", "participant", p.ID, "err", err)
|
||||
}
|
||||
case webrtc.RTPCodecTypeVideo:
|
||||
p.VideoTrack = track
|
||||
_, err := p.PeerConnection.AddTrack(track)
|
||||
if err != nil {
|
||||
slog.Error("Failed to add video track", "participant", p.ID, "err", err)
|
||||
}
|
||||
default:
|
||||
slog.Warn("Unknown track type", "participant", p.ID, "trackType", trackType)
|
||||
}
|
||||
}
|
||||
|
||||
// Close cleans up participant resources
|
||||
func (p *Participant) Close() {
|
||||
p.closeOnce.Do(func() {
|
||||
close(p.packetQueue)
|
||||
})
|
||||
if p.DataChannel != nil {
|
||||
err := p.DataChannel.Close()
|
||||
if err != nil {
|
||||
slog.Error("Failed to close DataChannel", "participant", p.ID, "err", err)
|
||||
}
|
||||
p.DataChannel = nil
|
||||
}
|
||||
if p.PeerConnection != nil {
|
||||
err := p.PeerConnection.Close()
|
||||
if err != nil {
|
||||
slog.Error("Failed to close PeerConnection", "participant", p.ID, "err", err)
|
||||
}
|
||||
p.PeerConnection = nil
|
||||
}
|
||||
if p.VideoTrack != nil {
|
||||
p.VideoTrack = nil
|
||||
}
|
||||
if p.AudioTrack != nil {
|
||||
p.AudioTrack = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Participant) packetWriter() {
|
||||
for pkt := range p.packetQueue {
|
||||
var track *webrtc.TrackLocalStaticRTP
|
||||
var sequenceNumber uint16
|
||||
var timestamp uint32
|
||||
|
||||
// No mutex needed - only this goroutine modifies these
|
||||
if pkt.kind == webrtc.RTPCodecTypeAudio {
|
||||
track = p.AudioTrack
|
||||
p.AudioSequenceNumber = uint16(int(p.AudioSequenceNumber) + pkt.sequenceDiff)
|
||||
p.AudioTimestamp = uint32(int64(p.AudioTimestamp) + pkt.timeDiff)
|
||||
sequenceNumber = p.AudioSequenceNumber
|
||||
timestamp = p.AudioTimestamp
|
||||
} else {
|
||||
track = p.VideoTrack
|
||||
p.VideoSequenceNumber = uint16(int(p.VideoSequenceNumber) + pkt.sequenceDiff)
|
||||
p.VideoTimestamp = uint32(int64(p.VideoTimestamp) + pkt.timeDiff)
|
||||
sequenceNumber = p.VideoSequenceNumber
|
||||
timestamp = p.VideoTimestamp
|
||||
}
|
||||
|
||||
if track != nil {
|
||||
pkt.packet.SequenceNumber = sequenceNumber
|
||||
pkt.packet.Timestamp = timestamp
|
||||
|
||||
if err := track.WriteRTP(pkt.packet); err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
slog.Error("WriteRTP failed", "participant", p.ID, "kind", pkt.kind, "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
// Return packet struct to pool
|
||||
participantPacketPool.Put(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,29 @@ package shared
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"relay/internal/common"
|
||||
"relay/internal/connections"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
var participantPacketPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &participantPacket{}
|
||||
},
|
||||
}
|
||||
|
||||
type participantPacket struct {
|
||||
kind webrtc.RTPCodecType
|
||||
packet *rtp.Packet
|
||||
timeDiff int64
|
||||
sequenceDiff int
|
||||
}
|
||||
|
||||
type RoomInfo struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -18,49 +33,139 @@ type RoomInfo struct {
|
||||
|
||||
type Room struct {
|
||||
RoomInfo
|
||||
AudioCodec webrtc.RTPCodecCapability
|
||||
VideoCodec webrtc.RTPCodecCapability
|
||||
PeerConnection *webrtc.PeerConnection
|
||||
AudioTrack *webrtc.TrackLocalStaticRTP
|
||||
VideoTrack *webrtc.TrackLocalStaticRTP
|
||||
DataChannel *connections.NestriDataChannel
|
||||
Participants *common.SafeMap[ulid.ULID, *Participant]
|
||||
|
||||
// Atomic pointer to slice of participant channels
|
||||
participantChannels atomic.Pointer[[]chan<- *participantPacket]
|
||||
participantsMtx sync.Mutex // Use only for add/remove
|
||||
|
||||
Participants map[ulid.ULID]*Participant // Keep general track of Participant(s)
|
||||
|
||||
// Track last seen values to calculate diffs
|
||||
LastVideoTimestamp uint32
|
||||
LastVideoSequenceNumber uint16
|
||||
LastAudioTimestamp uint32
|
||||
LastAudioSequenceNumber uint16
|
||||
|
||||
VideoTimestampSet bool
|
||||
VideoSequenceSet bool
|
||||
AudioTimestampSet bool
|
||||
AudioSequenceSet bool
|
||||
}
|
||||
|
||||
func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room {
|
||||
return &Room{
|
||||
r := &Room{
|
||||
RoomInfo: RoomInfo{
|
||||
ID: roomID,
|
||||
Name: name,
|
||||
OwnerID: ownerID,
|
||||
},
|
||||
Participants: common.NewSafeMap[ulid.ULID, *Participant](),
|
||||
PeerConnection: nil,
|
||||
DataChannel: nil,
|
||||
Participants: make(map[ulid.ULID]*Participant),
|
||||
}
|
||||
|
||||
emptyChannels := make([]chan<- *participantPacket, 0)
|
||||
r.participantChannels.Store(&emptyChannels)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Close closes up Room (stream ended)
|
||||
func (r *Room) Close() {
|
||||
if r.DataChannel != nil {
|
||||
err := r.DataChannel.Close()
|
||||
if err != nil {
|
||||
slog.Error("Failed to close Room DataChannel", err)
|
||||
}
|
||||
r.DataChannel = nil
|
||||
}
|
||||
if r.PeerConnection != nil {
|
||||
err := r.PeerConnection.Close()
|
||||
if err != nil {
|
||||
slog.Error("Failed to close Room PeerConnection", err)
|
||||
}
|
||||
r.PeerConnection = nil
|
||||
}
|
||||
}
|
||||
|
||||
// AddParticipant adds a Participant to a Room
|
||||
func (r *Room) AddParticipant(participant *Participant) {
|
||||
slog.Debug("Adding participant to room", "participant", participant.ID, "room", r.Name)
|
||||
r.Participants.Set(participant.ID, participant)
|
||||
r.participantsMtx.Lock()
|
||||
defer r.participantsMtx.Unlock()
|
||||
|
||||
r.Participants[participant.ID] = participant
|
||||
|
||||
// Update channel slice atomically
|
||||
current := r.participantChannels.Load()
|
||||
newChannels := make([]chan<- *participantPacket, len(*current)+1)
|
||||
copy(newChannels, *current)
|
||||
newChannels[len(*current)] = participant.packetQueue
|
||||
|
||||
r.participantChannels.Store(&newChannels)
|
||||
|
||||
slog.Debug("Added participant", "participant", participant.ID, "room", r.Name)
|
||||
}
|
||||
|
||||
// Removes a Participant from a Room by participant's ID
|
||||
func (r *Room) removeParticipantByID(pID ulid.ULID) {
|
||||
if _, ok := r.Participants.Get(pID); ok {
|
||||
r.Participants.Delete(pID)
|
||||
// RemoveParticipantByID removes a Participant from a Room by participant's ID
|
||||
func (r *Room) RemoveParticipantByID(pID ulid.ULID) {
|
||||
r.participantsMtx.Lock()
|
||||
defer r.participantsMtx.Unlock()
|
||||
|
||||
participant, ok := r.Participants[pID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delete(r.Participants, pID)
|
||||
|
||||
// Update channel slice
|
||||
current := r.participantChannels.Load()
|
||||
newChannels := make([]chan<- *participantPacket, 0, len(*current)-1)
|
||||
for _, ch := range *current {
|
||||
if ch != participant.packetQueue {
|
||||
newChannels = append(newChannels, ch)
|
||||
}
|
||||
}
|
||||
|
||||
r.participantChannels.Store(&newChannels)
|
||||
|
||||
slog.Debug("Removed participant", "participant", pID, "room", r.Name)
|
||||
}
|
||||
|
||||
// IsOnline checks if the room is online (has both audio and video tracks)
|
||||
// IsOnline checks if the room is online
|
||||
func (r *Room) IsOnline() bool {
|
||||
return r.AudioTrack != nil && r.VideoTrack != nil
|
||||
return r.PeerConnection != nil
|
||||
}
|
||||
|
||||
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
|
||||
switch trackType {
|
||||
case webrtc.RTPCodecTypeAudio:
|
||||
r.AudioTrack = track
|
||||
case webrtc.RTPCodecTypeVideo:
|
||||
r.VideoTrack = track
|
||||
default:
|
||||
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
|
||||
func (r *Room) BroadcastPacketRetimed(kind webrtc.RTPCodecType, pkt *rtp.Packet, timeDiff int64, sequenceDiff int) {
|
||||
// Lock-free load of channel slice
|
||||
channels := r.participantChannels.Load()
|
||||
|
||||
// no participants..
|
||||
if len(*channels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Send to each participant channel (non-blocking)
|
||||
for i, ch := range *channels {
|
||||
// Get packet struct from pool
|
||||
pp := participantPacketPool.Get().(*participantPacket)
|
||||
pp.kind = kind
|
||||
pp.packet = pkt.Clone()
|
||||
pp.timeDiff = timeDiff
|
||||
pp.sequenceDiff = sequenceDiff
|
||||
|
||||
select {
|
||||
case ch <- pp:
|
||||
// Sent successfully
|
||||
default:
|
||||
// Channel full, drop packet, log?
|
||||
slog.Warn("Channel full, dropping packet", "channel_index", i)
|
||||
participantPacketPool.Put(pp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ NVIDIA_INSTALLER_DIR="/tmp"
|
||||
TIMEOUT_SECONDS=10
|
||||
ENTCMD_PREFIX=""
|
||||
|
||||
# Ensures user directory ownership
|
||||
chown_user_directory() {
|
||||
# Ensures user ownership across directories
|
||||
handle_user_permissions() {
|
||||
if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" 2>/dev/null; then
|
||||
echo "Error: Failed to change ownership of ${NESTRI_HOME} to ${NESTRI_USER}:${NESTRI_USER}" >&2
|
||||
return 1
|
||||
fi
|
||||
# Also apply to .cache separately
|
||||
# Also apply to .cache
|
||||
if [[ -d "${NESTRI_HOME}/.cache" ]]; then
|
||||
if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}/.cache" 2>/dev/null; then
|
||||
echo "Error: Failed to change ownership of ${NESTRI_HOME}/.cache to ${NESTRI_USER}:${NESTRI_USER}" >&2
|
||||
@@ -324,9 +324,23 @@ main() {
|
||||
log "Skipping CAP_SYS_NICE for gamescope, capability not available"
|
||||
fi
|
||||
|
||||
# Handle user directory permissions
|
||||
log "Ensuring user directory permissions..."
|
||||
chown_user_directory || exit 1
|
||||
# Make sure /tmp/.X11-unix exists..
|
||||
if [[ ! -d "/tmp/.X11-unix" ]]; then
|
||||
log "Creating /tmp/.X11-unix directory.."
|
||||
$ENTCMD_PREFIX mkdir -p /tmp/.X11-unix || {
|
||||
log "Error: Failed to create /tmp/.X11-unix directory"
|
||||
exit 1
|
||||
}
|
||||
# Set required perms..
|
||||
$ENTCMD_PREFIX chmod 1777 /tmp/.X11-unix || {
|
||||
log "Error: Failed to chmod /tmp/.X11-unix to 1777"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Handle user permissions
|
||||
log "Ensuring user permissions..."
|
||||
handle_user_permissions || exit 1
|
||||
|
||||
# Setup namespaceless env if needed for container runtime
|
||||
if [[ "$container_runtime" != "podman" ]]; then
|
||||
@@ -336,7 +350,7 @@ main() {
|
||||
|
||||
# Make sure /run/udev/ directory exists with /run/udev/control, needed for virtual controller support
|
||||
if [[ ! -d "/run/udev" || ! -e "/run/udev/control" ]]; then
|
||||
log "Creating /run/udev directory and control file..."
|
||||
log "Creating /run/udev directory and control file.."
|
||||
$ENTCMD_PREFIX mkdir -p /run/udev || {
|
||||
log "Error: Failed to create /run/udev directory"
|
||||
exit 1
|
||||
|
||||
@@ -187,7 +187,7 @@ start_compositor() {
|
||||
|
||||
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
|
||||
log "Starting application: $NESTRI_LAUNCH_CMD"
|
||||
WAYLAND_DISPLAY=wayland-0 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
|
||||
WAYLAND_DISPLAY="$COMPOSITOR_SOCKET" /bin/bash -c "$NESTRI_LAUNCH_CMD" &
|
||||
APP_PID=$!
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -10,9 +10,6 @@ export DISPLAY=:0
|
||||
# Causes some setups to break
|
||||
export PROTON_NO_FSYNC=1
|
||||
|
||||
# Sleeker Mangohud preset :)
|
||||
export MANGOHUD_CONFIG=preset=2
|
||||
|
||||
# Make gstreamer GL elements work without display output (NVIDIA issue..)
|
||||
export GST_GL_API=gles2
|
||||
export GST_GL_WINDOW=surfaceless
|
||||
|
||||
129
packages/server/Cargo.lock
generated
129
packages/server/Cargo.lock
generated
@@ -181,7 +181,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -193,7 +193,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -205,7 +205,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -246,7 +246,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -257,7 +257,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -395,7 +395,7 @@ version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
@@ -406,7 +406,7 @@ dependencies = [
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -417,9 +417,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.4"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
@@ -603,9 +603,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.49"
|
||||
version = "4.5.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f"
|
||||
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -613,9 +613,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.49"
|
||||
version = "4.5.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730"
|
||||
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -632,7 +632,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -839,7 +839,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -879,7 +879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -956,7 +956,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1090,7 +1090,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1262,7 +1262,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1396,7 +1396,7 @@ version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1f2cbc4577536c849335878552f42086bfd25a8dcd6f54a18655cf818b20c8f"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
@@ -1421,7 +1421,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2299,9 +2299,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.4"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
@@ -2368,9 +2368,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
@@ -2828,7 +2828,7 @@ checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3151,6 +3151,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"unsigned-varint 0.8.0",
|
||||
"vimputti",
|
||||
"webrtc",
|
||||
]
|
||||
@@ -3238,7 +3239,7 @@ version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -3354,9 +3355,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -3498,7 +3499,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3603,7 +3604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3626,9 +3627,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -3653,7 +3654,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3676,7 +3677,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3860,7 +3861,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4037,7 +4038,7 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -4046,9 +4047,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.33"
|
||||
version = "0.23.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c"
|
||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -4179,7 +4180,7 @@ version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -4239,7 +4240,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4480,9 +4481,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4506,7 +4507,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4515,7 +4516,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
@@ -4573,7 +4574,7 @@ checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4602,7 +4603,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4613,7 +4614,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4706,7 +4707,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4816,7 +4817,7 @@ version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
@@ -4860,7 +4861,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4985,9 +4986,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
@@ -5078,9 +5079,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vimputti"
|
||||
version = "0.1.3"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5839a89185ccec572f746ccc02e37702cc6c0b62a6aa0d9bcd6e5921edba12"
|
||||
checksum = "6440b3684270f355fb89193bfb0de957686119626b8b207f21d91024a892d05c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"libc",
|
||||
@@ -5176,7 +5177,7 @@ dependencies = [
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -5211,7 +5212,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -5501,7 +5502,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5512,7 +5513,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5958,7 +5959,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -5979,7 +5980,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5999,7 +6000,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -6020,7 +6021,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6053,5 +6054,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@ rand = "0.9"
|
||||
rustls = { version = "0.23", features = ["ring"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
vimputti = "0.1.3"
|
||||
vimputti = "0.1.7"
|
||||
chrono = "0.4"
|
||||
prost = "0.14"
|
||||
prost-types = "0.14"
|
||||
@@ -40,3 +40,4 @@ libp2p-tcp = { version = "0.44", features = ["tokio"] }
|
||||
libp2p-websocket = "0.45"
|
||||
dashmap = "6.1"
|
||||
anyhow = "1.0"
|
||||
unsigned-varint = "0.8"
|
||||
|
||||
@@ -211,6 +211,14 @@ impl Args {
|
||||
.value_parser(value_parser!(u32).range(1..))
|
||||
.default_value("192"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("software-render")
|
||||
.long("software-render")
|
||||
.env("SOFTWARE_RENDER")
|
||||
.help("Use software rendering for wayland")
|
||||
.value_parser(BoolishValueParser::new())
|
||||
.default_value("false"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("zero-copy")
|
||||
.long("zero-copy")
|
||||
|
||||
@@ -15,6 +15,9 @@ pub struct AppArgs {
|
||||
/// vimputti socket path
|
||||
pub vimputti_path: Option<String>,
|
||||
|
||||
/// Use software rendering for wayland display
|
||||
pub software_render: bool,
|
||||
|
||||
/// Experimental zero-copy pipeline support
|
||||
/// TODO: Move to video encoding flags
|
||||
pub zero_copy: bool,
|
||||
@@ -51,6 +54,10 @@ impl AppArgs {
|
||||
vimputti_path: matches
|
||||
.get_one::<String>("vimputti-path")
|
||||
.map(|s| s.clone()),
|
||||
software_render: matches
|
||||
.get_one::<bool>("software-render")
|
||||
.unwrap_or(&false)
|
||||
.clone(),
|
||||
zero_copy: matches
|
||||
.get_one::<bool>("zero-copy")
|
||||
.unwrap_or(&false)
|
||||
@@ -73,6 +80,7 @@ impl AppArgs {
|
||||
"> vimputti_path: '{}'",
|
||||
self.vimputti_path.as_ref().map_or("None", |s| s.as_str())
|
||||
);
|
||||
tracing::info!("> software_render: {}", self.software_render);
|
||||
tracing::info!("> zero_copy: {}", self.zero_copy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,7 +585,6 @@ pub fn get_best_working_encoder(
|
||||
encoders: &Vec<VideoEncoderInfo>,
|
||||
codec: &Codec,
|
||||
encoder_type: &EncoderType,
|
||||
zero_copy: bool,
|
||||
) -> Result<VideoEncoderInfo, Box<dyn Error>> {
|
||||
let mut candidates = get_encoders_by_videocodec(
|
||||
encoders,
|
||||
@@ -601,7 +600,7 @@ pub fn get_best_working_encoder(
|
||||
while !candidates.is_empty() {
|
||||
let best = get_best_compatible_encoder(&candidates, codec, encoder_type)?;
|
||||
tracing::info!("Testing encoder: {}", best.name,);
|
||||
if test_encoder(&best, zero_copy).is_ok() {
|
||||
if test_encoder(&best).is_ok() {
|
||||
return Ok(best);
|
||||
} else {
|
||||
// Remove this encoder and try next best
|
||||
@@ -613,25 +612,10 @@ pub fn get_best_working_encoder(
|
||||
}
|
||||
|
||||
/// Test if a pipeline with the given encoder can be created and set to Playing
|
||||
pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), Box<dyn Error>> {
|
||||
let src = gstreamer::ElementFactory::make("waylanddisplaysrc").build()?;
|
||||
if let Some(gpu_info) = &encoder.gpu_info {
|
||||
src.set_property_from_str("render-node", gpu_info.render_path());
|
||||
}
|
||||
pub fn test_encoder(encoder: &VideoEncoderInfo) -> Result<(), Box<dyn Error>> {
|
||||
let src = gstreamer::ElementFactory::make("videotestsrc").build()?;
|
||||
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let caps = gstreamer::Caps::from_str(&format!(
|
||||
"{},width=1280,height=720,framerate=30/1{}",
|
||||
if zero_copy {
|
||||
if encoder.encoder_api == EncoderAPI::NVENC {
|
||||
"video/x-raw(memory:CUDAMemory)"
|
||||
} else {
|
||||
"video/x-raw(memory:DMABuf)"
|
||||
}
|
||||
} else {
|
||||
"video/x-raw"
|
||||
},
|
||||
if zero_copy { "" } else { ",format=RGBx" }
|
||||
))?;
|
||||
let caps = gstreamer::Caps::from_str("video/x-raw,width=1280,height=720,framerate=30/1")?;
|
||||
caps_filter.set_property("caps", &caps);
|
||||
|
||||
let enc = gstreamer::ElementFactory::make(&encoder.name).build()?;
|
||||
@@ -642,41 +626,9 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), B
|
||||
// Create pipeline and link elements
|
||||
let pipeline = gstreamer::Pipeline::new();
|
||||
|
||||
if zero_copy {
|
||||
if encoder.encoder_api == EncoderAPI::NVENC {
|
||||
// NVENC zero-copy path
|
||||
pipeline.add_many(&[&src, &caps_filter, &enc, &sink])?;
|
||||
gstreamer::Element::link_many(&[&src, &caps_filter, &enc, &sink])?;
|
||||
} else {
|
||||
// VA-API/QSV zero-copy path
|
||||
let vapostproc = gstreamer::ElementFactory::make("vapostproc").build()?;
|
||||
let va_caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let va_caps = gstreamer::Caps::from_str("video/x-raw(memory:VAMemory),format=NV12")?;
|
||||
va_caps_filter.set_property("caps", &va_caps);
|
||||
|
||||
pipeline.add_many(&[
|
||||
&src,
|
||||
&caps_filter,
|
||||
&vapostproc,
|
||||
&va_caps_filter,
|
||||
&enc,
|
||||
&sink,
|
||||
])?;
|
||||
gstreamer::Element::link_many(&[
|
||||
&src,
|
||||
&caps_filter,
|
||||
&vapostproc,
|
||||
&va_caps_filter,
|
||||
&enc,
|
||||
&sink,
|
||||
])?;
|
||||
}
|
||||
} else {
|
||||
// Non-zero-copy path for all encoders - needs videoconvert
|
||||
let videoconvert = gstreamer::ElementFactory::make("videoconvert").build()?;
|
||||
pipeline.add_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
|
||||
gstreamer::Element::link_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
|
||||
}
|
||||
let videoconvert = gstreamer::ElementFactory::make("videoconvert").build()?;
|
||||
pipeline.add_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
|
||||
gstreamer::Element::link_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
|
||||
|
||||
let bus = pipeline.bus().ok_or("Pipeline has no bus")?;
|
||||
pipeline.set_state(gstreamer::State::Playing)?;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::proto::proto::proto_input::InputType::{
|
||||
ControllerAttach, ControllerAxis, ControllerButton, ControllerDetach, ControllerRumble,
|
||||
ControllerStick, ControllerTrigger,
|
||||
};
|
||||
use crate::proto::proto::ProtoControllerAttach;
|
||||
use crate::proto::proto::proto_message::Payload;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -31,10 +29,8 @@ impl ControllerInput {
|
||||
client: &vimputti::client::VimputtiClient,
|
||||
) -> Result<Self> {
|
||||
let config = controller_string_to_type(&controller_type)?;
|
||||
Ok(Self {
|
||||
config: config.clone(),
|
||||
device: client.create_device(config).await?,
|
||||
})
|
||||
let device = client.create_device(config.clone()).await?;
|
||||
Ok(Self { config, device })
|
||||
}
|
||||
|
||||
pub fn device_mut(&mut self) -> &mut vimputti::client::VirtualController {
|
||||
@@ -48,157 +44,357 @@ impl ControllerInput {
|
||||
|
||||
pub struct ControllerManager {
|
||||
vimputti_client: Arc<vimputti::client::VimputtiClient>,
|
||||
cmd_tx: mpsc::Sender<crate::proto::proto::ProtoInput>,
|
||||
rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>, // (slot, strong, weak, duration_ms)
|
||||
cmd_tx: mpsc::Sender<Payload>,
|
||||
rumble_tx: mpsc::Sender<(u32, u16, u16, u16, String)>, // (slot, strong, weak, duration_ms, session_id)
|
||||
attach_tx: mpsc::Sender<ProtoControllerAttach>,
|
||||
}
|
||||
impl ControllerManager {
|
||||
pub fn new(
|
||||
vimputti_client: Arc<vimputti::client::VimputtiClient>,
|
||||
) -> Result<(Self, mpsc::Receiver<(u32, u16, u16, u16)>)> {
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel(100);
|
||||
let (rumble_tx, rumble_rx) = mpsc::channel(100);
|
||||
) -> Result<(
|
||||
Self,
|
||||
mpsc::Receiver<(u32, u16, u16, u16, String)>,
|
||||
mpsc::Receiver<ProtoControllerAttach>,
|
||||
)> {
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel(512);
|
||||
let (rumble_tx, rumble_rx) = mpsc::channel(256);
|
||||
let (attach_tx, attach_rx) = mpsc::channel(64);
|
||||
tokio::spawn(command_loop(
|
||||
cmd_rx,
|
||||
vimputti_client.clone(),
|
||||
rumble_tx.clone(),
|
||||
attach_tx.clone(),
|
||||
));
|
||||
Ok((
|
||||
Self {
|
||||
vimputti_client,
|
||||
cmd_tx,
|
||||
rumble_tx,
|
||||
attach_tx,
|
||||
},
|
||||
rumble_rx,
|
||||
attach_rx,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn send_command(&self, input: crate::proto::proto::ProtoInput) -> Result<()> {
|
||||
self.cmd_tx.send(input).await?;
|
||||
pub async fn send_command(&self, payload: Payload) -> Result<()> {
|
||||
self.cmd_tx.send(payload).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ControllerSlot {
|
||||
controller: ControllerInput,
|
||||
session_id: String,
|
||||
session_slot: u32,
|
||||
}
|
||||
|
||||
// Returns first free controller slot from 0-16
|
||||
fn get_free_slot(controllers: &HashMap<u32, ControllerSlot>) -> Option<u32> {
|
||||
for slot in 0..17 {
|
||||
if !controllers.contains_key(&slot) {
|
||||
return Some(slot);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn command_loop(
|
||||
mut cmd_rx: mpsc::Receiver<crate::proto::proto::ProtoInput>,
|
||||
mut cmd_rx: mpsc::Receiver<Payload>,
|
||||
vimputti_client: Arc<vimputti::client::VimputtiClient>,
|
||||
rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>,
|
||||
rumble_tx: mpsc::Sender<(u32, u16, u16, u16, String)>,
|
||||
attach_tx: mpsc::Sender<ProtoControllerAttach>,
|
||||
) {
|
||||
let mut controllers: HashMap<u32, ControllerInput> = HashMap::new();
|
||||
while let Some(input) = cmd_rx.recv().await {
|
||||
if let Some(input_type) = input.input_type {
|
||||
match input_type {
|
||||
ControllerAttach(data) => {
|
||||
// Check if controller already exists in the slot, if so, ignore
|
||||
if controllers.contains_key(&(data.slot as u32)) {
|
||||
tracing::warn!(
|
||||
"Controller slot {} already occupied, ignoring attach",
|
||||
data.slot
|
||||
let mut controllers: HashMap<u32, ControllerSlot> = HashMap::new();
|
||||
while let Some(payload) = cmd_rx.recv().await {
|
||||
match payload {
|
||||
Payload::ControllerAttach(data) => {
|
||||
let session_id = data.session_id.clone();
|
||||
let session_slot = data.session_slot.clone();
|
||||
|
||||
// Check if this session already has a slot (reconnection)
|
||||
let existing_slot = controllers
|
||||
.iter()
|
||||
.find(|(_, slot)| {
|
||||
slot.session_id == session_id && slot.session_slot == session_slot as u32
|
||||
})
|
||||
.map(|(slot_num, _)| *slot_num);
|
||||
|
||||
if let Some(existing_slot) = existing_slot {
|
||||
if let Some(controller_slot) = controllers.get_mut(&existing_slot) {
|
||||
let rumble_tx = rumble_tx.clone();
|
||||
let attach_tx = attach_tx.clone();
|
||||
|
||||
controller_slot
|
||||
.controller
|
||||
.device_mut()
|
||||
.on_rumble(move |strong, weak, duration_ms| {
|
||||
let _ = rumble_tx.try_send((
|
||||
existing_slot,
|
||||
strong,
|
||||
weak,
|
||||
duration_ms,
|
||||
data.session_id.clone(),
|
||||
));
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Failed to register rumble callback for slot {}: {}",
|
||||
existing_slot,
|
||||
e
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Return to attach_tx what slot was assigned
|
||||
let attach_info = ProtoControllerAttach {
|
||||
id: data.id.clone(),
|
||||
session_slot: existing_slot as i32,
|
||||
session_id: session_id.clone(),
|
||||
};
|
||||
|
||||
match attach_tx.send(attach_info).await {
|
||||
Ok(_) => {
|
||||
tracing::info!(
|
||||
"Controller {} re-attached to slot {} (session: {})",
|
||||
data.id,
|
||||
existing_slot,
|
||||
session_id
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to send re-attach info for slot {}: {}",
|
||||
existing_slot,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(slot) = get_free_slot(&controllers) {
|
||||
if let Ok(mut controller) =
|
||||
ControllerInput::new(data.id.clone(), &vimputti_client).await
|
||||
{
|
||||
let rumble_tx = rumble_tx.clone();
|
||||
let attach_tx = attach_tx.clone();
|
||||
|
||||
controller
|
||||
.device_mut()
|
||||
.on_rumble(move |strong, weak, duration_ms| {
|
||||
let _ = rumble_tx.try_send((
|
||||
slot,
|
||||
strong,
|
||||
weak,
|
||||
duration_ms,
|
||||
data.session_id.clone(),
|
||||
));
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Failed to register rumble callback for slot {}: {}",
|
||||
slot,
|
||||
e
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Return to attach_tx what slot was assigned
|
||||
let attach_info = ProtoControllerAttach {
|
||||
id: data.id.clone(),
|
||||
session_slot: slot as i32,
|
||||
session_id: session_id.clone(),
|
||||
};
|
||||
|
||||
match attach_tx.send(attach_info).await {
|
||||
Ok(_) => {
|
||||
controllers.insert(
|
||||
slot,
|
||||
ControllerSlot {
|
||||
controller,
|
||||
session_id: session_id.clone(),
|
||||
session_slot: session_slot.clone() as u32,
|
||||
},
|
||||
);
|
||||
tracing::info!(
|
||||
"Controller {} attached to slot {} (session: {})",
|
||||
data.id,
|
||||
slot,
|
||||
session_id
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to send attach info for slot {}: {}",
|
||||
slot,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to create controller of type {} for slot {}",
|
||||
data.id,
|
||||
slot
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Payload::ControllerDetach(data) => {
|
||||
if controllers.remove(&(data.session_slot as u32)).is_some() {
|
||||
tracing::info!("Controller detached from slot {}", data.session_slot);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"No controller found in slot {} to detach",
|
||||
data.session_slot
|
||||
);
|
||||
}
|
||||
}
|
||||
Payload::ClientDisconnected(data) => {
|
||||
tracing::info!(
|
||||
"Client disconnected, cleaning up controller slots: {:?} (client session: {})",
|
||||
data.controller_slots,
|
||||
data.session_id
|
||||
);
|
||||
// Remove all controllers for the disconnected slots
|
||||
for slot in &data.controller_slots {
|
||||
if controllers.remove(&(*slot as u32)).is_some() {
|
||||
tracing::info!(
|
||||
"Removed controller from slot {} (client session: {})",
|
||||
slot,
|
||||
data.session_id
|
||||
);
|
||||
} else {
|
||||
if let Ok(mut controller) =
|
||||
ControllerInput::new(data.id.clone(), &vimputti_client).await
|
||||
{
|
||||
let slot = data.slot as u32;
|
||||
let rumble_tx = rumble_tx.clone();
|
||||
tracing::warn!(
|
||||
"No controller found in slot {} to cleanup (client session: {})",
|
||||
slot,
|
||||
data.session_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Payload::ControllerStateBatch(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
|
||||
let device = controller.controller.device();
|
||||
|
||||
controller
|
||||
.device_mut()
|
||||
.on_rumble(move |strong, weak, duration_ms| {
|
||||
let _ = rumble_tx.try_send((slot, strong, weak, duration_ms));
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Failed to register rumble callback for slot {}: {}",
|
||||
slot,
|
||||
e
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
controllers.insert(data.slot as u32, controller);
|
||||
tracing::info!("Controller {} attached to slot {}", data.id, data.slot);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to create controller of type {} for slot {}",
|
||||
data.id,
|
||||
data.slot
|
||||
);
|
||||
// Handle inputs based on update type
|
||||
if data.update_type == 0 {
|
||||
// FULL_STATE: Update all values
|
||||
let _ = device.sync().await;
|
||||
for (btn_code, pressed) in data.button_changed_mask {
|
||||
if let Some(button) = vimputti::Button::from_ev_code(btn_code as u16) {
|
||||
let _ = device.button(button, pressed).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ControllerDetach(data) => {
|
||||
if controllers.remove(&(data.slot as u32)).is_some() {
|
||||
tracing::info!("Controller detached from slot {}", data.slot);
|
||||
} else {
|
||||
tracing::warn!("No controller found in slot {} to detach", data.slot);
|
||||
}
|
||||
}
|
||||
ControllerButton(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.slot as u32)) {
|
||||
if let Some(button) = vimputti::Button::from_ev_code(data.button as u16) {
|
||||
let device = controller.device();
|
||||
device.button(button, data.pressed);
|
||||
device.sync();
|
||||
if let Some(x) = data.left_stick_x {
|
||||
let _ = device.axis(vimputti::Axis::LeftStickX, x).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
if let Some(y) = data.left_stick_y {
|
||||
let _ = device.axis(vimputti::Axis::LeftStickY, y).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
if let Some(x) = data.right_stick_x {
|
||||
let _ = device.axis(vimputti::Axis::RightStickX, x).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
if let Some(y) = data.right_stick_y {
|
||||
let _ = device.axis(vimputti::Axis::RightStickY, y).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
if let Some(value) = data.left_trigger {
|
||||
let _ = device.axis(vimputti::Axis::LowerLeftTrigger, value).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
if let Some(value) = data.right_trigger {
|
||||
let _ = device.axis(vimputti::Axis::LowerRightTrigger, value).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
if let Some(x) = data.dpad_x {
|
||||
let _ = device.axis(vimputti::Axis::DPadX, x).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
if let Some(y) = data.dpad_y {
|
||||
let _ = device.axis(vimputti::Axis::DPadY, y).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Controller slot {} not found for button event", data.slot);
|
||||
}
|
||||
}
|
||||
ControllerStick(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.slot as u32)) {
|
||||
let device = controller.device();
|
||||
if data.stick == 0 {
|
||||
// Left stick
|
||||
device.axis(vimputti::Axis::LeftStickX, data.x);
|
||||
device.sync();
|
||||
device.axis(vimputti::Axis::LeftStickY, data.y);
|
||||
} else if data.stick == 1 {
|
||||
// Right stick
|
||||
device.axis(vimputti::Axis::RightStickX, data.x);
|
||||
device.sync();
|
||||
device.axis(vimputti::Axis::RightStickY, data.y);
|
||||
// DELTA: Only update changed values
|
||||
if let Some(changed_fields) = data.changed_fields {
|
||||
let _ = device.sync().await;
|
||||
if (changed_fields & (1 << 0)) != 0 {
|
||||
for (btn_code, pressed) in data.button_changed_mask {
|
||||
if let Some(button) =
|
||||
vimputti::Button::from_ev_code(btn_code as u16)
|
||||
{
|
||||
let _ = device.button(button, pressed).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed_fields & (1 << 1)) != 0 {
|
||||
if let Some(x) = data.left_stick_x {
|
||||
let _ = device.axis(vimputti::Axis::LeftStickX, x).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
if (changed_fields & (1 << 2)) != 0 {
|
||||
if let Some(y) = data.left_stick_y {
|
||||
let _ = device.axis(vimputti::Axis::LeftStickY, y).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
if (changed_fields & (1 << 3)) != 0 {
|
||||
if let Some(x) = data.right_stick_x {
|
||||
let _ = device.axis(vimputti::Axis::RightStickX, x).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
if (changed_fields & (1 << 4)) != 0 {
|
||||
if let Some(y) = data.right_stick_y {
|
||||
let _ = device.axis(vimputti::Axis::RightStickY, y).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
if (changed_fields & (1 << 5)) != 0 {
|
||||
if let Some(value) = data.left_trigger {
|
||||
let _ =
|
||||
device.axis(vimputti::Axis::LowerLeftTrigger, value).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
if (changed_fields & (1 << 6)) != 0 {
|
||||
if let Some(value) = data.right_trigger {
|
||||
let _ =
|
||||
device.axis(vimputti::Axis::LowerRightTrigger, value).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
if (changed_fields & (1 << 7)) != 0 {
|
||||
if let Some(x) = data.dpad_x {
|
||||
let _ = device.axis(vimputti::Axis::DPadX, x).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
if (changed_fields & (1 << 8)) != 0 {
|
||||
if let Some(y) = data.dpad_y {
|
||||
let _ = device.axis(vimputti::Axis::DPadY, y).await;
|
||||
let _ = device.sync().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
device.sync();
|
||||
} else {
|
||||
tracing::warn!("Controller slot {} not found for stick event", data.slot);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Controller slot {} not found for state batch event",
|
||||
data.session_slot
|
||||
);
|
||||
}
|
||||
ControllerTrigger(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.slot as u32)) {
|
||||
let device = controller.device();
|
||||
if data.trigger == 0 {
|
||||
// Left trigger
|
||||
device.axis(vimputti::Axis::LowerLeftTrigger, data.value);
|
||||
} else if data.trigger == 1 {
|
||||
// Right trigger
|
||||
device.axis(vimputti::Axis::LowerRightTrigger, data.value);
|
||||
}
|
||||
device.sync();
|
||||
} else {
|
||||
tracing::warn!("Controller slot {} not found for trigger event", data.slot);
|
||||
}
|
||||
}
|
||||
ControllerAxis(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.slot as u32)) {
|
||||
let device = controller.device();
|
||||
if data.axis == 0 {
|
||||
// dpad x
|
||||
device.axis(vimputti::Axis::DPadX, data.value);
|
||||
} else if data.axis == 1 {
|
||||
// dpad y
|
||||
device.axis(vimputti::Axis::DPadY, data.value);
|
||||
}
|
||||
device.sync();
|
||||
}
|
||||
}
|
||||
// Rumble will be outgoing event..
|
||||
ControllerRumble(_) => {
|
||||
//no-op
|
||||
}
|
||||
_ => {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ mod enc_helper;
|
||||
mod gpu;
|
||||
mod input;
|
||||
mod latency;
|
||||
mod messages;
|
||||
mod nestrisink;
|
||||
mod p2p;
|
||||
mod proto;
|
||||
@@ -25,7 +24,7 @@ use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
|
||||
// Handles gathering GPU information and selecting the most suitable GPU
|
||||
fn handle_gpus(args: &args::Args) -> Result<Vec<gpu::GPUInfo>, Box<dyn Error>> {
|
||||
fn handle_gpus(args: &args::Args) -> Result<Vec<GPUInfo>, Box<dyn Error>> {
|
||||
tracing::info!("Gathering GPU information..");
|
||||
let mut gpus = gpu::get_gpus()?;
|
||||
if gpus.is_empty() {
|
||||
@@ -120,7 +119,6 @@ fn handle_encoder_video(
|
||||
&video_encoders,
|
||||
&args.encoding.video.codec,
|
||||
&args.encoding.video.encoder_type,
|
||||
args.app.zero_copy,
|
||||
)?;
|
||||
}
|
||||
tracing::info!("Selected video encoder: '{}'", video_encoder.name);
|
||||
@@ -257,11 +255,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
None
|
||||
}
|
||||
};
|
||||
let (controller_manager, rumble_rx) = if let Some(vclient) = vimputti_client {
|
||||
let (controller_manager, rumble_rx) = ControllerManager::new(vclient)?;
|
||||
(Some(Arc::new(controller_manager)), Some(rumble_rx))
|
||||
let (controller_manager, rumble_rx, attach_rx) = if let Some(vclient) = vimputti_client {
|
||||
let (controller_manager, rumble_rx, attach_rx) = ControllerManager::new(vclient)?;
|
||||
(
|
||||
Some(Arc::new(controller_manager)),
|
||||
Some(rumble_rx),
|
||||
Some(attach_rx),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
/*** PIPELINE CREATION ***/
|
||||
@@ -320,7 +322,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
/* Video */
|
||||
// Video Source Element
|
||||
let video_source = Arc::new(gstreamer::ElementFactory::make("waylanddisplaysrc").build()?);
|
||||
if let Some(gpu_info) = &video_encoder_info.gpu_info {
|
||||
if args.app.software_render {
|
||||
video_source.set_property_from_str("render-node", "software");
|
||||
} else if let Some(gpu_info) = &video_encoder_info.gpu_info {
|
||||
video_source.set_property_from_str("render-node", gpu_info.render_path());
|
||||
}
|
||||
|
||||
@@ -416,6 +420,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
video_source.clone(),
|
||||
controller_manager,
|
||||
rumble_rx,
|
||||
attach_rx,
|
||||
)
|
||||
.await?;
|
||||
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone()));
|
||||
@@ -424,20 +429,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
webrtcsink.set_property("do-retransmission", false);
|
||||
|
||||
/* Queues */
|
||||
let video_source_queue = gstreamer::ElementFactory::make("queue")
|
||||
.property("max-size-buffers", 5u32)
|
||||
.build()?;
|
||||
|
||||
let audio_source_queue = gstreamer::ElementFactory::make("queue")
|
||||
.property("max-size-buffers", 5u32)
|
||||
.build()?;
|
||||
|
||||
let video_queue = gstreamer::ElementFactory::make("queue")
|
||||
.property("max-size-buffers", 5u32)
|
||||
.property("max-size-buffers", 2u32)
|
||||
.property("max-size-time", 0u64)
|
||||
.property("max-size-bytes", 0u32)
|
||||
.build()?;
|
||||
|
||||
let audio_queue = gstreamer::ElementFactory::make("queue")
|
||||
.property("max-size-buffers", 5u32)
|
||||
.property("max-size-buffers", 2u32)
|
||||
.property("max-size-time", 0u64)
|
||||
.property("max-size-bytes", 0u32)
|
||||
.build()?;
|
||||
|
||||
/* Clock Sync */
|
||||
@@ -456,7 +457,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
&caps_filter,
|
||||
&video_queue,
|
||||
&video_clocksync,
|
||||
&video_source_queue,
|
||||
&video_source,
|
||||
&audio_encoder,
|
||||
&audio_capsfilter,
|
||||
@@ -464,7 +464,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
&audio_clocksync,
|
||||
&audio_rate,
|
||||
&audio_converter,
|
||||
&audio_source_queue,
|
||||
&audio_source,
|
||||
])?;
|
||||
|
||||
@@ -491,7 +490,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Link main audio branch
|
||||
gstreamer::Element::link_many(&[
|
||||
&audio_source,
|
||||
&audio_source_queue,
|
||||
&audio_converter,
|
||||
&audio_rate,
|
||||
&audio_capsfilter,
|
||||
@@ -513,7 +511,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) {
|
||||
gstreamer::Element::link_many(&[
|
||||
&video_source,
|
||||
&video_source_queue,
|
||||
&caps_filter,
|
||||
&video_queue,
|
||||
&video_clocksync,
|
||||
@@ -525,7 +522,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
// NVENC pipeline
|
||||
gstreamer::Element::link_many(&[
|
||||
&video_source,
|
||||
&video_source_queue,
|
||||
&caps_filter,
|
||||
&video_encoder,
|
||||
])?;
|
||||
@@ -533,7 +529,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
} else {
|
||||
gstreamer::Element::link_many(&[
|
||||
&video_source,
|
||||
&video_source_queue,
|
||||
&caps_filter,
|
||||
&video_queue,
|
||||
&video_clocksync,
|
||||
@@ -550,7 +545,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
|
||||
// Make sure QOS is disabled to avoid latency
|
||||
video_encoder.set_property("qos", false);
|
||||
video_encoder.set_property("qos", true);
|
||||
|
||||
// Optimize latency of pipeline
|
||||
video_source
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use crate::latency::LatencyTracker;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit;
|
||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MessageBase {
|
||||
pub payload_type: String,
|
||||
pub latency: Option<LatencyTracker>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MessageRaw {
|
||||
#[serde(flatten)]
|
||||
pub base: MessageBase,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MessageLog {
|
||||
#[serde(flatten)]
|
||||
pub base: MessageBase,
|
||||
pub level: String,
|
||||
pub message: String,
|
||||
pub time: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MessageMetrics {
|
||||
#[serde(flatten)]
|
||||
pub base: MessageBase,
|
||||
pub usage_cpu: f64,
|
||||
pub usage_memory: f64,
|
||||
pub uptime: u64,
|
||||
pub pipeline_latency: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MessageICE {
|
||||
#[serde(flatten)]
|
||||
pub base: MessageBase,
|
||||
pub candidate: RTCIceCandidateInit,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MessageSDP {
|
||||
#[serde(flatten)]
|
||||
pub base: MessageBase,
|
||||
pub sdp: RTCSessionDescription,
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::input::controller::ControllerManager;
|
||||
use crate::messages::{MessageBase, MessageICE, MessageRaw, MessageSDP};
|
||||
use crate::p2p::p2p::NestriConnection;
|
||||
use crate::p2p::p2p_protocol_stream::NestriStreamProtocol;
|
||||
use crate::proto::proto::proto_input::InputType::{
|
||||
KeyDown, KeyUp, MouseKeyDown, MouseKeyUp, MouseMove, MouseMoveAbs, MouseWheel,
|
||||
use crate::proto::proto::proto_message::Payload;
|
||||
use crate::proto::proto::{
|
||||
ProtoControllerAttach, ProtoControllerRumble, ProtoIce, ProtoMessage, ProtoSdp,
|
||||
ProtoServerPushStream, RtcIceCandidateInit, RtcSessionDescriptionInit,
|
||||
};
|
||||
use crate::proto::proto::{ProtoInput, ProtoMessageInput};
|
||||
use anyhow::Result;
|
||||
use glib::subclass::prelude::*;
|
||||
use gstreamer::glib;
|
||||
@@ -16,8 +16,6 @@ use parking_lot::RwLock as PLRwLock;
|
||||
use prost::Message;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit;
|
||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||
|
||||
pub struct Signaller {
|
||||
stream_room: PLRwLock<Option<String>>,
|
||||
@@ -25,7 +23,8 @@ pub struct Signaller {
|
||||
wayland_src: PLRwLock<Option<Arc<gstreamer::Element>>>,
|
||||
data_channel: PLRwLock<Option<Arc<gstreamer_webrtc::WebRTCDataChannel>>>,
|
||||
controller_manager: PLRwLock<Option<Arc<ControllerManager>>>,
|
||||
rumble_rx: Mutex<Option<mpsc::Receiver<(u32, u16, u16, u16)>>>,
|
||||
rumble_rx: Mutex<Option<mpsc::Receiver<(u32, u16, u16, u16, String)>>>,
|
||||
attach_rx: Mutex<Option<mpsc::Receiver<ProtoControllerAttach>>>,
|
||||
}
|
||||
impl Default for Signaller {
|
||||
fn default() -> Self {
|
||||
@@ -36,6 +35,7 @@ impl Default for Signaller {
|
||||
data_channel: PLRwLock::new(None),
|
||||
controller_manager: PLRwLock::new(None),
|
||||
rumble_rx: Mutex::new(None),
|
||||
attach_rx: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,15 +70,27 @@ impl Signaller {
|
||||
self.controller_manager.read().clone()
|
||||
}
|
||||
|
||||
pub async fn set_rumble_rx(&self, rumble_rx: mpsc::Receiver<(u32, u16, u16, u16)>) {
|
||||
pub async fn set_rumble_rx(&self, rumble_rx: mpsc::Receiver<(u32, u16, u16, u16, String)>) {
|
||||
*self.rumble_rx.lock().await = Some(rumble_rx);
|
||||
}
|
||||
|
||||
// Change getter to take ownership:
|
||||
pub async fn take_rumble_rx(&self) -> Option<mpsc::Receiver<(u32, u16, u16, u16)>> {
|
||||
pub async fn take_rumble_rx(&self) -> Option<mpsc::Receiver<(u32, u16, u16, u16, String)>> {
|
||||
self.rumble_rx.lock().await.take()
|
||||
}
|
||||
|
||||
pub async fn set_attach_rx(
|
||||
&self,
|
||||
attach_rx: mpsc::Receiver<crate::proto::proto::ProtoControllerAttach>,
|
||||
) {
|
||||
*self.attach_rx.lock().await = Some(attach_rx);
|
||||
}
|
||||
|
||||
pub async fn take_attach_rx(
|
||||
&self,
|
||||
) -> Option<mpsc::Receiver<crate::proto::proto::ProtoControllerAttach>> {
|
||||
self.attach_rx.lock().await.take()
|
||||
}
|
||||
|
||||
pub fn set_data_channel(&self, data_channel: gstreamer_webrtc::WebRTCDataChannel) {
|
||||
*self.data_channel.write() = Some(Arc::new(data_channel));
|
||||
}
|
||||
@@ -95,68 +107,85 @@ impl Signaller {
|
||||
};
|
||||
{
|
||||
let self_obj = self.obj().clone();
|
||||
stream_protocol.register_callback("answer", move |data| {
|
||||
if let Ok(message) = serde_json::from_slice::<MessageSDP>(&data) {
|
||||
let sdp = gst_sdp::SDPMessage::parse_buffer(message.sdp.sdp.as_bytes())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid SDP in 'answer': {e:?}"))?;
|
||||
let answer = WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp);
|
||||
Ok(self_obj.emit_by_name::<()>(
|
||||
"session-description",
|
||||
&[&"unique-session-id", &answer],
|
||||
))
|
||||
stream_protocol.register_callback("answer", move |msg| {
|
||||
if let Some(payload) = msg.payload {
|
||||
match payload {
|
||||
Payload::Sdp(sdp) => {
|
||||
if let Some(sdp) = sdp.sdp {
|
||||
let sdp = gst_sdp::SDPMessage::parse_buffer(sdp.sdp.as_bytes())
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!("Invalid SDP in 'answer': {e:?}")
|
||||
})?;
|
||||
let answer =
|
||||
WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp);
|
||||
return Ok(self_obj.emit_by_name::<()>(
|
||||
"session-description",
|
||||
&[&"unique-session-id", &answer],
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Unexpected payload type for answer");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("Failed to decode SDP message");
|
||||
anyhow::bail!("Failed to decode answer message");
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
{
|
||||
let self_obj = self.obj().clone();
|
||||
stream_protocol.register_callback("ice-candidate", move |data| {
|
||||
if let Ok(message) = serde_json::from_slice::<MessageICE>(&data) {
|
||||
let candidate = message.candidate;
|
||||
let sdp_m_line_index = candidate.sdp_mline_index.unwrap_or(0) as u32;
|
||||
let sdp_mid = candidate.sdp_mid;
|
||||
Ok(self_obj.emit_by_name::<()>(
|
||||
"handle-ice",
|
||||
&[
|
||||
&"unique-session-id",
|
||||
&sdp_m_line_index,
|
||||
&sdp_mid,
|
||||
&candidate.candidate,
|
||||
],
|
||||
))
|
||||
stream_protocol.register_callback("ice-candidate", move |msg| {
|
||||
if let Some(payload) = msg.payload {
|
||||
match payload {
|
||||
Payload::Ice(ice) => {
|
||||
if let Some(candidate) = ice.candidate {
|
||||
let sdp_m_line_index = candidate.sdp_m_line_index.unwrap_or(0);
|
||||
return Ok(self_obj.emit_by_name::<()>(
|
||||
"handle-ice",
|
||||
&[
|
||||
&"unique-session-id",
|
||||
&sdp_m_line_index,
|
||||
&candidate.sdp_mid,
|
||||
&candidate.candidate,
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Unexpected payload type for ice-candidate");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("Failed to decode ICE message");
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
{
|
||||
let self_obj = self.obj().clone();
|
||||
stream_protocol.register_callback("push-stream-ok", move |data| {
|
||||
if let Ok(answer) = serde_json::from_slice::<MessageRaw>(&data) {
|
||||
// Decode room name string
|
||||
if let Some(room_name) = answer.data.as_str() {
|
||||
gstreamer::info!(
|
||||
gstreamer::CAT_DEFAULT,
|
||||
"Received OK answer for room: {}",
|
||||
room_name
|
||||
);
|
||||
} else {
|
||||
gstreamer::error!(
|
||||
gstreamer::CAT_DEFAULT,
|
||||
"Failed to decode room name from answer"
|
||||
);
|
||||
}
|
||||
|
||||
// Send our SDP offer
|
||||
Ok(self_obj.emit_by_name::<()>(
|
||||
"session-requested",
|
||||
&[
|
||||
&"unique-session-id",
|
||||
&"consumer-identifier",
|
||||
&None::<WebRTCSessionDescription>,
|
||||
],
|
||||
))
|
||||
stream_protocol.register_callback("push-stream-ok", move |msg| {
|
||||
if let Some(payload) = msg.payload {
|
||||
return match payload {
|
||||
Payload::ServerPushStream(_res) => {
|
||||
// Send our SDP offer
|
||||
Ok(self_obj.emit_by_name::<()>(
|
||||
"session-requested",
|
||||
&[
|
||||
&"unique-session-id",
|
||||
&"consumer-identifier",
|
||||
&None::<WebRTCSessionDescription>,
|
||||
],
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Unexpected payload type for push-stream-ok");
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
} else {
|
||||
anyhow::bail!("Failed to decode answer");
|
||||
}
|
||||
@@ -200,12 +229,14 @@ impl Signaller {
|
||||
// Spawn async task to take the receiver and set up
|
||||
tokio::spawn(async move {
|
||||
let rumble_rx = signaller.imp().take_rumble_rx().await;
|
||||
let attach_rx = signaller.imp().take_attach_rx().await;
|
||||
let controller_manager =
|
||||
signaller.imp().get_controller_manager();
|
||||
|
||||
setup_data_channel(
|
||||
controller_manager,
|
||||
rumble_rx,
|
||||
attach_rx,
|
||||
data_channel,
|
||||
&wayland_src,
|
||||
);
|
||||
@@ -243,19 +274,18 @@ impl SignallableImpl for Signaller {
|
||||
return;
|
||||
};
|
||||
|
||||
let push_msg = MessageRaw {
|
||||
base: MessageBase {
|
||||
payload_type: "push-stream-room".to_string(),
|
||||
latency: None,
|
||||
},
|
||||
data: serde_json::Value::from(stream_room),
|
||||
};
|
||||
|
||||
let Some(stream_protocol) = self.get_stream_protocol() else {
|
||||
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
|
||||
return;
|
||||
};
|
||||
|
||||
let push_msg = crate::proto::create_message(
|
||||
Payload::ServerPushStream(ProtoServerPushStream {
|
||||
room_name: stream_room,
|
||||
}),
|
||||
"push-stream-room",
|
||||
None,
|
||||
);
|
||||
if let Err(e) = stream_protocol.send_message(&push_msg) {
|
||||
tracing::error!("Failed to send push stream room message: {:?}", e);
|
||||
}
|
||||
@@ -266,20 +296,22 @@ impl SignallableImpl for Signaller {
|
||||
}
|
||||
|
||||
fn send_sdp(&self, _session_id: &str, sdp: &WebRTCSessionDescription) {
|
||||
let sdp_message = MessageSDP {
|
||||
base: MessageBase {
|
||||
payload_type: "offer".to_string(),
|
||||
latency: None,
|
||||
},
|
||||
sdp: RTCSessionDescription::offer(sdp.sdp().as_text().unwrap()).unwrap(),
|
||||
};
|
||||
|
||||
let Some(stream_protocol) = self.get_stream_protocol() else {
|
||||
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = stream_protocol.send_message(&sdp_message) {
|
||||
let sdp_msg = crate::proto::create_message(
|
||||
Payload::Sdp(ProtoSdp {
|
||||
sdp: Some(RtcSessionDescriptionInit {
|
||||
sdp: sdp.sdp().as_text().unwrap(),
|
||||
r#type: "offer".to_string(),
|
||||
}),
|
||||
}),
|
||||
"offer",
|
||||
None,
|
||||
);
|
||||
if let Err(e) = stream_protocol.send_message(&sdp_msg) {
|
||||
tracing::error!("Failed to send SDP message: {:?}", e);
|
||||
}
|
||||
}
|
||||
@@ -291,26 +323,25 @@ impl SignallableImpl for Signaller {
|
||||
sdp_m_line_index: u32,
|
||||
sdp_mid: Option<String>,
|
||||
) {
|
||||
let candidate_init = RTCIceCandidateInit {
|
||||
candidate: candidate.to_string(),
|
||||
sdp_mid,
|
||||
sdp_mline_index: Some(sdp_m_line_index as u16),
|
||||
..Default::default()
|
||||
};
|
||||
let ice_message = MessageICE {
|
||||
base: MessageBase {
|
||||
payload_type: "ice-candidate".to_string(),
|
||||
latency: None,
|
||||
},
|
||||
candidate: candidate_init,
|
||||
};
|
||||
|
||||
let Some(stream_protocol) = self.get_stream_protocol() else {
|
||||
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = stream_protocol.send_message(&ice_message) {
|
||||
let candidate_init = RtcIceCandidateInit {
|
||||
candidate: candidate.to_string(),
|
||||
sdp_mid,
|
||||
sdp_m_line_index: Some(sdp_m_line_index),
|
||||
..Default::default() //username_fragment: Some(session_id.to_string()), TODO: required?
|
||||
};
|
||||
let ice_msg = crate::proto::create_message(
|
||||
Payload::Ice(ProtoIce {
|
||||
candidate: Some(candidate_init),
|
||||
}),
|
||||
"ice-candidate",
|
||||
None,
|
||||
);
|
||||
if let Err(e) = stream_protocol.send_message(&ice_msg) {
|
||||
tracing::error!("Failed to send ICE candidate message: {:?}", e);
|
||||
}
|
||||
}
|
||||
@@ -351,7 +382,8 @@ impl ObjectImpl for Signaller {
|
||||
|
||||
fn setup_data_channel(
|
||||
controller_manager: Option<Arc<ControllerManager>>,
|
||||
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16)>>, // (slot, strong, weak, duration_ms)
|
||||
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16, String)>>, // (slot, strong, weak, duration_ms, session_id)
|
||||
attach_rx: Option<mpsc::Receiver<ProtoControllerAttach>>,
|
||||
data_channel: Arc<gstreamer_webrtc::WebRTCDataChannel>,
|
||||
wayland_src: &gstreamer::Element,
|
||||
) {
|
||||
@@ -361,11 +393,11 @@ fn setup_data_channel(
|
||||
// Spawn async processor
|
||||
tokio::spawn(async move {
|
||||
while let Some(data) = rx.recv().await {
|
||||
match ProtoMessageInput::decode(data.as_slice()) {
|
||||
Ok(message_input) => {
|
||||
if let Some(message_base) = message_input.message_base {
|
||||
match ProtoMessage::decode(data.as_slice()) {
|
||||
Ok(msg_wrapper) => {
|
||||
if let Some(message_base) = msg_wrapper.message_base {
|
||||
if message_base.payload_type == "input" {
|
||||
if let Some(input_data) = message_input.data {
|
||||
if let Some(input_data) = msg_wrapper.payload {
|
||||
if let Some(event) = handle_input_message(input_data) {
|
||||
// Send the event to wayland source, result bool is ignored
|
||||
let _ = wayland_src.send_event(event);
|
||||
@@ -373,7 +405,7 @@ fn setup_data_channel(
|
||||
}
|
||||
} else if message_base.payload_type == "controllerInput" {
|
||||
if let Some(controller_manager) = &controller_manager {
|
||||
if let Some(input_data) = message_input.data {
|
||||
if let Some(input_data) = msg_wrapper.payload {
|
||||
let _ = controller_manager.send_command(input_data).await;
|
||||
}
|
||||
}
|
||||
@@ -391,26 +423,18 @@ fn setup_data_channel(
|
||||
if let Some(mut rumble_rx) = rumble_rx {
|
||||
let data_channel_clone = data_channel.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some((slot, strong, weak, duration_ms)) = rumble_rx.recv().await {
|
||||
let rumble_msg = ProtoMessageInput {
|
||||
message_base: Some(crate::proto::proto::ProtoMessageBase {
|
||||
payload_type: "controllerInput".to_string(),
|
||||
latency: None,
|
||||
while let Some((slot, strong, weak, duration_ms, session_id)) = rumble_rx.recv().await {
|
||||
let rumble_msg = crate::proto::create_message(
|
||||
Payload::ControllerRumble(ProtoControllerRumble {
|
||||
session_slot: slot as i32,
|
||||
session_id: session_id,
|
||||
low_frequency: weak as i32,
|
||||
high_frequency: strong as i32,
|
||||
duration: duration_ms as i32,
|
||||
}),
|
||||
data: Some(ProtoInput {
|
||||
input_type: Some(
|
||||
crate::proto::proto::proto_input::InputType::ControllerRumble(
|
||||
crate::proto::proto::ProtoControllerRumble {
|
||||
r#type: "ControllerRumble".to_string(),
|
||||
slot: slot as i32,
|
||||
low_frequency: weak as i32,
|
||||
high_frequency: strong as i32,
|
||||
duration: duration_ms as i32,
|
||||
},
|
||||
),
|
||||
),
|
||||
}),
|
||||
};
|
||||
"controllerInput",
|
||||
None,
|
||||
);
|
||||
|
||||
let data = rumble_msg.encode_to_vec();
|
||||
let bytes = glib::Bytes::from_owned(data);
|
||||
@@ -422,6 +446,27 @@ fn setup_data_channel(
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn attach sender
|
||||
if let Some(mut attach_rx) = attach_rx {
|
||||
let data_channel_clone = data_channel.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(attach_msg) = attach_rx.recv().await {
|
||||
let proto_msg = crate::proto::create_message(
|
||||
Payload::ControllerAttach(attach_msg),
|
||||
"controllerInput",
|
||||
None,
|
||||
);
|
||||
|
||||
let data = proto_msg.encode_to_vec();
|
||||
let bytes = glib::Bytes::from_owned(data);
|
||||
|
||||
if let Err(e) = data_channel_clone.send_data_full(Some(&bytes)) {
|
||||
tracing::warn!("Failed to send controller attach data: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
data_channel.connect_on_message_data(move |_data_channel, data| {
|
||||
if let Some(data) = data {
|
||||
let _ = tx.send(data.to_vec());
|
||||
@@ -429,68 +474,64 @@ fn setup_data_channel(
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_input_message(input_msg: ProtoInput) -> Option<gstreamer::Event> {
|
||||
if let Some(input_type) = input_msg.input_type {
|
||||
match input_type {
|
||||
MouseMove(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseMoveRelative")
|
||||
.field("pointer_x", data.x as f64)
|
||||
.field("pointer_y", data.y as f64)
|
||||
.build();
|
||||
fn handle_input_message(payload: Payload) -> Option<gstreamer::Event> {
|
||||
match payload {
|
||||
Payload::MouseMove(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseMoveRelative")
|
||||
.field("pointer_x", data.x as f64)
|
||||
.field("pointer_y", data.y as f64)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
MouseMoveAbs(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseMoveAbsolute")
|
||||
.field("pointer_x", data.x as f64)
|
||||
.field("pointer_y", data.y as f64)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
KeyDown(data) => {
|
||||
let structure = gstreamer::Structure::builder("KeyboardKey")
|
||||
.field("key", data.key as u32)
|
||||
.field("pressed", true)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
KeyUp(data) => {
|
||||
let structure = gstreamer::Structure::builder("KeyboardKey")
|
||||
.field("key", data.key as u32)
|
||||
.field("pressed", false)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
MouseWheel(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseAxis")
|
||||
.field("x", data.x as f64)
|
||||
.field("y", data.y as f64)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
MouseKeyDown(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseButton")
|
||||
.field("button", data.key as u32)
|
||||
.field("pressed", true)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
MouseKeyUp(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseButton")
|
||||
.field("button", data.key as u32)
|
||||
.field("pressed", false)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
_ => None,
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
Payload::MouseMoveAbs(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseMoveAbsolute")
|
||||
.field("pointer_x", data.x as f64)
|
||||
.field("pointer_y", data.y as f64)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
Payload::KeyDown(data) => {
|
||||
let structure = gstreamer::Structure::builder("KeyboardKey")
|
||||
.field("key", data.key as u32)
|
||||
.field("pressed", true)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
Payload::KeyUp(data) => {
|
||||
let structure = gstreamer::Structure::builder("KeyboardKey")
|
||||
.field("key", data.key as u32)
|
||||
.field("pressed", false)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
Payload::MouseWheel(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseAxis")
|
||||
.field("x", data.x as f64)
|
||||
.field("y", data.y as f64)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
Payload::MouseKeyDown(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseButton")
|
||||
.field("button", data.key as u32)
|
||||
.field("pressed", true)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
Payload::MouseKeyUp(data) => {
|
||||
let structure = gstreamer::Structure::builder("MouseButton")
|
||||
.field("button", data.key as u32)
|
||||
.field("pressed", false)
|
||||
.build();
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ impl NestriSignaller {
|
||||
nestri_conn: NestriConnection,
|
||||
wayland_src: Arc<gstreamer::Element>,
|
||||
controller_manager: Option<Arc<ControllerManager>>,
|
||||
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16)>>,
|
||||
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16, String)>>,
|
||||
attach_rx: Option<mpsc::Receiver<crate::proto::proto::ProtoControllerAttach>>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let obj: Self = glib::Object::new();
|
||||
obj.imp().set_stream_room(room);
|
||||
@@ -30,6 +31,9 @@ impl NestriSignaller {
|
||||
if let Some(rumble_rx) = rumble_rx {
|
||||
obj.imp().set_rumble_rx(rumble_rx).await;
|
||||
}
|
||||
if let Some(attach_rx) = attach_rx {
|
||||
obj.imp().set_attach_rx(attach_rx).await;
|
||||
}
|
||||
Ok(obj)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,22 @@ use crate::p2p::p2p_safestream::SafeStream;
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use libp2p::StreamProtocol;
|
||||
use prost::Message;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// Cloneable callback type
|
||||
pub type CallbackInner = dyn Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static;
|
||||
pub type CallbackInner = dyn Fn(crate::proto::proto::ProtoMessage) -> Result<()> + Send + Sync + 'static;
|
||||
pub struct Callback(Arc<CallbackInner>);
|
||||
impl Callback {
|
||||
pub fn new<F>(f: F) -> Self
|
||||
where
|
||||
F: Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static,
|
||||
F: Fn(crate::proto::proto::ProtoMessage) -> Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
Callback(Arc::new(f))
|
||||
}
|
||||
|
||||
pub fn call(&self, data: Vec<u8>) -> Result<()> {
|
||||
pub fn call(&self, data: crate::proto::proto::ProtoMessage) -> Result<()> {
|
||||
self.0(data)
|
||||
}
|
||||
}
|
||||
@@ -104,26 +105,31 @@ impl NestriStreamProtocol {
|
||||
}
|
||||
};
|
||||
|
||||
match serde_json::from_slice::<crate::messages::MessageBase>(&data) {
|
||||
Ok(base_message) => {
|
||||
let response_type = base_message.payload_type;
|
||||
match crate::proto::proto::ProtoMessage::decode(data.as_slice()) {
|
||||
Ok(message) => {
|
||||
if let Some(base_message) = &message.message_base {
|
||||
let response_type = &base_message.payload_type;
|
||||
let response_type = response_type.clone();
|
||||
|
||||
// With DashMap, we don't need explicit locking
|
||||
// we just get the callback directly if it exists
|
||||
if let Some(callback) = callbacks.get(&response_type) {
|
||||
// Execute the callback
|
||||
if let Err(e) = callback.call(data.clone()) {
|
||||
tracing::error!(
|
||||
"Callback for response type '{}' errored: {:?}",
|
||||
response_type,
|
||||
e
|
||||
// With DashMap, we don't need explicit locking
|
||||
// we just get the callback directly if it exists
|
||||
if let Some(callback) = callbacks.get(&response_type) {
|
||||
// Execute the callback
|
||||
if let Err(e) = callback.call(message) {
|
||||
tracing::error!(
|
||||
"Callback for response type '{}' errored: {:?}",
|
||||
response_type,
|
||||
e
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"No callback registered for response type: {}",
|
||||
response_type
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"No callback registered for response type: {}",
|
||||
response_type
|
||||
);
|
||||
tracing::error!("No base message in decoded protobuf message",);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -154,8 +160,9 @@ impl NestriStreamProtocol {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_message<M: serde::Serialize>(&self, message: &M) -> Result<()> {
|
||||
let json_data = serde_json::to_vec(message)?;
|
||||
pub fn send_message(&self, message: &crate::proto::proto::ProtoMessage) -> Result<()> {
|
||||
let mut buf = Vec::new();
|
||||
message.encode(&mut buf)?;
|
||||
let Some(tx) = &self.tx else {
|
||||
return Err(anyhow::Error::msg(
|
||||
if self.read_handle.is_none() && self.write_handle.is_none() {
|
||||
@@ -165,13 +172,13 @@ impl NestriStreamProtocol {
|
||||
},
|
||||
));
|
||||
};
|
||||
tx.try_send(json_data)?;
|
||||
tx.try_send(buf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register_callback<F>(&self, response_type: &str, callback: F)
|
||||
where
|
||||
F: Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static,
|
||||
F: Fn(crate::proto::proto::ProtoMessage) -> Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
self.callbacks
|
||||
.insert(response_type.to_string(), Callback::new(callback));
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use libp2p::futures::io::{ReadHalf, WriteHalf};
|
||||
use libp2p::futures::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const MAX_SIZE: usize = 1024 * 1024; // 1MB
|
||||
use unsigned_varint::{decode, encode};
|
||||
|
||||
pub struct SafeStream {
|
||||
stream_read: Arc<Mutex<ReadHalf<libp2p::Stream>>>,
|
||||
@@ -29,34 +27,52 @@ impl SafeStream {
|
||||
}
|
||||
|
||||
async fn send_with_length_prefix(&self, data: &[u8]) -> Result<()> {
|
||||
if data.len() > MAX_SIZE {
|
||||
anyhow::bail!("Data exceeds maximum size");
|
||||
}
|
||||
|
||||
let mut buffer = Vec::with_capacity(4 + data.len());
|
||||
buffer.extend_from_slice(&(data.len() as u32).to_be_bytes()); // Length prefix
|
||||
buffer.extend_from_slice(data); // Payload
|
||||
|
||||
let mut stream_write = self.stream_write.lock().await;
|
||||
stream_write.write_all(&buffer).await?; // Single write
|
||||
|
||||
// Encode length as varint
|
||||
let mut length_buf = encode::usize_buffer();
|
||||
let length_bytes = encode::usize(data.len(), &mut length_buf);
|
||||
|
||||
// Write varint length prefix
|
||||
stream_write.write_all(length_bytes).await?;
|
||||
|
||||
// Write payload
|
||||
stream_write.write_all(data).await?;
|
||||
stream_write.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_with_length_prefix(&self) -> Result<Vec<u8>> {
|
||||
let mut stream_read = self.stream_read.lock().await;
|
||||
|
||||
// Read length prefix + data in one syscall
|
||||
let mut length_prefix = [0u8; 4];
|
||||
stream_read.read_exact(&mut length_prefix).await?;
|
||||
let length = BigEndian::read_u32(&length_prefix) as usize;
|
||||
// Read varint length prefix (up to 10 bytes for u64)
|
||||
let mut length_buf = Vec::new();
|
||||
let mut temp_byte = [0u8; 1];
|
||||
|
||||
if length > MAX_SIZE {
|
||||
anyhow::bail!("Received data exceeds maximum size");
|
||||
loop {
|
||||
stream_read.read_exact(&mut temp_byte).await?;
|
||||
length_buf.push(temp_byte[0]);
|
||||
|
||||
// Check if this is the last byte (MSB = 0)
|
||||
if temp_byte[0] & 0x80 == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// Protect against malicious infinite varints
|
||||
if length_buf.len() > 10 {
|
||||
anyhow::bail!("Invalid varint encoding");
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the varint
|
||||
let (length, _) = decode::usize(&length_buf)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to decode varint: {}", e))?;
|
||||
|
||||
// Read payload
|
||||
let mut buffer = vec![0u8; length];
|
||||
stream_read.read_exact(&mut buffer).await?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,35 @@
|
||||
pub mod proto;
|
||||
|
||||
pub struct CreateMessageOptions {
|
||||
pub sequence_id: Option<String>,
|
||||
pub latency: Option<proto::ProtoLatencyTracker>,
|
||||
}
|
||||
|
||||
pub fn create_message(
|
||||
payload: proto::proto_message::Payload,
|
||||
payload_type: impl Into<String>,
|
||||
options: Option<CreateMessageOptions>,
|
||||
) -> proto::ProtoMessage {
|
||||
let opts = options.unwrap_or(CreateMessageOptions {
|
||||
sequence_id: None,
|
||||
latency: None,
|
||||
});
|
||||
|
||||
let latency = opts.latency.or_else(|| {
|
||||
opts.sequence_id.map(|seq_id| proto::ProtoLatencyTracker {
|
||||
sequence_id: seq_id,
|
||||
timestamps: vec![proto::ProtoTimestampEntry {
|
||||
stage: "created".to_string(),
|
||||
time: Some(prost_types::Timestamp::from(std::time::SystemTime::now())),
|
||||
}],
|
||||
})
|
||||
});
|
||||
|
||||
proto::ProtoMessage {
|
||||
message_base: Some(proto::ProtoMessageBase {
|
||||
payload_type: payload_type.into(),
|
||||
latency,
|
||||
}),
|
||||
payload: Some(payload),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
// @generated
|
||||
// This file is @generated by prost-build.
|
||||
/// EntityState represents the state of an entity in the mesh (e.g., a room).
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct EntityState {
|
||||
/// Type of entity (e.g., "room")
|
||||
#[prost(string, tag="1")]
|
||||
pub entity_type: ::prost::alloc::string::String,
|
||||
/// Unique identifier (e.g., room name)
|
||||
#[prost(string, tag="2")]
|
||||
pub entity_id: ::prost::alloc::string::String,
|
||||
/// Whether the entity is active
|
||||
#[prost(bool, tag="3")]
|
||||
pub active: bool,
|
||||
/// Relay ID that owns this entity
|
||||
#[prost(string, tag="4")]
|
||||
pub owner_relay_id: ::prost::alloc::string::String,
|
||||
}
|
||||
/// MeshMessage is the top-level message for all relay-to-relay communication.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct MeshMessage {
|
||||
#[prost(oneof="mesh_message::Type", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13")]
|
||||
pub r#type: ::core::option::Option<mesh_message::Type>,
|
||||
}
|
||||
/// Nested message and enum types in `MeshMessage`.
|
||||
pub mod mesh_message {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum Type {
|
||||
/// Level 0
|
||||
#[prost(message, tag="1")]
|
||||
StateUpdate(super::StateUpdate),
|
||||
#[prost(message, tag="2")]
|
||||
Ack(super::Ack),
|
||||
#[prost(message, tag="3")]
|
||||
RetransmissionRequest(super::RetransmissionRequest),
|
||||
#[prost(message, tag="4")]
|
||||
Retransmission(super::Retransmission),
|
||||
#[prost(message, tag="5")]
|
||||
Heartbeat(super::Heartbeat),
|
||||
#[prost(message, tag="6")]
|
||||
SuspectRelay(super::SuspectRelay),
|
||||
#[prost(message, tag="7")]
|
||||
Disconnect(super::Disconnect),
|
||||
/// Level 1
|
||||
#[prost(message, tag="8")]
|
||||
ForwardSdp(super::ForwardSdp),
|
||||
#[prost(message, tag="9")]
|
||||
ForwardIce(super::ForwardIce),
|
||||
#[prost(message, tag="10")]
|
||||
ForwardIngest(super::ForwardIngest),
|
||||
#[prost(message, tag="11")]
|
||||
StreamRequest(super::StreamRequest),
|
||||
/// Level 2
|
||||
#[prost(message, tag="12")]
|
||||
Handshake(super::Handshake),
|
||||
#[prost(message, tag="13")]
|
||||
HandshakeResponse(super::HandshakeResponse),
|
||||
}
|
||||
}
|
||||
/// Handshake to inititiate new connection to mesh.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Handshake {
|
||||
/// UUID of the relay
|
||||
#[prost(string, tag="1")]
|
||||
pub relay_id: ::prost::alloc::string::String,
|
||||
/// base64 encoded Diffie-Hellman public key
|
||||
#[prost(string, tag="2")]
|
||||
pub dh_public_key: ::prost::alloc::string::String,
|
||||
}
|
||||
/// HandshakeResponse to respond to a mesh joiner.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct HandshakeResponse {
|
||||
#[prost(string, tag="1")]
|
||||
pub relay_id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="2")]
|
||||
pub dh_public_key: ::prost::alloc::string::String,
|
||||
/// relay id to signature
|
||||
#[prost(map="string, string", tag="3")]
|
||||
pub approvals: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>,
|
||||
}
|
||||
/// Forwarded SDP from another relay.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ForwardSdp {
|
||||
#[prost(string, tag="1")]
|
||||
pub room_name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="2")]
|
||||
pub participant_id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="3")]
|
||||
pub sdp: ::prost::alloc::string::String,
|
||||
/// "offer" or "answer"
|
||||
#[prost(string, tag="4")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Forwarded ICE candidate from another relay.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ForwardIce {
|
||||
#[prost(string, tag="1")]
|
||||
pub room_name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="2")]
|
||||
pub participant_id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="3")]
|
||||
pub candidate: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Forwarded ingest room from another relay.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ForwardIngest {
|
||||
#[prost(string, tag="1")]
|
||||
pub room_name: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Stream request from mesh.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct StreamRequest {
|
||||
#[prost(string, tag="1")]
|
||||
pub room_name: ::prost::alloc::string::String,
|
||||
}
|
||||
/// StateUpdate propagates entity state changes across the mesh.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct StateUpdate {
|
||||
/// Unique sequence number for this update
|
||||
#[prost(uint64, tag="1")]
|
||||
pub sequence_number: u64,
|
||||
/// Key: entity_id (e.g., room name), Value: EntityState
|
||||
#[prost(map="string, message", tag="2")]
|
||||
pub entities: ::std::collections::HashMap<::prost::alloc::string::String, EntityState>,
|
||||
}
|
||||
/// Ack acknowledges receipt of a StateUpdate.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Ack {
|
||||
/// UUID of the acknowledging relay
|
||||
#[prost(string, tag="1")]
|
||||
pub relay_id: ::prost::alloc::string::String,
|
||||
/// Sequence number being acknowledged
|
||||
#[prost(uint64, tag="2")]
|
||||
pub sequence_number: u64,
|
||||
}
|
||||
/// RetransmissionRequest requests a missed StateUpdate.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct RetransmissionRequest {
|
||||
/// UUID of the requesting relay
|
||||
#[prost(string, tag="1")]
|
||||
pub relay_id: ::prost::alloc::string::String,
|
||||
/// Sequence number of the missed update
|
||||
#[prost(uint64, tag="2")]
|
||||
pub sequence_number: u64,
|
||||
}
|
||||
/// Retransmission resends a StateUpdate.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Retransmission {
|
||||
/// UUID of the sending relay
|
||||
#[prost(string, tag="1")]
|
||||
pub relay_id: ::prost::alloc::string::String,
|
||||
/// The retransmitted update
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub state_update: ::core::option::Option<StateUpdate>,
|
||||
}
|
||||
/// Heartbeat signals relay liveness.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Heartbeat {
|
||||
/// UUID of the sending relay
|
||||
#[prost(string, tag="1")]
|
||||
pub relay_id: ::prost::alloc::string::String,
|
||||
/// Time of the heartbeat
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub timestamp: ::core::option::Option<::prost_types::Timestamp>,
|
||||
}
|
||||
/// SuspectRelay marks a relay as potentially unresponsive.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SuspectRelay {
|
||||
/// UUID of the suspected relay
|
||||
#[prost(string, tag="1")]
|
||||
pub relay_id: ::prost::alloc::string::String,
|
||||
/// Reason for suspicion (e.g., "no heartbeat")
|
||||
#[prost(string, tag="2")]
|
||||
pub reason: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Disconnect signals to remove a relay from the mesh.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Disconnect {
|
||||
/// UUID of the relay to disconnect
|
||||
#[prost(string, tag="1")]
|
||||
pub relay_id: ::prost::alloc::string::String,
|
||||
/// Reason for disconnection (e.g., "unresponsive")
|
||||
#[prost(string, tag="2")]
|
||||
pub reason: ::prost::alloc::string::String,
|
||||
}
|
||||
// @@protoc_insertion_point(module)
|
||||
@@ -20,80 +20,59 @@ pub struct ProtoLatencyTracker {
|
||||
|
||||
/// MouseMove message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseMove {
|
||||
/// Fixed value "MouseMove"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag="1")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseMoveAbs message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseMoveAbs {
|
||||
/// Fixed value "MouseMoveAbs"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag="1")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseWheel message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseWheel {
|
||||
/// Fixed value "MouseWheel"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag="1")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseKeyDown message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseKeyDown {
|
||||
/// Fixed value "MouseKeyDown"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag="1")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// MouseKeyUp message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseKeyUp {
|
||||
/// Fixed value "MouseKeyUp"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag="1")]
|
||||
pub key: i32,
|
||||
}
|
||||
// Keyboard messages
|
||||
|
||||
/// KeyDown message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoKeyDown {
|
||||
/// Fixed value "KeyDown"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag="1")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// KeyUp message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoKeyUp {
|
||||
/// Fixed value "KeyUp"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag="1")]
|
||||
pub key: i32,
|
||||
}
|
||||
// Controller messages
|
||||
@@ -102,108 +81,37 @@ pub struct ProtoKeyUp {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerAttach {
|
||||
/// Fixed value "ControllerAttach"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// One of the following enums: "ps", "xbox" or "switch"
|
||||
#[prost(string, tag="2")]
|
||||
#[prost(string, tag="1")]
|
||||
pub id: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="3")]
|
||||
pub slot: i32,
|
||||
/// Session specific slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub session_slot: i32,
|
||||
/// Session ID of the client
|
||||
#[prost(string, tag="3")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
}
|
||||
/// ControllerDetach message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerDetach {
|
||||
/// Fixed value "ControllerDetach"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
}
|
||||
/// ControllerButton message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerButton {
|
||||
/// Fixed value "ControllerButtons"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Button code (linux input event code)
|
||||
#[prost(int32, tag="3")]
|
||||
pub button: i32,
|
||||
/// true if pressed, false if released
|
||||
#[prost(bool, tag="4")]
|
||||
pub pressed: bool,
|
||||
}
|
||||
/// ControllerTriggers message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerTrigger {
|
||||
/// Fixed value "ControllerTriggers"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Trigger number (0 for left, 1 for right)
|
||||
#[prost(int32, tag="3")]
|
||||
pub trigger: i32,
|
||||
/// trigger value (-32768 to 32767)
|
||||
#[prost(int32, tag="4")]
|
||||
pub value: i32,
|
||||
}
|
||||
/// ControllerSticks message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerStick {
|
||||
/// Fixed value "ControllerStick"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Stick number (0 for left, 1 for right)
|
||||
#[prost(int32, tag="3")]
|
||||
pub stick: i32,
|
||||
/// X axis value (-32768 to 32767)
|
||||
#[prost(int32, tag="4")]
|
||||
pub x: i32,
|
||||
/// Y axis value (-32768 to 32767)
|
||||
#[prost(int32, tag="5")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// ControllerAxis message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerAxis {
|
||||
/// Fixed value "ControllerAxis"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
|
||||
#[prost(int32, tag="3")]
|
||||
pub axis: i32,
|
||||
/// axis value (-1 to 1)
|
||||
#[prost(int32, tag="4")]
|
||||
pub value: i32,
|
||||
/// Session specific slot number (0-3)
|
||||
#[prost(int32, tag="1")]
|
||||
pub session_slot: i32,
|
||||
/// Session ID of the client
|
||||
#[prost(string, tag="2")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
}
|
||||
/// ControllerRumble message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerRumble {
|
||||
/// Fixed value "ControllerRumble"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Session specific slot number (0-3)
|
||||
#[prost(int32, tag="1")]
|
||||
pub session_slot: i32,
|
||||
/// Session ID of the client
|
||||
#[prost(string, tag="2")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
/// Low frequency rumble (0-65535)
|
||||
#[prost(int32, tag="3")]
|
||||
pub low_frequency: i32,
|
||||
@@ -214,47 +122,153 @@ pub struct ProtoControllerRumble {
|
||||
#[prost(int32, tag="5")]
|
||||
pub duration: i32,
|
||||
}
|
||||
/// Union of all Input types
|
||||
/// ControllerStateBatch - single message containing full or partial controller state
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoInput {
|
||||
#[prost(oneof="proto_input::InputType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14")]
|
||||
pub input_type: ::core::option::Option<proto_input::InputType>,
|
||||
pub struct ProtoControllerStateBatch {
|
||||
/// Session specific slot number (0-3)
|
||||
#[prost(int32, tag="1")]
|
||||
pub session_slot: i32,
|
||||
/// Session ID of the client
|
||||
#[prost(string, tag="2")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
#[prost(enumeration="proto_controller_state_batch::UpdateType", tag="3")]
|
||||
pub update_type: i32,
|
||||
/// Sequence number for packet loss detection
|
||||
#[prost(uint32, tag="4")]
|
||||
pub sequence: u32,
|
||||
/// Button state map (Linux event codes)
|
||||
#[prost(map="int32, bool", tag="5")]
|
||||
pub button_changed_mask: ::std::collections::HashMap<i32, bool>,
|
||||
/// Analog inputs
|
||||
///
|
||||
/// -32768 to 32767
|
||||
#[prost(int32, optional, tag="6")]
|
||||
pub left_stick_x: ::core::option::Option<i32>,
|
||||
/// -32768 to 32767
|
||||
#[prost(int32, optional, tag="7")]
|
||||
pub left_stick_y: ::core::option::Option<i32>,
|
||||
/// -32768 to 32767
|
||||
#[prost(int32, optional, tag="8")]
|
||||
pub right_stick_x: ::core::option::Option<i32>,
|
||||
/// -32768 to 32767
|
||||
#[prost(int32, optional, tag="9")]
|
||||
pub right_stick_y: ::core::option::Option<i32>,
|
||||
/// -32768 to 32767
|
||||
#[prost(int32, optional, tag="10")]
|
||||
pub left_trigger: ::core::option::Option<i32>,
|
||||
/// -32768 to 32767
|
||||
#[prost(int32, optional, tag="11")]
|
||||
pub right_trigger: ::core::option::Option<i32>,
|
||||
/// -1, 0, or 1
|
||||
#[prost(int32, optional, tag="12")]
|
||||
pub dpad_x: ::core::option::Option<i32>,
|
||||
/// -1, 0, or 1
|
||||
#[prost(int32, optional, tag="13")]
|
||||
pub dpad_y: ::core::option::Option<i32>,
|
||||
/// Bitmask indicating which fields have changed
|
||||
/// Bit 0: button_changed_mask, Bit 1: left_stick_x, Bit 2: left_stick_y, etc.
|
||||
#[prost(uint32, optional, tag="14")]
|
||||
pub changed_fields: ::core::option::Option<u32>,
|
||||
}
|
||||
/// Nested message and enum types in `ProtoInput`.
|
||||
pub mod proto_input {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum InputType {
|
||||
#[prost(message, tag="1")]
|
||||
MouseMove(super::ProtoMouseMove),
|
||||
#[prost(message, tag="2")]
|
||||
MouseMoveAbs(super::ProtoMouseMoveAbs),
|
||||
#[prost(message, tag="3")]
|
||||
MouseWheel(super::ProtoMouseWheel),
|
||||
#[prost(message, tag="4")]
|
||||
MouseKeyDown(super::ProtoMouseKeyDown),
|
||||
#[prost(message, tag="5")]
|
||||
MouseKeyUp(super::ProtoMouseKeyUp),
|
||||
#[prost(message, tag="6")]
|
||||
KeyDown(super::ProtoKeyDown),
|
||||
#[prost(message, tag="7")]
|
||||
KeyUp(super::ProtoKeyUp),
|
||||
#[prost(message, tag="8")]
|
||||
ControllerAttach(super::ProtoControllerAttach),
|
||||
#[prost(message, tag="9")]
|
||||
ControllerDetach(super::ProtoControllerDetach),
|
||||
#[prost(message, tag="10")]
|
||||
ControllerButton(super::ProtoControllerButton),
|
||||
#[prost(message, tag="11")]
|
||||
ControllerTrigger(super::ProtoControllerTrigger),
|
||||
#[prost(message, tag="12")]
|
||||
ControllerStick(super::ProtoControllerStick),
|
||||
#[prost(message, tag="13")]
|
||||
ControllerAxis(super::ProtoControllerAxis),
|
||||
#[prost(message, tag="14")]
|
||||
ControllerRumble(super::ProtoControllerRumble),
|
||||
/// Nested message and enum types in `ProtoControllerStateBatch`.
|
||||
pub mod proto_controller_state_batch {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum UpdateType {
|
||||
/// Complete controller state
|
||||
FullState = 0,
|
||||
/// Only changed fields
|
||||
Delta = 1,
|
||||
}
|
||||
impl UpdateType {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
///
|
||||
/// The values are not transformed in any way and thus are considered stable
|
||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||
pub fn as_str_name(&self) -> &'static str {
|
||||
match self {
|
||||
UpdateType::FullState => "FULL_STATE",
|
||||
UpdateType::Delta => "DELTA",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"FULL_STATE" => Some(Self::FullState),
|
||||
"DELTA" => Some(Self::Delta),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// WebRTC + signaling
|
||||
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct RtcIceCandidateInit {
|
||||
#[prost(string, tag="1")]
|
||||
pub candidate: ::prost::alloc::string::String,
|
||||
#[prost(uint32, optional, tag="2")]
|
||||
pub sdp_m_line_index: ::core::option::Option<u32>,
|
||||
#[prost(string, optional, tag="3")]
|
||||
pub sdp_mid: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(string, optional, tag="4")]
|
||||
pub username_fragment: ::core::option::Option<::prost::alloc::string::String>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct RtcSessionDescriptionInit {
|
||||
#[prost(string, tag="1")]
|
||||
pub sdp: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="2")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
}
|
||||
/// ProtoICE message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoIce {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub candidate: ::core::option::Option<RtcIceCandidateInit>,
|
||||
}
|
||||
/// ProtoSDP message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoSdp {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub sdp: ::core::option::Option<RtcSessionDescriptionInit>,
|
||||
}
|
||||
/// ProtoRaw message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoRaw {
|
||||
#[prost(string, tag="1")]
|
||||
pub data: ::prost::alloc::string::String,
|
||||
}
|
||||
/// ProtoClientRequestRoomStream message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoClientRequestRoomStream {
|
||||
#[prost(string, tag="1")]
|
||||
pub room_name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="2")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
}
|
||||
/// ProtoClientDisconnected message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoClientDisconnected {
|
||||
#[prost(string, tag="1")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
#[prost(int32, repeated, tag="2")]
|
||||
pub controller_slots: ::prost::alloc::vec::Vec<i32>,
|
||||
}
|
||||
/// ProtoServerPushStream message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoServerPushStream {
|
||||
#[prost(string, tag="1")]
|
||||
pub room_name: ::prost::alloc::string::String,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
@@ -266,10 +280,54 @@ pub struct ProtoMessageBase {
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMessageInput {
|
||||
pub struct ProtoMessage {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub message_base: ::core::option::Option<ProtoMessageBase>,
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub data: ::core::option::Option<ProtoInput>,
|
||||
#[prost(oneof="proto_message::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 21, 22, 23, 24, 25")]
|
||||
pub payload: ::core::option::Option<proto_message::Payload>,
|
||||
}
|
||||
/// Nested message and enum types in `ProtoMessage`.
|
||||
pub mod proto_message {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum Payload {
|
||||
/// Input types
|
||||
#[prost(message, tag="2")]
|
||||
MouseMove(super::ProtoMouseMove),
|
||||
#[prost(message, tag="3")]
|
||||
MouseMoveAbs(super::ProtoMouseMoveAbs),
|
||||
#[prost(message, tag="4")]
|
||||
MouseWheel(super::ProtoMouseWheel),
|
||||
#[prost(message, tag="5")]
|
||||
MouseKeyDown(super::ProtoMouseKeyDown),
|
||||
#[prost(message, tag="6")]
|
||||
MouseKeyUp(super::ProtoMouseKeyUp),
|
||||
#[prost(message, tag="7")]
|
||||
KeyDown(super::ProtoKeyDown),
|
||||
#[prost(message, tag="8")]
|
||||
KeyUp(super::ProtoKeyUp),
|
||||
/// Controller input types
|
||||
#[prost(message, tag="9")]
|
||||
ControllerAttach(super::ProtoControllerAttach),
|
||||
#[prost(message, tag="10")]
|
||||
ControllerDetach(super::ProtoControllerDetach),
|
||||
#[prost(message, tag="11")]
|
||||
ControllerRumble(super::ProtoControllerRumble),
|
||||
#[prost(message, tag="12")]
|
||||
ControllerStateBatch(super::ProtoControllerStateBatch),
|
||||
/// Signaling types
|
||||
#[prost(message, tag="20")]
|
||||
Ice(super::ProtoIce),
|
||||
#[prost(message, tag="21")]
|
||||
Sdp(super::ProtoSdp),
|
||||
#[prost(message, tag="22")]
|
||||
Raw(super::ProtoRaw),
|
||||
#[prost(message, tag="23")]
|
||||
ClientRequestRoomStream(super::ProtoClientRequestRoomStream),
|
||||
#[prost(message, tag="24")]
|
||||
ClientDisconnected(super::ProtoClientDisconnected),
|
||||
#[prost(message, tag="25")]
|
||||
ServerPushStream(super::ProtoServerPushStream),
|
||||
}
|
||||
}
|
||||
// @@protoc_insertion_point(module)
|
||||
|
||||
@@ -12,7 +12,30 @@ message ProtoMessageBase {
|
||||
ProtoLatencyTracker latency = 2;
|
||||
}
|
||||
|
||||
message ProtoMessageInput {
|
||||
ProtoMessageBase message_base = 1;
|
||||
ProtoInput data = 2;
|
||||
message ProtoMessage {
|
||||
ProtoMessageBase message_base = 1;
|
||||
oneof payload {
|
||||
// Input types
|
||||
ProtoMouseMove mouse_move = 2;
|
||||
ProtoMouseMoveAbs mouse_move_abs = 3;
|
||||
ProtoMouseWheel mouse_wheel = 4;
|
||||
ProtoMouseKeyDown mouse_key_down = 5;
|
||||
ProtoMouseKeyUp mouse_key_up = 6;
|
||||
ProtoKeyDown key_down = 7;
|
||||
ProtoKeyUp key_up = 8;
|
||||
|
||||
// Controller input types
|
||||
ProtoControllerAttach controller_attach = 9;
|
||||
ProtoControllerDetach controller_detach = 10;
|
||||
ProtoControllerRumble controller_rumble = 11;
|
||||
ProtoControllerStateBatch controller_state_batch = 12;
|
||||
|
||||
// Signaling types
|
||||
ProtoICE ice = 20;
|
||||
ProtoSDP sdp = 21;
|
||||
ProtoRaw raw = 22;
|
||||
ProtoClientRequestRoomStream client_request_room_stream = 23;
|
||||
ProtoClientDisconnected client_disconnected = 24;
|
||||
ProtoServerPushStream server_push_stream = 25;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,124 +8,142 @@ package proto;
|
||||
|
||||
// MouseMove message
|
||||
message ProtoMouseMove {
|
||||
string type = 1; // Fixed value "MouseMove"
|
||||
int32 x = 2;
|
||||
int32 y = 3;
|
||||
int32 x = 1;
|
||||
int32 y = 2;
|
||||
}
|
||||
|
||||
// MouseMoveAbs message
|
||||
message ProtoMouseMoveAbs {
|
||||
string type = 1; // Fixed value "MouseMoveAbs"
|
||||
int32 x = 2;
|
||||
int32 y = 3;
|
||||
int32 x = 1;
|
||||
int32 y = 2;
|
||||
}
|
||||
|
||||
// MouseWheel message
|
||||
message ProtoMouseWheel {
|
||||
string type = 1; // Fixed value "MouseWheel"
|
||||
int32 x = 2;
|
||||
int32 y = 3;
|
||||
int32 x = 1;
|
||||
int32 y = 2;
|
||||
}
|
||||
|
||||
// MouseKeyDown message
|
||||
message ProtoMouseKeyDown {
|
||||
string type = 1; // Fixed value "MouseKeyDown"
|
||||
int32 key = 2;
|
||||
int32 key = 1;
|
||||
}
|
||||
|
||||
// MouseKeyUp message
|
||||
message ProtoMouseKeyUp {
|
||||
string type = 1; // Fixed value "MouseKeyUp"
|
||||
int32 key = 2;
|
||||
int32 key = 1;
|
||||
}
|
||||
|
||||
/* Keyboard messages */
|
||||
|
||||
// KeyDown message
|
||||
message ProtoKeyDown {
|
||||
string type = 1; // Fixed value "KeyDown"
|
||||
int32 key = 2;
|
||||
int32 key = 1;
|
||||
}
|
||||
|
||||
// KeyUp message
|
||||
message ProtoKeyUp {
|
||||
string type = 1; // Fixed value "KeyUp"
|
||||
int32 key = 2;
|
||||
int32 key = 1;
|
||||
}
|
||||
|
||||
/* Controller messages */
|
||||
|
||||
// ControllerAttach message
|
||||
message ProtoControllerAttach {
|
||||
string type = 1; // Fixed value "ControllerAttach"
|
||||
string id = 2; // One of the following enums: "ps", "xbox" or "switch"
|
||||
int32 slot = 3; // Slot number (0-3)
|
||||
string id = 1; // One of the following enums: "ps", "xbox" or "switch"
|
||||
int32 session_slot = 2; // Session specific slot number (0-3)
|
||||
string session_id = 3; // Session ID of the client
|
||||
}
|
||||
|
||||
// ControllerDetach message
|
||||
message ProtoControllerDetach {
|
||||
string type = 1; // Fixed value "ControllerDetach"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
}
|
||||
|
||||
// ControllerButton message
|
||||
message ProtoControllerButton {
|
||||
string type = 1; // Fixed value "ControllerButtons"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 button = 3; // Button code (linux input event code)
|
||||
bool pressed = 4; // true if pressed, false if released
|
||||
}
|
||||
|
||||
// ControllerTriggers message
|
||||
message ProtoControllerTrigger {
|
||||
string type = 1; // Fixed value "ControllerTriggers"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 trigger = 3; // Trigger number (0 for left, 1 for right)
|
||||
int32 value = 4; // trigger value (-32768 to 32767)
|
||||
}
|
||||
|
||||
// ControllerSticks message
|
||||
message ProtoControllerStick {
|
||||
string type = 1; // Fixed value "ControllerStick"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 stick = 3; // Stick number (0 for left, 1 for right)
|
||||
int32 x = 4; // X axis value (-32768 to 32767)
|
||||
int32 y = 5; // Y axis value (-32768 to 32767)
|
||||
}
|
||||
|
||||
// ControllerAxis message
|
||||
message ProtoControllerAxis {
|
||||
string type = 1; // Fixed value "ControllerAxis"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 axis = 3; // Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
|
||||
int32 value = 4; // axis value (-1 to 1)
|
||||
int32 session_slot = 1; // Session specific slot number (0-3)
|
||||
string session_id = 2; // Session ID of the client
|
||||
}
|
||||
|
||||
// ControllerRumble message
|
||||
message ProtoControllerRumble {
|
||||
string type = 1; // Fixed value "ControllerRumble"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 session_slot = 1; // Session specific slot number (0-3)
|
||||
string session_id = 2; // Session ID of the client
|
||||
int32 low_frequency = 3; // Low frequency rumble (0-65535)
|
||||
int32 high_frequency = 4; // High frequency rumble (0-65535)
|
||||
int32 duration = 5; // Duration in milliseconds
|
||||
}
|
||||
|
||||
// Union of all Input types
|
||||
message ProtoInput {
|
||||
oneof input_type {
|
||||
ProtoMouseMove mouse_move = 1;
|
||||
ProtoMouseMoveAbs mouse_move_abs = 2;
|
||||
ProtoMouseWheel mouse_wheel = 3;
|
||||
ProtoMouseKeyDown mouse_key_down = 4;
|
||||
ProtoMouseKeyUp mouse_key_up = 5;
|
||||
ProtoKeyDown key_down = 6;
|
||||
ProtoKeyUp key_up = 7;
|
||||
ProtoControllerAttach controller_attach = 8;
|
||||
ProtoControllerDetach controller_detach = 9;
|
||||
ProtoControllerButton controller_button = 10;
|
||||
ProtoControllerTrigger controller_trigger = 11;
|
||||
ProtoControllerStick controller_stick = 12;
|
||||
ProtoControllerAxis controller_axis = 13;
|
||||
ProtoControllerRumble controller_rumble = 14;
|
||||
// ControllerStateBatch - single message containing full or partial controller state
|
||||
message ProtoControllerStateBatch {
|
||||
int32 session_slot = 1; // Session specific slot number (0-3)
|
||||
string session_id = 2; // Session ID of the client
|
||||
|
||||
enum UpdateType {
|
||||
FULL_STATE = 0; // Complete controller state
|
||||
DELTA = 1; // Only changed fields
|
||||
}
|
||||
UpdateType update_type = 3;
|
||||
|
||||
// Sequence number for packet loss detection
|
||||
uint32 sequence = 4;
|
||||
|
||||
// Button state map (Linux event codes)
|
||||
map<int32, bool> button_changed_mask = 5;
|
||||
|
||||
// Analog inputs
|
||||
optional int32 left_stick_x = 6; // -32768 to 32767
|
||||
optional int32 left_stick_y = 7; // -32768 to 32767
|
||||
optional int32 right_stick_x = 8; // -32768 to 32767
|
||||
optional int32 right_stick_y = 9; // -32768 to 32767
|
||||
optional int32 left_trigger = 10; // -32768 to 32767
|
||||
optional int32 right_trigger = 11; // -32768 to 32767
|
||||
optional int32 dpad_x = 12; // -1, 0, or 1
|
||||
optional int32 dpad_y = 13; // -1, 0, or 1
|
||||
|
||||
// Bitmask indicating which fields have changed
|
||||
// Bit 0: button_changed_mask, Bit 1: left_stick_x, Bit 2: left_stick_y, etc.
|
||||
optional uint32 changed_fields = 14;
|
||||
}
|
||||
|
||||
/* WebRTC + signaling */
|
||||
|
||||
message RTCIceCandidateInit {
|
||||
string candidate = 1;
|
||||
optional uint32 sdpMLineIndex = 2;
|
||||
optional string sdpMid = 3;
|
||||
optional string usernameFragment = 4;
|
||||
}
|
||||
|
||||
message RTCSessionDescriptionInit {
|
||||
string sdp = 1;
|
||||
string type = 2;
|
||||
}
|
||||
|
||||
// ProtoICE message
|
||||
message ProtoICE {
|
||||
RTCIceCandidateInit candidate = 1;
|
||||
}
|
||||
|
||||
// ProtoSDP message
|
||||
message ProtoSDP {
|
||||
RTCSessionDescriptionInit sdp = 1;
|
||||
}
|
||||
|
||||
// ProtoRaw message
|
||||
message ProtoRaw {
|
||||
string data = 1;
|
||||
}
|
||||
|
||||
// ProtoClientRequestRoomStream message
|
||||
message ProtoClientRequestRoomStream {
|
||||
string room_name = 1;
|
||||
string session_id = 2;
|
||||
}
|
||||
|
||||
// ProtoClientDisconnected message
|
||||
message ProtoClientDisconnected {
|
||||
string session_id = 1;
|
||||
repeated int32 controller_slots = 2;
|
||||
}
|
||||
|
||||
// ProtoServerPushStream message
|
||||
message ProtoServerPushStream {
|
||||
string room_name = 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user