Restructure protobufs and use them everywhere

This commit is contained in:
DatCaptainHorse
2025-10-21 18:41:45 +03:00
parent 32341574dc
commit 67f9a7d0a0
37 changed files with 3455 additions and 3074 deletions

View File

@@ -7,24 +7,22 @@
".": "./src/index.ts" ".": "./src/index.ts"
}, },
"devDependencies": { "devDependencies": {
"@bufbuild/buf": "^1.57.2", "@bufbuild/buf": "^1.59.0",
"@bufbuild/protoc-gen-es": "^2.9.0" "@bufbuild/protoc-gen-es": "^2.10.0"
}, },
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.9.0", "@bufbuild/protobuf": "^2.10.0",
"@chainsafe/libp2p-noise": "^16.1.4", "@chainsafe/libp2p-noise": "^17.0.0",
"@chainsafe/libp2p-quic": "^1.1.3", "@chainsafe/libp2p-quic": "^1.1.3",
"@chainsafe/libp2p-yamux": "^7.0.4", "@chainsafe/libp2p-yamux": "^8.0.1",
"@libp2p/identify": "^3.0.39", "@libp2p/identify": "^4.0.5",
"@libp2p/interface": "^2.11.0", "@libp2p/interface": "^3.0.2",
"@libp2p/ping": "^2.0.37", "@libp2p/ping": "^3.0.5",
"@libp2p/websockets": "^9.2.19", "@libp2p/websockets": "^10.0.6",
"@libp2p/webtransport": "^5.0.51", "@libp2p/webtransport": "^6.0.7",
"@multiformats/multiaddr": "^12.5.1", "@libp2p/utils": "^7.0.5",
"it-length-prefixed": "^10.0.1", "@multiformats/multiaddr": "^13.0.1",
"it-pipe": "^3.0.1", "libp2p": "^3.0.6",
"libp2p": "^2.10.0", "uint8arraylist": "^2.4.8"
"uint8arraylist": "^2.4.8",
"uint8arrays": "^5.1.0"
} }
} }

View File

@@ -1,12 +1,6 @@
import { controllerButtonToLinuxEventCode } from "./codes"; import { controllerButtonToLinuxEventCode } from "./codes";
import { WebRTCStream } from "./webrtc-stream"; import { WebRTCStream } from "./webrtc-stream";
import { import {
ProtoMessageBase,
ProtoMessageInput,
ProtoMessageInputSchema,
} from "./proto/messages_pb";
import {
ProtoInputSchema,
ProtoControllerAttachSchema, ProtoControllerAttachSchema,
ProtoControllerDetachSchema, ProtoControllerDetachSchema,
ProtoControllerButtonSchema, ProtoControllerButtonSchema,
@@ -16,6 +10,8 @@ import {
ProtoControllerRumble, ProtoControllerRumble,
} from "./proto/types_pb"; } from "./proto/types_pb";
import { create, toBinary, fromBinary } from "@bufbuild/protobuf"; import { create, toBinary, fromBinary } from "@bufbuild/protobuf";
import { createMessage } from "./utils";
import { ProtoMessageSchema } from "./proto/messages_pb";
interface Props { interface Props {
webrtc: WebRTCStream; webrtc: WebRTCStream;
@@ -36,7 +32,7 @@ interface GamepadState {
export class Controller { export class Controller {
protected wrtc: WebRTCStream; protected wrtc: WebRTCStream;
protected slot: number; protected slotMap: Map<number, number> = new Map(); // local slot to server slot
protected connected: boolean = false; protected connected: boolean = false;
protected gamepad: Gamepad | null = null; protected gamepad: Gamepad | null = null;
protected lastState: GamepadState = { protected lastState: GamepadState = {
@@ -54,17 +50,13 @@ export class Controller {
protected stickDeadzone: number = 2048; // 2048 / 32768 = ~0.06 (6% of stick range) protected stickDeadzone: number = 2048; // 2048 / 32768 = ~0.06 (6% of stick range)
private updateInterval = 10.0; // 100 updates per second private updateInterval = 10.0; // 100 updates per second
private _dcRumbleHandler: ((data: ArrayBuffer) => void) | null = null; private _dcHandler: ((data: ArrayBuffer) => void) | null = null;
constructor({ webrtc, e }: Props) { constructor({ webrtc, e }: Props) {
this.wrtc = webrtc; this.wrtc = webrtc;
this.slot = e.gamepad.index;
this.updateInterval = 1000 / webrtc.currentFrameRate; this.updateInterval = 1000 / webrtc.currentFrameRate;
// Gamepad connected
this.gamepad = e.gamepad;
// Get vendor of gamepad from id string (i.e. "... Vendor: 054c Product: 09cc") // 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})/); const vendorMatch = e.gamepad.id.match(/Vendor:\s?([0-9a-fA-F]{4})/);
const vendorId = vendorMatch ? vendorMatch[1].toLowerCase() : "unknown"; const vendorId = vendorMatch ? vendorMatch[1].toLowerCase() : "unknown";
@@ -72,30 +64,40 @@ export class Controller {
const productMatch = e.gamepad.id.match(/Product:\s?([0-9a-fA-F]{4})/); const productMatch = e.gamepad.id.match(/Product:\s?([0-9a-fA-F]{4})/);
const productId = productMatch ? productMatch[1].toLowerCase() : "unknown"; const productId = productMatch ? productMatch[1].toLowerCase() : "unknown";
const attachMsg = create(ProtoInputSchema, { // Listen to datachannel events from server
$typeName: "proto.ProtoInput", this._dcHandler = (data: ArrayBuffer) => {
inputType: { if (!this.connected) return;
case: "controllerAttach", try {
value: create(ProtoControllerAttachSchema, { // First decode the wrapper message
type: "ControllerAttach", const uint8Data = new Uint8Array(data);
id: this.vendor_id_to_controller(vendorId, productId), const messageWrapper = fromBinary(ProtoMessageSchema, uint8Data);
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 feedback rumble events from server if (messageWrapper.payload.case === "controllerRumble") {
this._dcRumbleHandler = (data: any) => this.rumbleCallback(data as ArrayBuffer); this.rumbleCallback(messageWrapper.payload.value);
this.wrtc.addDataChannelCallback(this._dcRumbleHandler); } else if (messageWrapper.payload.case === "controllerAttach") {
if (this.gamepad) return; // already attached
const attachMsg = messageWrapper.payload.value;
// Gamepad connected succesfully
this.gamepad = e.gamepad;
this.slotMap.set(e.gamepad.index, attachMsg.slot);
console.log(
`Gamepad connected: ${e.gamepad.id} assigned to slot ${attachMsg.slot} on server, local slot ${e.gamepad.index}`,
);
}
} 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),
sessionId: this.wrtc.getSessionID(),
}),
"controllerInput",
);
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, attachMsg));
this.run(); this.run();
} }
@@ -150,12 +152,13 @@ export class Controller {
} }
private pollGamepad() { private pollGamepad() {
// Get updated gamepad state
const gamepads = navigator.getGamepads(); const gamepads = navigator.getGamepads();
if (this.slot < gamepads.length) { if (this.gamepad) {
const gamepad = gamepads[this.slot]; if (gamepads[this.gamepad.index]) {
if (gamepad) { this.gamepad = gamepads[this.gamepad!.index];
/* Button handling */ /* Button handling */
gamepad.buttons.forEach((button, index) => { this.gamepad.buttons.forEach((button, index) => {
// Ignore d-pad buttons (12-15) as we handle those as axis // Ignore d-pad buttons (12-15) as we handle those as axis
if (index >= 12 && index <= 15) return; if (index >= 12 && index <= 15) return;
// ignore trigger buttons (6-7) as we handle those as axis // ignore trigger buttons (6-7) as we handle those as axis
@@ -169,29 +172,15 @@ export class Controller {
return; return;
} }
const buttonProto = create(ProtoInputSchema, { const buttonMessage = createMessage(
$typeName: "proto.ProtoInput", create(ProtoControllerButtonSchema, {
inputType: { slot: this.getServerSlot(),
case: "controllerButton", button: linuxCode,
value: create(ProtoControllerButtonSchema, { pressed: button.pressed,
type: "ControllerButton", }),
slot: this.slot, "controllerInput",
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),
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, buttonMessage));
// Store button state // Store button state
this.lastState.buttonState.set(index, button.pressed); this.lastState.buttonState.set(index, button.pressed);
} }
@@ -200,128 +189,100 @@ export class Controller {
/* Trigger handling */ /* Trigger handling */
// map trigger value from 0.0 to 1.0 to -32768 to 32767 // map trigger value from 0.0 to 1.0 to -32768 to 32767
const leftTrigger = Math.round( const leftTrigger = Math.round(
this.remapFromTo(gamepad.buttons[6]?.value ?? 0, 0, 1, -32768, 32767), this.remapFromTo(
this.gamepad.buttons[6]?.value ?? 0,
0,
1,
-32768,
32767,
),
); );
// If state differs, send // If state differs, send
if (leftTrigger !== this.lastState.leftTrigger) { if (leftTrigger !== this.lastState.leftTrigger) {
const triggerProto = create(ProtoInputSchema, { const triggerMessage = createMessage(
$typeName: "proto.ProtoInput", create(ProtoControllerTriggerSchema, {
inputType: { slot: this.getServerSlot(),
case: "controllerTrigger", trigger: 0, // 0 = left, 1 = right
value: create(ProtoControllerTriggerSchema, { value: leftTrigger,
type: "ControllerTrigger", }),
slot: this.slot, "controllerInput",
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),
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, triggerMessage));
this.lastState.leftTrigger = leftTrigger;
} }
const rightTrigger = Math.round( const rightTrigger = Math.round(
this.remapFromTo(gamepad.buttons[7]?.value ?? 0, 0, 1, -32768, 32767), this.remapFromTo(
this.gamepad.buttons[7]?.value ?? 0,
0,
1,
-32768,
32767,
),
); );
// If state differs, send // If state differs, send
if (rightTrigger !== this.lastState.rightTrigger) { if (rightTrigger !== this.lastState.rightTrigger) {
const triggerProto = create(ProtoInputSchema, { const triggerMessage = createMessage(
$typeName: "proto.ProtoInput", create(ProtoControllerTriggerSchema, {
inputType: { slot: this.getServerSlot(),
case: "controllerTrigger", trigger: 1, // 0 = left, 1 = right
value: create(ProtoControllerTriggerSchema, { value: rightTrigger,
type: "ControllerTrigger", }),
slot: this.slot, "controllerInput",
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),
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, triggerMessage));
this.lastState.rightTrigger = rightTrigger;
} }
/* DPad handling */ /* DPad handling */
// We send dpad buttons as axis values -1 to 1 for left/up, right/down // We send dpad buttons as axis values -1 to 1 for left/up, right/down
const dpadLeft = gamepad.buttons[14]?.pressed ? 1 : 0; const dpadLeft = this.gamepad.buttons[14]?.pressed ? 1 : 0;
const dpadRight = gamepad.buttons[15]?.pressed ? 1 : 0; const dpadRight = this.gamepad.buttons[15]?.pressed ? 1 : 0;
const dpadX = dpadLeft ? -1 : dpadRight ? 1 : 0; const dpadX = dpadLeft ? -1 : dpadRight ? 1 : 0;
if (dpadX !== this.lastState.dpadX) { if (dpadX !== this.lastState.dpadX) {
const dpadProto = create(ProtoInputSchema, { const dpadMessage = createMessage(
$typeName: "proto.ProtoInput", create(ProtoControllerAxisSchema, {
inputType: { slot: this.getServerSlot(),
case: "controllerAxis", axis: 0, // 0 = dpadX, 1 = dpadY
value: create(ProtoControllerAxisSchema, { value: dpadX,
type: "ControllerAxis", }),
slot: this.slot, "controllerInput",
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.lastState.dpadX = dpadX;
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, dpadMessage)); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, dpadMessage));
} }
const dpadUp = gamepad.buttons[12]?.pressed ? 1 : 0; const dpadUp = this.gamepad.buttons[12]?.pressed ? 1 : 0;
const dpadDown = gamepad.buttons[13]?.pressed ? 1 : 0; const dpadDown = this.gamepad.buttons[13]?.pressed ? 1 : 0;
const dpadY = dpadUp ? -1 : dpadDown ? 1 : 0; const dpadY = dpadUp ? -1 : dpadDown ? 1 : 0;
if (dpadY !== this.lastState.dpadY) { if (dpadY !== this.lastState.dpadY) {
const dpadProto = create(ProtoInputSchema, { const dpadMessage = createMessage(
$typeName: "proto.ProtoInput", create(ProtoControllerAxisSchema, {
inputType: { slot: this.getServerSlot(),
case: "controllerAxis", axis: 1, // 0 = dpadX, 1 = dpadY
value: create(ProtoControllerAxisSchema, { value: dpadY,
type: "ControllerAxis", }),
slot: this.slot, "controllerInput",
axis: 1, // 0 = dpadX, 1 = dpadY );
value: dpadY, this.wrtc.sendBinary(toBinary(ProtoMessageSchema, dpadMessage));
}),
},
});
const dpadMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: dpadProto,
};
this.lastState.dpadY = dpadY; this.lastState.dpadY = dpadY;
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, dpadMessage));
} }
/* Stick handling */ /* Stick handling */
// stick values need to be mapped from -1.0 to 1.0 to -32768 to 32767 // 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 leftX = this.remapFromTo(
const leftY = this.remapFromTo(gamepad.axes[1] ?? 0, -1, 1, -32768, 32767); this.gamepad.axes[0] ?? 0,
-1,
1,
-32768,
32767,
);
const leftY = this.remapFromTo(
this.gamepad.axes[1] ?? 0,
-1,
1,
-32768,
32767,
);
// Apply deadzone // Apply deadzone
const sendLeftX = const sendLeftX =
Math.abs(leftX) > this.stickDeadzone ? Math.round(leftX) : 0; Math.abs(leftX) > this.stickDeadzone ? Math.round(leftX) : 0;
@@ -333,35 +294,33 @@ export class Controller {
sendLeftX !== this.lastState.leftX || sendLeftX !== this.lastState.leftX ||
sendLeftY !== this.lastState.leftY sendLeftY !== this.lastState.leftY
) { ) {
// console.log("Sticks: ", sendLeftX, sendLeftY, sendRightX, sendRightY); const stickMessage = createMessage(
const stickProto = create(ProtoInputSchema, { create(ProtoControllerStickSchema, {
$typeName: "proto.ProtoInput", stick: 0, // 0 = left, 1 = right
inputType: { x: sendLeftX,
case: "controllerStick", y: sendLeftY,
value: create(ProtoControllerStickSchema, { }),
type: "ControllerStick", "controllerInput",
slot: this.slot, );
stick: 0, // 0 = left, 1 = right this.wrtc.sendBinary(toBinary(ProtoMessageSchema, stickMessage));
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.leftX = sendLeftX;
this.lastState.leftY = sendLeftY; this.lastState.leftY = sendLeftY;
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, stickMessage));
} }
const rightX = this.remapFromTo(gamepad.axes[2] ?? 0, -1, 1, -32768, 32767); const rightX = this.remapFromTo(
const rightY = this.remapFromTo(gamepad.axes[3] ?? 0, -1, 1, -32768, 32767); this.gamepad.axes[2] ?? 0,
-1,
1,
-32768,
32767,
);
const rightY = this.remapFromTo(
this.gamepad.axes[3] ?? 0,
-1,
1,
-32768,
32767,
);
// Apply deadzone // Apply deadzone
const sendRightX = const sendRightX =
Math.abs(rightX) > this.stickDeadzone ? Math.round(rightX) : 0; Math.abs(rightX) > this.stickDeadzone ? Math.round(rightX) : 0;
@@ -371,30 +330,17 @@ export class Controller {
sendRightX !== this.lastState.rightX || sendRightX !== this.lastState.rightX ||
sendRightY !== this.lastState.rightY sendRightY !== this.lastState.rightY
) { ) {
const stickProto = create(ProtoInputSchema, { const stickMessage = createMessage(
$typeName: "proto.ProtoInput", create(ProtoControllerStickSchema, {
inputType: { stick: 1, // 0 = left, 1 = right
case: "controllerStick", x: sendRightX,
value: create(ProtoControllerStickSchema, { y: sendRightY,
type: "ControllerStick", }),
slot: this.slot, "controllerInput",
stick: 1, // 0 = left, 1 = right );
x: sendRightX, this.wrtc.sendBinary(toBinary(ProtoMessageSchema, stickMessage));
y: sendRightY,
}),
},
});
const stickMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: stickProto,
};
this.lastState.rightX = sendRightX; this.lastState.rightX = sendRightX;
this.lastState.rightY = sendRightY; this.lastState.rightY = sendRightY;
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, stickMessage));
} }
} }
} }
@@ -403,8 +349,7 @@ export class Controller {
private loopInterval: any = null; private loopInterval: any = null;
public run() { public run() {
if (this.connected) if (this.connected) this.stop();
this.stop();
this.connected = true; this.connected = true;
// Poll gamepads in setInterval loop // Poll gamepads in setInterval loop
@@ -421,89 +366,75 @@ export class Controller {
this.connected = false; this.connected = false;
} }
public getSlot() { public getLocalSlot(): number {
return this.slot; if (this.gamepad) {
return this.gamepad.index;
}
return -1;
}
public getServerSlot(): number {
if (this.gamepad) {
const slot = this.slotMap.get(this.gamepad.index);
if (slot !== undefined) return slot;
}
return -1;
} }
public dispose() { public dispose() {
this.stop(); this.stop();
// Remove callback // Remove callback
if (this._dcRumbleHandler !== null) { if (this._dcHandler !== null) {
this.wrtc.removeDataChannelCallback(this._dcRumbleHandler); this.wrtc.removeDataChannelCallback(this._dcHandler);
this._dcRumbleHandler = null; this._dcHandler = null;
} }
// Gamepad disconnected // Gamepad disconnected
const detachMsg = create(ProtoInputSchema, { const detachMsg = createMessage(
$typeName: "proto.ProtoInput", create(ProtoControllerDetachSchema, {
inputType: { slot: this.getServerSlot(),
case: "controllerDetach", }),
value: create(ProtoControllerDetachSchema, { "controllerInput",
type: "ControllerDetach", );
slot: this.slot, this.wrtc.sendBinary(toBinary(ProtoMessageSchema, detachMsg));
}),
},
});
const message: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: detachMsg,
};
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
} }
private controllerButtonToVirtualKeyCode(code: number) { private controllerButtonToVirtualKeyCode(code: number) {
return controllerButtonToLinuxEventCode[code] || undefined; return controllerButtonToLinuxEventCode[code] || undefined;
} }
private rumbleCallback(data: ArrayBuffer) { private rumbleCallback(rumbleMsg: ProtoControllerRumble) {
// If not connected, ignore // If not connected, ignore
if (!this.connected) return; if (!this.connected) return;
try {
// First decode the wrapper message
const uint8Data = new Uint8Array(data);
const messageWrapper = fromBinary(ProtoMessageInputSchema, uint8Data);
// Check if it contains controller rumble data // Check if aimed at this controller slot
if (messageWrapper.data?.inputType?.case === "controllerRumble") { if (rumbleMsg.slot !== this.getServerSlot()) return;
const rumbleMsg = messageWrapper.data.inputType.value as ProtoControllerRumble;
// Check if aimed at this controller slot // Trigger actual rumble
if (rumbleMsg.slot !== this.slot) return; // Need to remap from 0-65535 to 0.0-1.0 ranges
const clampedLowFreq = Math.max(0, Math.min(65535, rumbleMsg.lowFrequency));
// Trigger actual rumble const rumbleLowFreq = this.remapFromTo(clampedLowFreq, 0, 65535, 0.0, 1.0);
// Need to remap from 0-65535 to 0.0-1.0 ranges const clampedHighFreq = Math.max(
const clampedLowFreq = Math.max(0, Math.min(65535, rumbleMsg.lowFrequency)); 0,
const rumbleLowFreq = this.remapFromTo( Math.min(65535, rumbleMsg.highFrequency),
clampedLowFreq, );
0, const rumbleHighFreq = this.remapFromTo(
65535, clampedHighFreq,
0.0, 0,
1.0, 65535,
); 0.0,
const clampedHighFreq = Math.max(0, Math.min(65535, rumbleMsg.highFrequency)); 1.0,
const rumbleHighFreq = this.remapFromTo( );
clampedHighFreq, // Cap to valid range (max 5000)
0, const rumbleDuration = Math.max(0, Math.min(5000, rumbleMsg.duration));
65535, if (this.gamepad.vibrationActuator) {
0.0, this.gamepad.vibrationActuator
1.0, .playEffect("dual-rumble", {
); startDelay: 0,
// Cap to valid range (max 5000) duration: rumbleDuration,
const rumbleDuration = Math.max(0, Math.min(5000, rumbleMsg.duration)); weakMagnitude: rumbleLowFreq,
if (this.gamepad.vibrationActuator) { strongMagnitude: rumbleHighFreq,
this.gamepad.vibrationActuator.playEffect("dual-rumble", { })
startDelay: 0, .catch(console.error);
duration: rumbleDuration,
weakMagnitude: rumbleLowFreq,
strongMagnitude: rumbleHighFreq,
}).catch(console.error);
}
}
} catch (error) {
console.error("Failed to decode rumble message:", error);
} }
} }
} }

View File

@@ -1,16 +1,9 @@
import {keyCodeToLinuxEventCode} from "./codes" import { keyCodeToLinuxEventCode } from "./codes";
import {WebRTCStream} from "./webrtc-stream"; import { WebRTCStream } from "./webrtc-stream";
import {LatencyTracker} from "./latency"; import { ProtoKeyDownSchema, ProtoKeyUpSchema } from "./proto/types_pb";
import {ProtoLatencyTracker, ProtoTimestampEntry} from "./proto/latency_tracker_pb"; import { create, toBinary } from "@bufbuild/protobuf";
import {timestampFromDate} from "@bufbuild/protobuf/wkt"; import { createMessage } from "./utils";
import {ProtoMessageBase, ProtoMessageInput, ProtoMessageInputSchema} from "./proto/messages_pb"; import { ProtoMessageSchema } from "./proto/messages_pb";
import {
ProtoInput,
ProtoInputSchema,
ProtoKeyDownSchema,
ProtoKeyUpSchema,
} from "./proto/types_pb";
import {create, toBinary} from "@bufbuild/protobuf";
interface Props { interface Props {
webrtc: WebRTCStream; webrtc: WebRTCStream;
@@ -24,38 +17,29 @@ export class Keyboard {
private readonly keydownListener: (e: KeyboardEvent) => void; private readonly keydownListener: (e: KeyboardEvent) => void;
private readonly keyupListener: (e: KeyboardEvent) => void; private readonly keyupListener: (e: KeyboardEvent) => void;
constructor({webrtc}: Props) { constructor({ webrtc }: Props) {
this.wrtc = webrtc; this.wrtc = webrtc;
this.keydownListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, { this.keydownListener = this.createKeyboardListener((e: any) =>
$typeName: "proto.ProtoInput", create(ProtoKeyDownSchema, {
inputType: { key: this.keyToVirtualKeyCode(e.code),
case: "keyDown", }),
value: create(ProtoKeyDownSchema, { );
type: "KeyDown", this.keyupListener = this.createKeyboardListener((e: any) =>
key: this.keyToVirtualKeyCode(e.code) create(ProtoKeyUpSchema, {
}), key: this.keyToVirtualKeyCode(e.code),
} }),
})); );
this.keyupListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, { this.run();
$typeName: "proto.ProtoInput",
inputType: {
case: "keyUp",
value: create(ProtoKeyUpSchema, {
type: "KeyUp",
key: this.keyToVirtualKeyCode(e.code)
}),
}
}));
this.run()
} }
private run() { private run() {
if (this.connected) if (this.connected) this.stop();
this.stop()
this.connected = true this.connected = true;
document.addEventListener("keydown", this.keydownListener, {passive: false}); document.addEventListener("keydown", this.keydownListener, {
document.addEventListener("keyup", this.keyupListener, {passive: false}); passive: false,
});
document.addEventListener("keyup", this.keyupListener, { passive: false });
} }
private stop() { private stop() {
@@ -65,42 +49,19 @@ export class Keyboard {
} }
// Helper function to create and return mouse listeners // 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) => { return (e: Event) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Prevent repeated key events from being sent (important for games) // Prevent repeated key events from being sent (important for games)
if ((e as any).repeat) if ((e as any).repeat) return;
return;
const data = dataCreator(e as any); const data = dataCreator(e as any);
// Latency tracking const message = createMessage(data, "input");
const tracker = new LatencyTracker("input-keyboard"); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
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));
}; };
} }
@@ -114,4 +75,4 @@ export class Keyboard {
if (code === "Home") return 1; if (code === "Home") return 1;
return keyCodeToLinuxEventCode[code] || undefined; return keyCodeToLinuxEventCode[code] || undefined;
} }
} }

View File

@@ -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;
}
}

View File

@@ -1,18 +1,14 @@
import {WebRTCStream} from "./webrtc-stream"; import { WebRTCStream } from "./webrtc-stream";
import {LatencyTracker} from "./latency";
import {ProtoMessageInput, ProtoMessageBase, ProtoMessageInputSchema} from "./proto/messages_pb";
import { import {
ProtoInput, ProtoInputSchema, ProtoMouseKeyDownSchema,
ProtoMouseKeyDown, ProtoMouseKeyDownSchema, ProtoMouseKeyUpSchema,
ProtoMouseKeyUp, ProtoMouseKeyUpSchema,
ProtoMouseMove,
ProtoMouseMoveSchema, ProtoMouseMoveSchema,
ProtoMouseWheel, ProtoMouseWheelSchema ProtoMouseWheelSchema,
} from "./proto/types_pb"; } from "./proto/types_pb";
import {mouseButtonToLinuxEventCode} from "./codes"; import { mouseButtonToLinuxEventCode } from "./codes";
import {ProtoLatencyTracker, ProtoTimestampEntry} from "./proto/latency_tracker_pb"; import { create, toBinary } from "@bufbuild/protobuf";
import {create, toBinary} from "@bufbuild/protobuf"; import { createMessage } from "./utils";
import {timestampFromDate} from "@bufbuild/protobuf/wkt"; import { ProtoMessageSchema } from "./proto/messages_pb";
interface Props { interface Props {
webrtc: WebRTCStream; webrtc: WebRTCStream;
@@ -24,7 +20,7 @@ export class Mouse {
protected canvas: HTMLCanvasElement; protected canvas: HTMLCanvasElement;
protected connected!: boolean; protected connected!: boolean;
private sendInterval = 10 // 100 updates per second private sendInterval = 10; // 100 updates per second
// Store references to event listeners // Store references to event listeners
private readonly mousemoveListener: (e: MouseEvent) => void; private readonly mousemoveListener: (e: MouseEvent) => void;
@@ -35,7 +31,7 @@ export class Mouse {
private readonly mouseupListener: (e: MouseEvent) => void; private readonly mouseupListener: (e: MouseEvent) => void;
private readonly mousewheelListener: (e: WheelEvent) => void; private readonly mousewheelListener: (e: WheelEvent) => void;
constructor({webrtc, canvas}: Props) { constructor({ webrtc, canvas }: Props) {
this.wrtc = webrtc; this.wrtc = webrtc;
this.canvas = canvas; this.canvas = canvas;
@@ -48,65 +44,56 @@ export class Mouse {
this.movementY += e.movementY; this.movementY += e.movementY;
}; };
this.mousedownListener = this.createMouseListener((e: any) => create(ProtoInputSchema, { this.mousedownListener = this.createMouseListener((e: any) =>
$typeName: "proto.ProtoInput", create(ProtoMouseKeyDownSchema, {
inputType: { key: this.keyToVirtualKeyCode(e.button),
case: "mouseKeyDown", }),
value: create(ProtoMouseKeyDownSchema, { );
type: "MouseKeyDown", this.mouseupListener = this.createMouseListener((e: any) =>
key: this.keyToVirtualKeyCode(e.button) create(ProtoMouseKeyUpSchema, {
}), key: this.keyToVirtualKeyCode(e.button),
} }),
})); );
this.mouseupListener = this.createMouseListener((e: any) => create(ProtoInputSchema, { this.mousewheelListener = this.createMouseListener((e: any) =>
$typeName: "proto.ProtoInput", create(ProtoMouseWheelSchema, {
inputType: { x: Math.round(e.deltaX),
case: "mouseKeyUp", y: Math.round(e.deltaY),
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.run() this.run();
this.startProcessing(); this.startProcessing();
} }
private run() { private run() {
//calls all the other functions //calls all the other functions
if (!document.pointerLockElement) { if (!document.pointerLockElement) {
console.log("no pointerlock") console.log("no pointerlock");
if (this.connected) { if (this.connected) {
this.stop() this.stop();
} }
return; return;
} }
if (document.pointerLockElement == this.canvas) { if (document.pointerLockElement == this.canvas) {
this.connected = true this.connected = true;
this.canvas.addEventListener("mousemove", this.mousemoveListener, {passive: false}); this.canvas.addEventListener("mousemove", this.mousemoveListener, {
this.canvas.addEventListener("mousedown", this.mousedownListener, {passive: false}); passive: false,
this.canvas.addEventListener("mouseup", this.mouseupListener, {passive: false}); });
this.canvas.addEventListener("wheel", this.mousewheelListener, {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 { } else {
if (this.connected) { if (this.connected) {
this.stop() this.stop();
} }
} }
} }
private stop() { private stop() {
@@ -128,79 +115,26 @@ export class Mouse {
} }
private sendAggregatedMouseMove() { private sendAggregatedMouseMove() {
const data = create(ProtoInputSchema, { const data = create(ProtoMouseMoveSchema, {
$typeName: "proto.ProtoInput", x: Math.round(this.movementX),
inputType: { y: Math.round(this.movementY),
case: "mouseMove",
value: create(ProtoMouseMoveSchema, {
type: "MouseMove",
x: Math.round(this.movementX),
y: Math.round(this.movementY),
}),
},
}); });
// Latency tracking const message = createMessage(data, "input");
const tracker = new LatencyTracker("input-mouse"); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
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));
} }
// Helper function to create and return mouse listeners // 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) => { return (e: Event) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const data = dataCreator(e as any); const data = dataCreator(e as any);
// Latency tracking const message = createMessage(data, "input");
const tracker = new LatencyTracker("input-mouse"); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
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));
}; };
} }
@@ -213,4 +147,4 @@ export class Mouse {
private keyToVirtualKeyCode(code: number) { private keyToVirtualKeyCode(code: number) {
return mouseButtonToLinuxEventCode[code] || undefined; return mouseButtonToLinuxEventCode[code] || undefined;
} }
} }

View File

@@ -4,7 +4,7 @@
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
import type { ProtoInput } from "./types_pb"; import type { ProtoClientDisconnected, ProtoClientRequestRoomStream, ProtoControllerAttach, ProtoControllerAxis, ProtoControllerButton, ProtoControllerDetach, ProtoControllerRumble, ProtoControllerStick, ProtoControllerTrigger, ProtoICE, ProtoKeyDown, ProtoKeyUp, ProtoMouseKeyDown, ProtoMouseKeyUp, ProtoMouseMove, ProtoMouseMoveAbs, ProtoMouseWheel, ProtoRaw, ProtoSDP, ProtoServerPushStream } from "./types_pb";
import { file_types } from "./types_pb"; import { file_types } from "./types_pb";
import type { ProtoLatencyTracker } from "./latency_tracker_pb"; import type { ProtoLatencyTracker } from "./latency_tracker_pb";
import { file_latency_tracker } 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. * Describes the file messages.proto.
*/ */
export const file_messages: GenFile = /*@__PURE__*/ export const file_messages: GenFile = /*@__PURE__*/
fileDesc("Cg5tZXNzYWdlcy5wcm90bxIFcHJvdG8iVQoQUHJvdG9NZXNzYWdlQmFzZRIUCgxwYXlsb2FkX3R5cGUYASABKAkSKwoHbGF0ZW5jeRgCIAEoCzIaLnByb3RvLlByb3RvTGF0ZW5jeVRyYWNrZXIiYwoRUHJvdG9NZXNzYWdlSW5wdXQSLQoMbWVzc2FnZV9iYXNlGAEgASgLMhcucHJvdG8uUHJvdG9NZXNzYWdlQmFzZRIfCgRkYXRhGAIgASgLMhEucHJvdG8uUHJvdG9JbnB1dEIWWhRyZWxheS9pbnRlcm5hbC9wcm90b2IGcHJvdG8z", [file_types, file_latency_tracker]); fileDesc("Cg5tZXNzYWdlcy5wcm90bxIFcHJvdG8iVQoQUHJvdG9NZXNzYWdlQmFzZRIUCgxwYXlsb2FkX3R5cGUYASABKAkSKwoHbGF0ZW5jeRgCIAEoCzIaLnByb3RvLlByb3RvTGF0ZW5jeVRyYWNrZXIiyQgKDFByb3RvTWVzc2FnZRItCgxtZXNzYWdlX2Jhc2UYASABKAsyFy5wcm90by5Qcm90b01lc3NhZ2VCYXNlEisKCm1vdXNlX21vdmUYAiABKAsyFS5wcm90by5Qcm90b01vdXNlTW92ZUgAEjIKDm1vdXNlX21vdmVfYWJzGAMgASgLMhgucHJvdG8uUHJvdG9Nb3VzZU1vdmVBYnNIABItCgttb3VzZV93aGVlbBgEIAEoCzIWLnByb3RvLlByb3RvTW91c2VXaGVlbEgAEjIKDm1vdXNlX2tleV9kb3duGAUgASgLMhgucHJvdG8uUHJvdG9Nb3VzZUtleURvd25IABIuCgxtb3VzZV9rZXlfdXAYBiABKAsyFi5wcm90by5Qcm90b01vdXNlS2V5VXBIABInCghrZXlfZG93bhgHIAEoCzITLnByb3RvLlByb3RvS2V5RG93bkgAEiMKBmtleV91cBgIIAEoCzIRLnByb3RvLlByb3RvS2V5VXBIABI5ChFjb250cm9sbGVyX2F0dGFjaBgJIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckF0dGFjaEgAEjkKEWNvbnRyb2xsZXJfZGV0YWNoGAogASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyRGV0YWNoSAASOQoRY29udHJvbGxlcl9idXR0b24YCyABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJCdXR0b25IABI7ChJjb250cm9sbGVyX3RyaWdnZXIYDCABKAsyHS5wcm90by5Qcm90b0NvbnRyb2xsZXJUcmlnZ2VySAASNwoQY29udHJvbGxlcl9zdGljaxgNIAEoCzIbLnByb3RvLlByb3RvQ29udHJvbGxlclN0aWNrSAASNQoPY29udHJvbGxlcl9heGlzGA4gASgLMhoucHJvdG8uUHJvdG9Db250cm9sbGVyQXhpc0gAEjkKEWNvbnRyb2xsZXJfcnVtYmxlGA8gASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyUnVtYmxlSAASHgoDaWNlGBQgASgLMg8ucHJvdG8uUHJvdG9JQ0VIABIeCgNzZHAYFSABKAsyDy5wcm90by5Qcm90b1NEUEgAEh4KA3JhdxgWIAEoCzIPLnByb3RvLlByb3RvUmF3SAASSQoaY2xpZW50X3JlcXVlc3Rfcm9vbV9zdHJlYW0YFyABKAsyIy5wcm90by5Qcm90b0NsaWVudFJlcXVlc3RSb29tU3RyZWFtSAASPQoTY2xpZW50X2Rpc2Nvbm5lY3RlZBgYIAEoCzIeLnByb3RvLlByb3RvQ2xpZW50RGlzY29ubmVjdGVkSAASOgoSc2VydmVyX3B1c2hfc3RyZWFtGBkgASgLMhwucHJvdG8uUHJvdG9TZXJ2ZXJQdXNoU3RyZWFtSABCCQoHcGF5bG9hZEIWWhRyZWxheS9pbnRlcm5hbC9wcm90b2IGcHJvdG8z", [file_types, file_latency_tracker]);
/** /**
* @generated from message proto.ProtoMessageBase * @generated from message proto.ProtoMessageBase
@@ -39,24 +39,148 @@ export const ProtoMessageBaseSchema: GenMessage<ProtoMessageBase> = /*@__PURE__*
messageDesc(file_messages, 0); 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; * @generated from field: proto.ProtoMessageBase message_base = 1;
*/ */
messageBase?: ProtoMessageBase; 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";
} | {
/**
* @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.ProtoControllerButton controller_button = 11;
*/
value: ProtoControllerButton;
case: "controllerButton";
} | {
/**
* @generated from field: proto.ProtoControllerTrigger controller_trigger = 12;
*/
value: ProtoControllerTrigger;
case: "controllerTrigger";
} | {
/**
* @generated from field: proto.ProtoControllerStick controller_stick = 13;
*/
value: ProtoControllerStick;
case: "controllerStick";
} | {
/**
* @generated from field: proto.ProtoControllerAxis controller_axis = 14;
*/
value: ProtoControllerAxis;
case: "controllerAxis";
} | {
/**
* @generated from field: proto.ProtoControllerRumble controller_rumble = 15;
*/
value: ProtoControllerRumble;
case: "controllerRumble";
} | {
/**
* 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. * Describes the message proto.ProtoMessage.
* Use `create(ProtoMessageInputSchema)` to create a new message. * Use `create(ProtoMessageSchema)` to create a new message.
*/ */
export const ProtoMessageInputSchema: GenMessage<ProtoMessageInput> = /*@__PURE__*/ export const ProtoMessageSchema: GenMessage<ProtoMessage> = /*@__PURE__*/
messageDesc(file_messages, 1); messageDesc(file_messages, 1);

View File

@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file types.proto. * Describes the file types.proto.
*/ */
export const file_types: GenFile = /*@__PURE__*/ export const file_types: GenFile = /*@__PURE__*/
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSI/ChVQcm90b0NvbnRyb2xsZXJBdHRhY2gSDAoEdHlwZRgBIAEoCRIKCgJpZBgCIAEoCRIMCgRzbG90GAMgASgFIjMKFVByb3RvQ29udHJvbGxlckRldGFjaBIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUiVAoVUHJvdG9Db250cm9sbGVyQnV0dG9uEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIOCgZidXR0b24YAyABKAUSDwoHcHJlc3NlZBgEIAEoCCJUChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIPCgd0cmlnZ2VyGAMgASgFEg0KBXZhbHVlGAQgASgFIlcKFFByb3RvQ29udHJvbGxlclN0aWNrEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRINCgVzdGljaxgDIAEoBRIJCgF4GAQgASgFEgkKAXkYBSABKAUiTgoTUHJvdG9Db250cm9sbGVyQXhpcxIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUSDAoEYXhpcxgDIAEoBRINCgV2YWx1ZRgEIAEoBSJ0ChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEdHlwZRgBIAEoCRIMCgRzbG90GAIgASgFEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi9QUKClByb3RvSW5wdXQSKwoKbW91c2VfbW92ZRgBIAEoCzIVLnByb3RvLlByb3RvTW91c2VNb3ZlSAASMgoObW91c2VfbW92ZV9hYnMYAiABKAsyGC5wcm90by5Qcm90b01vdXNlTW92ZUFic0gAEi0KC21vdXNlX3doZWVsGAMgASgLMhYucHJvdG8uUHJvdG9Nb3VzZVdoZWVsSAASMgoObW91c2Vfa2V5X2Rvd24YBCABKAsyGC5wcm90by5Qcm90b01vdXNlS2V5RG93bkgAEi4KDG1vdXNlX2tleV91cBgFIAEoCzIWLnByb3RvLlByb3RvTW91c2VLZXlVcEgAEicKCGtleV9kb3duGAYgASgLMhMucHJvdG8uUHJvdG9LZXlEb3duSAASIwoGa2V5X3VwGAcgASgLMhEucHJvdG8uUHJvdG9LZXlVcEgAEjkKEWNvbnRyb2xsZXJfYXR0YWNoGAggASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyQXR0YWNoSAASOQoRY29udHJvbGxlcl9kZXRhY2gYCSABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJEZXRhY2hIABI5ChFjb250cm9sbGVyX2J1dHRvbhgKIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckJ1dHRvbkgAEjsKEmNvbnRyb2xsZXJfdHJpZ2dlchgLIAEoCzIdLnByb3RvLlByb3RvQ29udHJvbGxlclRyaWdnZXJIABI3ChBjb250cm9sbGVyX3N0aWNrGAwgASgLMhsucHJvdG8uUHJvdG9Db250cm9sbGVyU3RpY2tIABI1Cg9jb250cm9sbGVyX2F4aXMYDSABKAsyGi5wcm90by5Qcm90b0NvbnRyb2xsZXJBeGlzSAASOQoRY29udHJvbGxlcl9ydW1ibGUYDiABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIAEIMCgppbnB1dF90eXBlQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM"); fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iJgoOUHJvdG9Nb3VzZU1vdmUSCQoBeBgBIAEoBRIJCgF5GAIgASgFIikKEVByb3RvTW91c2VNb3ZlQWJzEgkKAXgYASABKAUSCQoBeRgCIAEoBSInCg9Qcm90b01vdXNlV2hlZWwSCQoBeBgBIAEoBRIJCgF5GAIgASgFIiAKEVByb3RvTW91c2VLZXlEb3duEgsKA2tleRgBIAEoBSIeCg9Qcm90b01vdXNlS2V5VXASCwoDa2V5GAEgASgFIhsKDFByb3RvS2V5RG93bhILCgNrZXkYASABKAUiGQoKUHJvdG9LZXlVcBILCgNrZXkYASABKAUiRQoVUHJvdG9Db250cm9sbGVyQXR0YWNoEgoKAmlkGAEgASgJEgwKBHNsb3QYAiABKAUSEgoKc2Vzc2lvbl9pZBgDIAEoCSIlChVQcm90b0NvbnRyb2xsZXJEZXRhY2gSDAoEc2xvdBgBIAEoBSJGChVQcm90b0NvbnRyb2xsZXJCdXR0b24SDAoEc2xvdBgBIAEoBRIOCgZidXR0b24YAiABKAUSDwoHcHJlc3NlZBgDIAEoCCJGChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHNsb3QYASABKAUSDwoHdHJpZ2dlchgCIAEoBRINCgV2YWx1ZRgDIAEoBSJJChRQcm90b0NvbnRyb2xsZXJTdGljaxIMCgRzbG90GAEgASgFEg0KBXN0aWNrGAIgASgFEgkKAXgYAyABKAUSCQoBeRgEIAEoBSJAChNQcm90b0NvbnRyb2xsZXJBeGlzEgwKBHNsb3QYASABKAUSDAoEYXhpcxgCIAEoBRINCgV2YWx1ZRgDIAEoBSJmChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEc2xvdBgBIAEoBRIVCg1sb3dfZnJlcXVlbmN5GAIgASgFEhYKDmhpZ2hfZnJlcXVlbmN5GAMgASgFEhAKCGR1cmF0aW9uGAQgASgFIqoBChNSVENJY2VDYW5kaWRhdGVJbml0EhEKCWNhbmRpZGF0ZRgBIAEoCRIaCg1zZHBNTGluZUluZGV4GAIgASgNSACIAQESEwoGc2RwTWlkGAMgASgJSAGIAQESHQoQdXNlcm5hbWVGcmFnbWVudBgEIAEoCUgCiAEBQhAKDl9zZHBNTGluZUluZGV4QgkKB19zZHBNaWRCEwoRX3VzZXJuYW1lRnJhZ21lbnQiNgoZUlRDU2Vzc2lvbkRlc2NyaXB0aW9uSW5pdBILCgNzZHAYASABKAkSDAoEdHlwZRgCIAEoCSI5CghQcm90b0lDRRItCgljYW5kaWRhdGUYASABKAsyGi5wcm90by5SVENJY2VDYW5kaWRhdGVJbml0IjkKCFByb3RvU0RQEi0KA3NkcBgBIAEoCzIgLnByb3RvLlJUQ1Nlc3Npb25EZXNjcmlwdGlvbkluaXQiGAoIUHJvdG9SYXcSDAoEZGF0YRgBIAEoCSJFChxQcm90b0NsaWVudFJlcXVlc3RSb29tU3RyZWFtEhEKCXJvb21fbmFtZRgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJIkcKF1Byb3RvQ2xpZW50RGlzY29ubmVjdGVkEhIKCnNlc3Npb25faWQYASABKAkSGAoQY29udHJvbGxlcl9zbG90cxgCIAMoBSIqChVQcm90b1NlcnZlclB1c2hTdHJlYW0SEQoJcm9vbV9uYW1lGAEgASgJQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM");
/** /**
* MouseMove message * MouseMove message
@@ -19,19 +19,12 @@ export const file_types: GenFile = /*@__PURE__*/
*/ */
export type ProtoMouseMove = Message<"proto.ProtoMouseMove"> & { export type ProtoMouseMove = Message<"proto.ProtoMouseMove"> & {
/** /**
* Fixed value "MouseMove" * @generated from field: int32 x = 1;
*
* @generated from field: string type = 1;
*/
type: string;
/**
* @generated from field: int32 x = 2;
*/ */
x: number; x: number;
/** /**
* @generated from field: int32 y = 3; * @generated from field: int32 y = 2;
*/ */
y: number; y: number;
}; };
@@ -50,19 +43,12 @@ export const ProtoMouseMoveSchema: GenMessage<ProtoMouseMove> = /*@__PURE__*/
*/ */
export type ProtoMouseMoveAbs = Message<"proto.ProtoMouseMoveAbs"> & { export type ProtoMouseMoveAbs = Message<"proto.ProtoMouseMoveAbs"> & {
/** /**
* Fixed value "MouseMoveAbs" * @generated from field: int32 x = 1;
*
* @generated from field: string type = 1;
*/
type: string;
/**
* @generated from field: int32 x = 2;
*/ */
x: number; x: number;
/** /**
* @generated from field: int32 y = 3; * @generated from field: int32 y = 2;
*/ */
y: number; y: number;
}; };
@@ -81,19 +67,12 @@ export const ProtoMouseMoveAbsSchema: GenMessage<ProtoMouseMoveAbs> = /*@__PURE_
*/ */
export type ProtoMouseWheel = Message<"proto.ProtoMouseWheel"> & { export type ProtoMouseWheel = Message<"proto.ProtoMouseWheel"> & {
/** /**
* Fixed value "MouseWheel" * @generated from field: int32 x = 1;
*
* @generated from field: string type = 1;
*/
type: string;
/**
* @generated from field: int32 x = 2;
*/ */
x: number; x: number;
/** /**
* @generated from field: int32 y = 3; * @generated from field: int32 y = 2;
*/ */
y: number; y: number;
}; };
@@ -112,14 +91,7 @@ export const ProtoMouseWheelSchema: GenMessage<ProtoMouseWheel> = /*@__PURE__*/
*/ */
export type ProtoMouseKeyDown = Message<"proto.ProtoMouseKeyDown"> & { export type ProtoMouseKeyDown = Message<"proto.ProtoMouseKeyDown"> & {
/** /**
* Fixed value "MouseKeyDown" * @generated from field: int32 key = 1;
*
* @generated from field: string type = 1;
*/
type: string;
/**
* @generated from field: int32 key = 2;
*/ */
key: number; key: number;
}; };
@@ -138,14 +110,7 @@ export const ProtoMouseKeyDownSchema: GenMessage<ProtoMouseKeyDown> = /*@__PURE_
*/ */
export type ProtoMouseKeyUp = Message<"proto.ProtoMouseKeyUp"> & { export type ProtoMouseKeyUp = Message<"proto.ProtoMouseKeyUp"> & {
/** /**
* Fixed value "MouseKeyUp" * @generated from field: int32 key = 1;
*
* @generated from field: string type = 1;
*/
type: string;
/**
* @generated from field: int32 key = 2;
*/ */
key: number; key: number;
}; };
@@ -164,14 +129,7 @@ export const ProtoMouseKeyUpSchema: GenMessage<ProtoMouseKeyUp> = /*@__PURE__*/
*/ */
export type ProtoKeyDown = Message<"proto.ProtoKeyDown"> & { export type ProtoKeyDown = Message<"proto.ProtoKeyDown"> & {
/** /**
* Fixed value "KeyDown" * @generated from field: int32 key = 1;
*
* @generated from field: string type = 1;
*/
type: string;
/**
* @generated from field: int32 key = 2;
*/ */
key: number; key: number;
}; };
@@ -190,14 +148,7 @@ export const ProtoKeyDownSchema: GenMessage<ProtoKeyDown> = /*@__PURE__*/
*/ */
export type ProtoKeyUp = Message<"proto.ProtoKeyUp"> & { export type ProtoKeyUp = Message<"proto.ProtoKeyUp"> & {
/** /**
* Fixed value "KeyUp" * @generated from field: int32 key = 1;
*
* @generated from field: string type = 1;
*/
type: string;
/**
* @generated from field: int32 key = 2;
*/ */
key: number; key: number;
}; };
@@ -215,26 +166,26 @@ export const ProtoKeyUpSchema: GenMessage<ProtoKeyUp> = /*@__PURE__*/
* @generated from message proto.ProtoControllerAttach * @generated from message proto.ProtoControllerAttach
*/ */
export type ProtoControllerAttach = 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" * One of the following enums: "ps", "xbox" or "switch"
* *
* @generated from field: string id = 2; * @generated from field: string id = 1;
*/ */
id: string; id: string;
/** /**
* Slot number (0-3) * Slot number (0-3)
* *
* @generated from field: int32 slot = 3; * @generated from field: int32 slot = 2;
*/ */
slot: number; slot: number;
/**
* Session ID of the client attaching the controller
*
* @generated from field: string session_id = 3;
*/
sessionId: string;
}; };
/** /**
@@ -250,17 +201,10 @@ export const ProtoControllerAttachSchema: GenMessage<ProtoControllerAttach> = /*
* @generated from message proto.ProtoControllerDetach * @generated from message proto.ProtoControllerDetach
*/ */
export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & { export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & {
/**
* Fixed value "ControllerDetach"
*
* @generated from field: string type = 1;
*/
type: string;
/** /**
* Slot number (0-3) * Slot number (0-3)
* *
* @generated from field: int32 slot = 2; * @generated from field: int32 slot = 1;
*/ */
slot: number; slot: number;
}; };
@@ -278,31 +222,24 @@ export const ProtoControllerDetachSchema: GenMessage<ProtoControllerDetach> = /*
* @generated from message proto.ProtoControllerButton * @generated from message proto.ProtoControllerButton
*/ */
export type ProtoControllerButton = Message<"proto.ProtoControllerButton"> & { export type ProtoControllerButton = Message<"proto.ProtoControllerButton"> & {
/**
* Fixed value "ControllerButtons"
*
* @generated from field: string type = 1;
*/
type: string;
/** /**
* Slot number (0-3) * Slot number (0-3)
* *
* @generated from field: int32 slot = 2; * @generated from field: int32 slot = 1;
*/ */
slot: number; slot: number;
/** /**
* Button code (linux input event code) * Button code (linux input event code)
* *
* @generated from field: int32 button = 3; * @generated from field: int32 button = 2;
*/ */
button: number; button: number;
/** /**
* true if pressed, false if released * true if pressed, false if released
* *
* @generated from field: bool pressed = 4; * @generated from field: bool pressed = 3;
*/ */
pressed: boolean; pressed: boolean;
}; };
@@ -320,31 +257,24 @@ export const ProtoControllerButtonSchema: GenMessage<ProtoControllerButton> = /*
* @generated from message proto.ProtoControllerTrigger * @generated from message proto.ProtoControllerTrigger
*/ */
export type ProtoControllerTrigger = Message<"proto.ProtoControllerTrigger"> & { export type ProtoControllerTrigger = Message<"proto.ProtoControllerTrigger"> & {
/**
* Fixed value "ControllerTriggers"
*
* @generated from field: string type = 1;
*/
type: string;
/** /**
* Slot number (0-3) * Slot number (0-3)
* *
* @generated from field: int32 slot = 2; * @generated from field: int32 slot = 1;
*/ */
slot: number; slot: number;
/** /**
* Trigger number (0 for left, 1 for right) * Trigger number (0 for left, 1 for right)
* *
* @generated from field: int32 trigger = 3; * @generated from field: int32 trigger = 2;
*/ */
trigger: number; trigger: number;
/** /**
* trigger value (-32768 to 32767) * trigger value (-32768 to 32767)
* *
* @generated from field: int32 value = 4; * @generated from field: int32 value = 3;
*/ */
value: number; value: number;
}; };
@@ -362,38 +292,31 @@ export const ProtoControllerTriggerSchema: GenMessage<ProtoControllerTrigger> =
* @generated from message proto.ProtoControllerStick * @generated from message proto.ProtoControllerStick
*/ */
export type ProtoControllerStick = Message<"proto.ProtoControllerStick"> & { export type ProtoControllerStick = Message<"proto.ProtoControllerStick"> & {
/**
* Fixed value "ControllerStick"
*
* @generated from field: string type = 1;
*/
type: string;
/** /**
* Slot number (0-3) * Slot number (0-3)
* *
* @generated from field: int32 slot = 2; * @generated from field: int32 slot = 1;
*/ */
slot: number; slot: number;
/** /**
* Stick number (0 for left, 1 for right) * Stick number (0 for left, 1 for right)
* *
* @generated from field: int32 stick = 3; * @generated from field: int32 stick = 2;
*/ */
stick: number; stick: number;
/** /**
* X axis value (-32768 to 32767) * X axis value (-32768 to 32767)
* *
* @generated from field: int32 x = 4; * @generated from field: int32 x = 3;
*/ */
x: number; x: number;
/** /**
* Y axis value (-32768 to 32767) * Y axis value (-32768 to 32767)
* *
* @generated from field: int32 y = 5; * @generated from field: int32 y = 4;
*/ */
y: number; y: number;
}; };
@@ -411,31 +334,24 @@ export const ProtoControllerStickSchema: GenMessage<ProtoControllerStick> = /*@_
* @generated from message proto.ProtoControllerAxis * @generated from message proto.ProtoControllerAxis
*/ */
export type ProtoControllerAxis = Message<"proto.ProtoControllerAxis"> & { export type ProtoControllerAxis = Message<"proto.ProtoControllerAxis"> & {
/**
* Fixed value "ControllerAxis"
*
* @generated from field: string type = 1;
*/
type: string;
/** /**
* Slot number (0-3) * Slot number (0-3)
* *
* @generated from field: int32 slot = 2; * @generated from field: int32 slot = 1;
*/ */
slot: number; slot: number;
/** /**
* Axis number (0 for d-pad horizontal, 1 for d-pad vertical) * Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
* *
* @generated from field: int32 axis = 3; * @generated from field: int32 axis = 2;
*/ */
axis: number; axis: number;
/** /**
* axis value (-1 to 1) * axis value (-1 to 1)
* *
* @generated from field: int32 value = 4; * @generated from field: int32 value = 3;
*/ */
value: number; value: number;
}; };
@@ -453,38 +369,31 @@ export const ProtoControllerAxisSchema: GenMessage<ProtoControllerAxis> = /*@__P
* @generated from message proto.ProtoControllerRumble * @generated from message proto.ProtoControllerRumble
*/ */
export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & { export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & {
/**
* Fixed value "ControllerRumble"
*
* @generated from field: string type = 1;
*/
type: string;
/** /**
* Slot number (0-3) * Slot number (0-3)
* *
* @generated from field: int32 slot = 2; * @generated from field: int32 slot = 1;
*/ */
slot: number; slot: number;
/** /**
* Low frequency rumble (0-65535) * Low frequency rumble (0-65535)
* *
* @generated from field: int32 low_frequency = 3; * @generated from field: int32 low_frequency = 2;
*/ */
lowFrequency: number; lowFrequency: number;
/** /**
* High frequency rumble (0-65535) * High frequency rumble (0-65535)
* *
* @generated from field: int32 high_frequency = 4; * @generated from field: int32 high_frequency = 3;
*/ */
highFrequency: number; highFrequency: number;
/** /**
* Duration in milliseconds * Duration in milliseconds
* *
* @generated from field: int32 duration = 5; * @generated from field: int32 duration = 4;
*/ */
duration: number; duration: number;
}; };
@@ -497,105 +406,180 @@ export const ProtoControllerRumbleSchema: GenMessage<ProtoControllerRumble> = /*
messageDesc(file_types, 13); messageDesc(file_types, 13);
/** /**
* Union of all Input types * @generated from message proto.RTCIceCandidateInit
*
* @generated from message proto.ProtoInput
*/ */
export type ProtoInput = Message<"proto.ProtoInput"> & { export type RTCIceCandidateInit = Message<"proto.RTCIceCandidateInit"> & {
/** /**
* @generated from oneof proto.ProtoInput.input_type * @generated from field: string candidate = 1;
*/ */
inputType: { candidate: string;
/**
* @generated from field: proto.ProtoMouseMove mouse_move = 1; /**
*/ * @generated from field: optional uint32 sdpMLineIndex = 2;
value: ProtoMouseMove; */
case: "mouseMove"; sdpMLineIndex?: number;
} | {
/** /**
* @generated from field: proto.ProtoMouseMoveAbs mouse_move_abs = 2; * @generated from field: optional string sdpMid = 3;
*/ */
value: ProtoMouseMoveAbs; sdpMid?: string;
case: "mouseMoveAbs";
} | { /**
/** * @generated from field: optional string usernameFragment = 4;
* @generated from field: proto.ProtoMouseWheel mouse_wheel = 3; */
*/ usernameFragment?: string;
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 };
}; };
/** /**
* Describes the message proto.ProtoInput. * Describes the message proto.RTCIceCandidateInit.
* Use `create(ProtoInputSchema)` to create a new message. * Use `create(RTCIceCandidateInitSchema)` to create a new message.
*/ */
export const ProtoInputSchema: GenMessage<ProtoInput> = /*@__PURE__*/ export const RTCIceCandidateInitSchema: GenMessage<RTCIceCandidateInit> = /*@__PURE__*/
messageDesc(file_types, 14); messageDesc(file_types, 14);
/**
* @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, 15);
/**
* 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, 16);
/**
* 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, 17);
/**
* 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, 18);
/**
* 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, 19);
/**
* 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, 20);
/**
* 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, 21);

View 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();
}
}

View 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
});
}

View File

@@ -1,9 +1,3 @@
import {
NewMessageRaw,
NewMessageSDP,
NewMessageICE,
SafeStream,
} from "./messages";
import { webSockets } from "@libp2p/websockets"; import { webSockets } from "@libp2p/websockets";
import { webTransport } from "@libp2p/webtransport"; import { webTransport } from "@libp2p/webtransport";
import { createLibp2p, Libp2p } from "libp2p"; import { createLibp2p, Libp2p } from "libp2p";
@@ -13,19 +7,32 @@ import { identify } from "@libp2p/identify";
import { multiaddr } from "@multiformats/multiaddr"; import { multiaddr } from "@multiformats/multiaddr";
import { Connection } from "@libp2p/interface"; import { Connection } from "@libp2p/interface";
import { ping } from "@libp2p/ping"; 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"; const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0";
export class WebRTCStream { export class WebRTCStream {
private _p2p: Libp2p | undefined = undefined; private _p2p: Libp2p | undefined = undefined;
private _p2pConn: Connection | undefined = undefined; private _p2pConn: Connection | undefined = undefined;
private _p2pSafeStream: SafeStream | undefined = undefined; private _msgStream: P2PMessageStream | undefined = undefined;
private _pc: RTCPeerConnection | undefined = undefined; private _pc: RTCPeerConnection | undefined = undefined;
private _audioTrack: MediaStreamTrack | undefined = undefined; private _audioTrack: MediaStreamTrack | undefined = undefined;
private _videoTrack: MediaStreamTrack | undefined = undefined; private _videoTrack: MediaStreamTrack | undefined = undefined;
private _dataChannel: RTCDataChannel | undefined = undefined; private _dataChannel: RTCDataChannel | undefined = undefined;
private _onConnected: ((stream: MediaStream | null) => void) | undefined = undefined; private _onConnected: ((stream: MediaStream | null) => void) | undefined =
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined; undefined;
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined =
undefined;
private _serverURL: string | undefined = undefined; private _serverURL: string | undefined = undefined;
private _roomName: string | undefined = undefined; private _roomName: string | undefined = undefined;
private _isConnected: boolean = false; private _isConnected: boolean = false;
@@ -89,14 +96,20 @@ export class WebRTCStream {
.newStream(NESTRI_PROTOCOL_STREAM_REQUEST) .newStream(NESTRI_PROTOCOL_STREAM_REQUEST)
.catch(console.error); .catch(console.error);
if (stream) { if (stream) {
this._p2pSafeStream = new SafeStream(stream); this._msgStream = new P2PMessageStream(stream);
console.log("Stream opened with peer"); console.log("Stream opened with peer");
let iceHolder: RTCIceCandidateInit[] = []; 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) {
if (this._pc.remoteDescription) { if (this._pc.remoteDescription) {
this._pc.addIceCandidate(data.candidate).catch((err) => { this._pc.addIceCandidate(cand).catch((err) => {
console.error("Error adding ICE candidate:", err); console.error("Error adding ICE candidate:", err);
}); });
// Add held candidates // Add held candidates
@@ -107,45 +120,70 @@ export class WebRTCStream {
}); });
iceHolder = []; iceHolder = [];
} else { } else {
iceHolder.push(data.candidate); iceHolder.push(cand);
} }
} else { } else {
iceHolder.push(data.candidate); iceHolder.push(cand);
} }
}); });
this._p2pSafeStream.registerCallback("offer", async (data) => { this._msgStream.on("session-assigned", (data: ProtoClientRequestRoomStream) => {
const sessionId = data.sessionId;
localStorage.setItem("nestri-session-id", sessionId);
console.log("Session ID assigned:", sessionId, "for room:", data.roomName);
});
this._msgStream.on("offer", async (data: ProtoSDP) => {
if (!this._pc) { if (!this._pc) {
// Setup peer connection now // Setup peer connection now
this._setupPeerConnection(); this._setupPeerConnection();
} }
await this._pc!.setRemoteDescription(data.sdp); await this._pc!.setRemoteDescription({
sdp: data.sdp.sdp,
type: data.sdp.type as RTCSdpType,
});
// Create our answer // Create our answer
const answer = await this._pc!.createAnswer(); const answer = await this._pc!.createAnswer();
// Force stereo in Chromium browsers // Force stereo in Chromium browsers
answer.sdp = this.forceOpusStereo(answer.sdp!); answer.sdp = this.forceOpusStereo(answer.sdp!);
await this._pc!.setLocalDescription(answer); await this._pc!.setLocalDescription(answer);
// Send answer back // Send answer back
const answerMsg = NewMessageSDP("answer", answer); const answerMsg = createMessage(
await this._p2pSafeStream?.writeMessage(answerMsg); create(ProtoSDPSchema, {
sdp: answer,
}),
"answer",
);
await this._msgStream?.write(answerMsg);
}); });
this._p2pSafeStream.registerCallback("request-stream-offline", (data) => { this._msgStream.on("request-stream-offline", (msg: ProtoRaw) => {
console.warn("Stream is offline for room:", data.roomName); console.warn("Stream is offline for room:", msg.data);
this._onConnected?.(null); this._onConnected?.(null);
}); });
const clientId = localStorage.getItem("nestri-session-id");
if (clientId) {
console.debug("Using existing session ID:", clientId);
}
// Send stream request // Send stream request
// marshal room name into json const requestMsg = createMessage(
const request = NewMessageRaw( create(ProtoClientRequestRoomStreamSchema, {
roomName: roomName,
sessionId: clientId,
}),
"request-stream-room", "request-stream-room",
roomName,
); );
await this._p2pSafeStream.writeMessage(request); await this._msgStream.write(requestMsg);
} }
} }
} }
public getSessionID(): string {
return localStorage.getItem("nestri-session-id") || "";
}
// Forces opus to stereo in Chromium browsers, because of course // Forces opus to stereo in Chromium browsers, because of course
private forceOpusStereo(SDP: string): string { private forceOpusStereo(SDP: string): string {
// Look for "minptime=10;useinbandfec=1" and replace with "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1;" // Look for "minptime=10;useinbandfec=1" and replace with "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1;"
@@ -200,11 +238,16 @@ export class WebRTCStream {
this._pc.onicecandidate = (e) => { this._pc.onicecandidate = (e) => {
if (e.candidate) { if (e.candidate) {
const iceMsg = NewMessageICE("ice-candidate", e.candidate); const iceMsg = createMessage(
if (this._p2pSafeStream) { create(ProtoICESchema, {
this._p2pSafeStream.writeMessage(iceMsg).catch((err) => candidate: e.candidate,
console.error("Error sending ICE candidate:", err), }),
); "ice-candidate",
);
if (this._msgStream) {
this._msgStream
.write(iceMsg)
.catch((err) => console.error("Error sending ICE candidate:", err));
} else { } else {
console.warn("P2P stream not established, cannot send ICE candidate"); console.warn("P2P stream not established, cannot send ICE candidate");
} }
@@ -218,8 +261,7 @@ export class WebRTCStream {
} }
private _checkConnectionState() { private _checkConnectionState() {
if (!this._pc || !this._p2p || !this._p2pConn) if (!this._pc || !this._p2p || !this._p2pConn) return;
return;
console.debug("Checking connection state:", { console.debug("Checking connection state:", {
connectionState: this._pc.connectionState, connectionState: this._pc.connectionState,
@@ -286,7 +328,9 @@ export class WebRTCStream {
// Attempt to reconnect only if not already connected // Attempt to reconnect only if not already connected
if (!this._isConnected && this._serverURL && this._roomName) { 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 +379,9 @@ export class WebRTCStream {
} }
public removeDataChannelCallback(callback: (data: any) => void) { public removeDataChannelCallback(callback: (data: any) => void) {
this._dataChannelCallbacks = this._dataChannelCallbacks.filter(cb => cb !== callback); this._dataChannelCallbacks = this._dataChannelCallbacks.filter(
(cb) => cb !== callback,
);
} }
private _setupDataChannelEvents() { private _setupDataChannelEvents() {
@@ -343,7 +389,7 @@ export class WebRTCStream {
this._dataChannel.onclose = () => console.log("sendChannel has closed"); this._dataChannel.onclose = () => console.log("sendChannel has closed");
this._dataChannel.onopen = () => console.log("sendChannel has opened"); this._dataChannel.onopen = () => console.log("sendChannel has opened");
this._dataChannel.onmessage = (event => { this._dataChannel.onmessage = (event) => {
// Parse as ProtoBuf message // Parse as ProtoBuf message
const data = event.data; const data = event.data;
// Call registered callback if exists // Call registered callback if exists
@@ -354,7 +400,7 @@ export class WebRTCStream {
console.error("Error in data channel callback:", err); console.error("Error in data channel callback:", err);
} }
}); });
}); };
} }
private _gatherFrameRate() { private _gatherFrameRate() {

View File

@@ -106,7 +106,7 @@ if (envs_map.size > 0) {
if (e.gamepad.id.toLowerCase().includes("nestri")) if (e.gamepad.id.toLowerCase().includes("nestri"))
return; return;
let disconnected = nestriControllers.find((c) => c.getSlot() === e.gamepad.index); let disconnected = nestriControllers.find((c) => c.getLocalSlot() === e.gamepad.index);
if (disconnected) { if (disconnected) {
disconnected.dispose(); disconnected.dispose();
nestriControllers = nestriControllers.filter((c) => c !== disconnected); nestriControllers = nestriControllers.filter((c) => c !== disconnected);

View File

@@ -33,7 +33,7 @@ require (
github.com/ipfs/go-cid v0.5.0 // indirect github.com/ipfs/go-cid v0.5.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // 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/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/koron/go-ssdp v0.1.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-buffer-pool v0.1.0 // indirect

View File

@@ -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/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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y= github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y=

View File

@@ -3,16 +3,28 @@ package common
import ( import (
"bufio" "bufio"
"encoding/binary" "encoding/binary"
"encoding/json"
"errors" "errors"
"io" "io"
gen "relay/internal/proto"
"sync" "sync"
"google.golang.org/protobuf/proto" "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) // readUvarint reads an unsigned varint from the reader
const MaxSize = 1024 * 1024 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 // SafeBufioRW wraps a bufio.ReadWriter for sending and receiving JSON and protobufs safely
type SafeBufioRW struct { type SafeBufioRW struct {
@@ -24,83 +36,6 @@ func NewSafeBufioRW(brw *bufio.ReadWriter) *SafeBufioRW {
return &SafeBufioRW{brw: brw} 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 { func (bu *SafeBufioRW) SendProto(msg proto.Message) error {
bu.mutex.Lock() bu.mutex.Lock()
defer bu.mutex.Unlock() defer bu.mutex.Unlock()
@@ -110,12 +45,8 @@ func (bu *SafeBufioRW) SendProto(msg proto.Message) error {
return err return err
} }
if len(protoData) > MaxSize { // Write varint length prefix
return errors.New("protobuf data exceeds maximum size") if err := writeUvarint(bu.brw, uint64(len(protoData))); err != nil {
}
// Write the 4-byte length prefix
if err = binary.Write(bu.brw, binary.BigEndian, uint32(len(protoData))); err != nil {
return err return err
} }
@@ -124,25 +55,19 @@ func (bu *SafeBufioRW) SendProto(msg proto.Message) error {
return err return err
} }
// Flush the writer to ensure data is sent
return bu.brw.Flush() 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 { func (bu *SafeBufioRW) ReceiveProto(msg proto.Message) error {
bu.mutex.RLock() bu.mutex.RLock()
defer bu.mutex.RUnlock() defer bu.mutex.RUnlock()
// Read the 4-byte length prefix // Read varint length prefix
var length uint32 length, err := readUvarint(bu.brw)
if err := binary.Read(bu.brw, binary.BigEndian, &length); err != nil { if err != nil {
return err return err
} }
if length > MaxSize {
return errors.New("received Protobuf data exceeds maximum size")
}
// Read the Protobuf data // Read the Protobuf data
data := make([]byte, length) data := make([]byte, length)
if _, err := io.ReadFull(bu.brw, data); err != nil { 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) return proto.Unmarshal(data, msg)
} }
// Write writes raw data to the underlying buffer type CreateMessageOptions struct {
func (bu *SafeBufioRW) Write(data []byte) (int, error) { SequenceID string
bu.mutex.Lock() Latency *gen.ProtoLatencyTracker
defer bu.mutex.Unlock() }
if len(data) > MaxSize { func CreateMessage(payload proto.Message, payloadType string, opts *CreateMessageOptions) (*gen.ProtoMessage, error) {
return 0, errors.New("data exceeds maximum size") msg := &gen.ProtoMessage{
} MessageBase: &gen.ProtoMessageBase{
PayloadType: payloadType,
n, err := bu.brw.Write(data) },
if err != nil { }
return n, err
} if opts != nil {
if opts.Latency != nil {
// Flush the writer to ensure data is sent msg.MessageBase.Latency = opts.Latency
if err = bu.brw.Flush(); err != nil { } else if opts.SequenceID != "" {
return n, err msg.MessageBase.Latency = &gen.ProtoLatencyTracker{
} SequenceId: opts.SequenceID,
Timestamps: []*gen.ProtoTimestampEntry{
return n, nil {
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")
} }

View File

@@ -31,16 +31,18 @@ func NewNestriDataChannel(dc *webrtc.DataChannel) *NestriDataChannel {
} }
// Decode message // Decode message
var base gen.ProtoMessageInput var base gen.ProtoMessage
if err := proto.Unmarshal(msg.Data, &base); err != nil { if err := proto.Unmarshal(msg.Data, &base); err != nil {
slog.Error("failed to decode binary DataChannel message", "err", err) slog.Error("failed to decode binary DataChannel message", "err", err)
return return
} }
// Handle message type callback // Route based on PayloadType
if callback, ok := ndc.callbacks["input"]; ok { if base.MessageBase != nil && len(base.MessageBase.PayloadType) > 0 {
go callback(msg.Data) if callback, ok := ndc.callbacks[base.MessageBase.PayloadType]; ok {
} // We don't care about unhandled messages go callback(msg.Data)
}
}
}) })
return ndc return ndc

View File

@@ -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,
}
}

View File

@@ -10,6 +10,7 @@ import (
"os" "os"
"relay/internal/common" "relay/internal/common"
"relay/internal/shared" "relay/internal/shared"
"time"
"github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p"
pubsub "github.com/libp2p/go-libp2p-pubsub" pubsub "github.com/libp2p/go-libp2p-pubsub"
@@ -37,6 +38,16 @@ var globalRelay *Relay
// -- Structs -- // -- Structs --
// ClientSession tracks browser client connections
type ClientSession struct {
PeerID peer.ID
SessionID string
RoomName string
ConnectedAt time.Time
LastActivity time.Time
ControllerSlots []int32 // Track which controller slots this client owns
}
// Relay structure enhanced with metrics and state // Relay structure enhanced with metrics and state
type Relay struct { type Relay struct {
*PeerInfo *PeerInfo
@@ -48,6 +59,7 @@ type Relay struct {
// Local // Local
LocalRooms *common.SafeMap[ulid.ULID, *shared.Room] // room ID -> local Room struct (hosted by this relay) LocalRooms *common.SafeMap[ulid.ULID, *shared.Room] // room ID -> local Room struct (hosted by this relay)
LocalMeshConnections *common.SafeMap[peer.ID, *webrtc.PeerConnection] // peer ID -> PeerConnection (connected to this relay) LocalMeshConnections *common.SafeMap[peer.ID, *webrtc.PeerConnection] // peer ID -> PeerConnection (connected to this relay)
ClientSessions *common.SafeMap[peer.ID, *ClientSession] // peer ID -> ClientSession
// Protocols // Protocols
ProtocolRegistry ProtocolRegistry
@@ -144,6 +156,7 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
PingService: pingSvc, PingService: pingSvc,
LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](), LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](),
LocalMeshConnections: common.NewSafeMap[peer.ID, *webrtc.PeerConnection](), LocalMeshConnections: common.NewSafeMap[peer.ID, *webrtc.PeerConnection](),
ClientSessions: common.NewSafeMap[peer.ID, *ClientSession](),
} }
// Add network notifier after relay is initialized // Add network notifier after relay is initialized

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"log/slog" "log/slog"
"relay/internal/common"
"relay/internal/shared" "relay/internal/shared"
"time" "time"
gen "relay/internal/proto"
"google.golang.org/protobuf/proto"
pubsub "github.com/libp2p/go-libp2p-pubsub" pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
@@ -129,12 +134,51 @@ func (r *Relay) onPeerConnected(peerID peer.ID) {
// onPeerDisconnected marks a peer as disconnected in our status view and removes latency info // onPeerDisconnected marks a peer as disconnected in our status view and removes latency info
func (r *Relay) onPeerDisconnected(peerID peer.ID) { func (r *Relay) onPeerDisconnected(peerID peer.ID) {
// Check if this was a client session disconnect
if session, ok := r.ClientSessions.Get(peerID); ok {
slog.Info("Client session disconnected",
"peer", peerID,
"session", session.SessionID,
"room", session.RoomName,
"controller_slots", session.ControllerSlots)
// Send cleanup message to nestri-server if client had active controllers
if len(session.ControllerSlots) > 0 {
room := r.GetRoomByName(session.RoomName)
if room != nil && room.DataChannel != nil {
// Create disconnect notification
disconnectMsg, err := common.CreateMessage(&gen.ProtoClientDisconnected{
SessionId: session.SessionID,
ControllerSlots: session.ControllerSlots,
}, "client-disconnected", nil)
if err != nil {
slog.Error("Failed to create client disconnect message", "err", err)
}
disMarshal, err := proto.Marshal(disconnectMsg)
if err != nil {
slog.Error("Failed to marshal client disconnect message", "err", err)
} else {
if err = room.DataChannel.SendBinary(disMarshal); err != nil {
slog.Error("Failed to send client disconnect notification", "err", err)
} else {
slog.Info("Sent controller cleanup notification to nestri-server",
"session", session.SessionID,
"slots", session.ControllerSlots)
}
}
}
}
r.ClientSessions.Delete(peerID)
return
}
// Relay peer disconnect handling
slog.Info("Mesh peer disconnected, deleting from local peer map", "peer", peerID) slog.Info("Mesh peer disconnected, deleting from local peer map", "peer", peerID)
// Remove peer from local mesh peers
if r.Peers.Has(peerID) { if r.Peers.Has(peerID) {
r.Peers.Delete(peerID) r.Peers.Delete(peerID)
} }
// Remove any rooms associated with this peer
if r.Rooms.Has(peerID.String()) { if r.Rooms.Has(peerID.String()) {
r.Rooms.Delete(peerID.String()) r.Rooms.Delete(peerID.String())
} }

View File

@@ -73,28 +73,50 @@ func (x *ProtoMessageBase) GetLatency() *ProtoLatencyTracker {
return nil return nil
} }
type ProtoMessageInput struct { type ProtoMessage struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
MessageBase *ProtoMessageBase `protobuf:"bytes,1,opt,name=message_base,json=messageBase,proto3" json:"message_base,omitempty"` 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"` // 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_ControllerButton
// *ProtoMessage_ControllerTrigger
// *ProtoMessage_ControllerStick
// *ProtoMessage_ControllerAxis
// *ProtoMessage_ControllerRumble
// *ProtoMessage_Ice
// *ProtoMessage_Sdp
// *ProtoMessage_Raw
// *ProtoMessage_ClientRequestRoomStream
// *ProtoMessage_ClientDisconnected
// *ProtoMessage_ServerPushStream
Payload isProtoMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *ProtoMessageInput) Reset() { func (x *ProtoMessage) Reset() {
*x = ProtoMessageInput{} *x = ProtoMessage{}
mi := &file_messages_proto_msgTypes[1] mi := &file_messages_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
func (x *ProtoMessageInput) String() string { func (x *ProtoMessage) String() string {
return protoimpl.X.MessageStringOf(x) 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] mi := &file_messages_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -106,25 +128,331 @@ func (x *ProtoMessageInput) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use ProtoMessageInput.ProtoReflect.Descriptor instead. // Deprecated: Use ProtoMessage.ProtoReflect.Descriptor instead.
func (*ProtoMessageInput) Descriptor() ([]byte, []int) { func (*ProtoMessage) Descriptor() ([]byte, []int) {
return file_messages_proto_rawDescGZIP(), []int{1} return file_messages_proto_rawDescGZIP(), []int{1}
} }
func (x *ProtoMessageInput) GetMessageBase() *ProtoMessageBase { func (x *ProtoMessage) GetMessageBase() *ProtoMessageBase {
if x != nil { if x != nil {
return x.MessageBase return x.MessageBase
} }
return nil return nil
} }
func (x *ProtoMessageInput) GetData() *ProtoInput { func (x *ProtoMessage) GetPayload() isProtoMessage_Payload {
if x != nil { if x != nil {
return x.Data return x.Payload
} }
return nil 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) GetControllerButton() *ProtoControllerButton {
if x != nil {
if x, ok := x.Payload.(*ProtoMessage_ControllerButton); ok {
return x.ControllerButton
}
}
return nil
}
func (x *ProtoMessage) GetControllerTrigger() *ProtoControllerTrigger {
if x != nil {
if x, ok := x.Payload.(*ProtoMessage_ControllerTrigger); ok {
return x.ControllerTrigger
}
}
return nil
}
func (x *ProtoMessage) GetControllerStick() *ProtoControllerStick {
if x != nil {
if x, ok := x.Payload.(*ProtoMessage_ControllerStick); ok {
return x.ControllerStick
}
}
return nil
}
func (x *ProtoMessage) GetControllerAxis() *ProtoControllerAxis {
if x != nil {
if x, ok := x.Payload.(*ProtoMessage_ControllerAxis); ok {
return x.ControllerAxis
}
}
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) 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 {
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_ControllerButton struct {
ControllerButton *ProtoControllerButton `protobuf:"bytes,11,opt,name=controller_button,json=controllerButton,proto3,oneof"`
}
type ProtoMessage_ControllerTrigger struct {
ControllerTrigger *ProtoControllerTrigger `protobuf:"bytes,12,opt,name=controller_trigger,json=controllerTrigger,proto3,oneof"`
}
type ProtoMessage_ControllerStick struct {
ControllerStick *ProtoControllerStick `protobuf:"bytes,13,opt,name=controller_stick,json=controllerStick,proto3,oneof"`
}
type ProtoMessage_ControllerAxis struct {
ControllerAxis *ProtoControllerAxis `protobuf:"bytes,14,opt,name=controller_axis,json=controllerAxis,proto3,oneof"`
}
type ProtoMessage_ControllerRumble struct {
ControllerRumble *ProtoControllerRumble `protobuf:"bytes,15,opt,name=controller_rumble,json=controllerRumble,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_ControllerButton) isProtoMessage_Payload() {}
func (*ProtoMessage_ControllerTrigger) isProtoMessage_Payload() {}
func (*ProtoMessage_ControllerStick) isProtoMessage_Payload() {}
func (*ProtoMessage_ControllerAxis) isProtoMessage_Payload() {}
func (*ProtoMessage_ControllerRumble) 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 var File_messages_proto protoreflect.FileDescriptor
const file_messages_proto_rawDesc = "" + const file_messages_proto_rawDesc = "" +
@@ -132,10 +460,35 @@ const file_messages_proto_rawDesc = "" +
"\x0emessages.proto\x12\x05proto\x1a\vtypes.proto\x1a\x15latency_tracker.proto\"k\n" + "\x0emessages.proto\x12\x05proto\x1a\vtypes.proto\x1a\x15latency_tracker.proto\"k\n" +
"\x10ProtoMessageBase\x12!\n" + "\x10ProtoMessageBase\x12!\n" +
"\fpayload_type\x18\x01 \x01(\tR\vpayloadType\x124\n" + "\fpayload_type\x18\x01 \x01(\tR\vpayloadType\x124\n" +
"\alatency\x18\x02 \x01(\v2\x1a.proto.ProtoLatencyTrackerR\alatency\"v\n" + "\alatency\x18\x02 \x01(\v2\x1a.proto.ProtoLatencyTrackerR\alatency\"\xef\n" +
"\x11ProtoMessageInput\x12:\n" + "\n" +
"\fmessage_base\x18\x01 \x01(\v2\x17.proto.ProtoMessageBaseR\vmessageBase\x12%\n" + "\fProtoMessage\x12:\n" +
"\x04data\x18\x02 \x01(\v2\x11.proto.ProtoInputR\x04dataB\x16Z\x14relay/internal/protob\x06proto3" "\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_button\x18\v \x01(\v2\x1c.proto.ProtoControllerButtonH\x00R\x10controllerButton\x12N\n" +
"\x12controller_trigger\x18\f \x01(\v2\x1d.proto.ProtoControllerTriggerH\x00R\x11controllerTrigger\x12H\n" +
"\x10controller_stick\x18\r \x01(\v2\x1b.proto.ProtoControllerStickH\x00R\x0fcontrollerStick\x12E\n" +
"\x0fcontroller_axis\x18\x0e \x01(\v2\x1a.proto.ProtoControllerAxisH\x00R\x0econtrollerAxis\x12K\n" +
"\x11controller_rumble\x18\x0f \x01(\v2\x1c.proto.ProtoControllerRumbleH\x00R\x10controllerRumble\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 ( var (
file_messages_proto_rawDescOnce sync.Once file_messages_proto_rawDescOnce sync.Once
@@ -151,20 +504,58 @@ func file_messages_proto_rawDescGZIP() []byte {
var file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_messages_proto_goTypes = []any{ var file_messages_proto_goTypes = []any{
(*ProtoMessageBase)(nil), // 0: proto.ProtoMessageBase (*ProtoMessageBase)(nil), // 0: proto.ProtoMessageBase
(*ProtoMessageInput)(nil), // 1: proto.ProtoMessageInput (*ProtoMessage)(nil), // 1: proto.ProtoMessage
(*ProtoLatencyTracker)(nil), // 2: proto.ProtoLatencyTracker (*ProtoLatencyTracker)(nil), // 2: proto.ProtoLatencyTracker
(*ProtoInput)(nil), // 3: proto.ProtoInput (*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
(*ProtoControllerButton)(nil), // 12: proto.ProtoControllerButton
(*ProtoControllerTrigger)(nil), // 13: proto.ProtoControllerTrigger
(*ProtoControllerStick)(nil), // 14: proto.ProtoControllerStick
(*ProtoControllerAxis)(nil), // 15: proto.ProtoControllerAxis
(*ProtoControllerRumble)(nil), // 16: proto.ProtoControllerRumble
(*ProtoICE)(nil), // 17: proto.ProtoICE
(*ProtoSDP)(nil), // 18: proto.ProtoSDP
(*ProtoRaw)(nil), // 19: proto.ProtoRaw
(*ProtoClientRequestRoomStream)(nil), // 20: proto.ProtoClientRequestRoomStream
(*ProtoClientDisconnected)(nil), // 21: proto.ProtoClientDisconnected
(*ProtoServerPushStream)(nil), // 22: proto.ProtoServerPushStream
} }
var file_messages_proto_depIdxs = []int32{ var file_messages_proto_depIdxs = []int32{
2, // 0: proto.ProtoMessageBase.latency:type_name -> proto.ProtoLatencyTracker 2, // 0: proto.ProtoMessageBase.latency:type_name -> proto.ProtoLatencyTracker
0, // 1: proto.ProtoMessageInput.message_base:type_name -> proto.ProtoMessageBase 0, // 1: proto.ProtoMessage.message_base:type_name -> proto.ProtoMessageBase
3, // 2: proto.ProtoMessageInput.data:type_name -> proto.ProtoInput 3, // 2: proto.ProtoMessage.mouse_move:type_name -> proto.ProtoMouseMove
3, // [3:3] is the sub-list for method output_type 4, // 3: proto.ProtoMessage.mouse_move_abs:type_name -> proto.ProtoMouseMoveAbs
3, // [3:3] is the sub-list for method input_type 5, // 4: proto.ProtoMessage.mouse_wheel:type_name -> proto.ProtoMouseWheel
3, // [3:3] is the sub-list for extension type_name 6, // 5: proto.ProtoMessage.mouse_key_down:type_name -> proto.ProtoMouseKeyDown
3, // [3:3] is the sub-list for extension extendee 7, // 6: proto.ProtoMessage.mouse_key_up:type_name -> proto.ProtoMouseKeyUp
0, // [0:3] is the sub-list for field type_name 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_button:type_name -> proto.ProtoControllerButton
13, // 12: proto.ProtoMessage.controller_trigger:type_name -> proto.ProtoControllerTrigger
14, // 13: proto.ProtoMessage.controller_stick:type_name -> proto.ProtoControllerStick
15, // 14: proto.ProtoMessage.controller_axis:type_name -> proto.ProtoControllerAxis
16, // 15: proto.ProtoMessage.controller_rumble:type_name -> proto.ProtoControllerRumble
17, // 16: proto.ProtoMessage.ice:type_name -> proto.ProtoICE
18, // 17: proto.ProtoMessage.sdp:type_name -> proto.ProtoSDP
19, // 18: proto.ProtoMessage.raw:type_name -> proto.ProtoRaw
20, // 19: proto.ProtoMessage.client_request_room_stream:type_name -> proto.ProtoClientRequestRoomStream
21, // 20: proto.ProtoMessage.client_disconnected:type_name -> proto.ProtoClientDisconnected
22, // 21: proto.ProtoMessage.server_push_stream:type_name -> proto.ProtoServerPushStream
22, // [22:22] is the sub-list for method output_type
22, // [22:22] is the sub-list for method input_type
22, // [22:22] is the sub-list for extension type_name
22, // [22:22] is the sub-list for extension extendee
0, // [0:22] is the sub-list for field type_name
} }
func init() { file_messages_proto_init() } func init() { file_messages_proto_init() }
@@ -174,6 +565,28 @@ func file_messages_proto_init() {
} }
file_types_proto_init() file_types_proto_init()
file_latency_tracker_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_ControllerButton)(nil),
(*ProtoMessage_ControllerTrigger)(nil),
(*ProtoMessage_ControllerStick)(nil),
(*ProtoMessage_ControllerAxis)(nil),
(*ProtoMessage_ControllerRumble)(nil),
(*ProtoMessage_Ice)(nil),
(*ProtoMessage_Sdp)(nil),
(*ProtoMessage_Raw)(nil),
(*ProtoMessage_ClientRequestRoomStream)(nil),
(*ProtoMessage_ClientDisconnected)(nil),
(*ProtoMessage_ServerPushStream)(nil),
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{

File diff suppressed because it is too large Load Diff

View File

@@ -2,43 +2,59 @@ package shared
import ( import (
"fmt" "fmt"
"log/slog"
"relay/internal/common" "relay/internal/common"
"relay/internal/connections" "relay/internal/connections"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
) )
type Participant struct { type Participant struct {
ID ulid.ULID ID ulid.ULID
SessionID string // Track session for reconnection
PeerID peer.ID // libp2p peer ID
PeerConnection *webrtc.PeerConnection PeerConnection *webrtc.PeerConnection
DataChannel *connections.NestriDataChannel DataChannel *connections.NestriDataChannel
// Per-viewer tracks and channels
VideoTrack *webrtc.TrackLocalStaticRTP
AudioTrack *webrtc.TrackLocalStaticRTP
VideoChan chan *rtp.Packet
AudioChan chan *rtp.Packet
} }
func NewParticipant() (*Participant, error) { func NewParticipant(sessionID string, peerID peer.ID) (*Participant, error) {
id, err := common.NewULID() id, err := common.NewULID()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create ULID for Participant: %w", err) return nil, fmt.Errorf("failed to create ULID for Participant: %w", err)
} }
return &Participant{ return &Participant{
ID: id, ID: id,
SessionID: sessionID,
PeerID: peerID,
VideoChan: make(chan *rtp.Packet, 500),
AudioChan: make(chan *rtp.Packet, 100),
}, nil }, nil
} }
func (p *Participant) addTrack(trackLocal *webrtc.TrackLocalStaticRTP) error { // Close cleans up participant resources
rtpSender, err := p.PeerConnection.AddTrack(trackLocal) func (p *Participant) Close() {
if err != nil { if p.VideoChan != nil {
return err close(p.VideoChan)
p.VideoChan = nil
} }
if p.AudioChan != nil {
go func() { close(p.AudioChan)
rtcpBuffer := make([]byte, 1400) p.AudioChan = nil
for { }
if _, _, rtcpErr := rtpSender.Read(rtcpBuffer); rtcpErr != nil { if p.PeerConnection != nil {
break err := p.PeerConnection.Close()
} if err != nil {
slog.Error("Failed to close Participant PeerConnection", err)
} }
}() p.PeerConnection = nil
}
return nil
} }

View File

@@ -4,9 +4,11 @@ import (
"log/slog" "log/slog"
"relay/internal/common" "relay/internal/common"
"relay/internal/connections" "relay/internal/connections"
"time"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
) )
@@ -23,17 +25,31 @@ type Room struct {
VideoTrack *webrtc.TrackLocalStaticRTP VideoTrack *webrtc.TrackLocalStaticRTP
DataChannel *connections.NestriDataChannel DataChannel *connections.NestriDataChannel
Participants *common.SafeMap[ulid.ULID, *Participant] Participants *common.SafeMap[ulid.ULID, *Participant]
// Broadcast queues (unbuffered, fan-out happens async)
videoBroadcastChan chan *rtp.Packet
audioBroadcastChan chan *rtp.Packet
broadcastStop chan struct{}
} }
func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room { func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room {
return &Room{ r := &Room{
RoomInfo: RoomInfo{ RoomInfo: RoomInfo{
ID: roomID, ID: roomID,
Name: name, Name: name,
OwnerID: ownerID, OwnerID: ownerID,
}, },
Participants: common.NewSafeMap[ulid.ULID, *Participant](), Participants: common.NewSafeMap[ulid.ULID, *Participant](),
videoBroadcastChan: make(chan *rtp.Packet, 1000), // Large buffer for incoming packets
audioBroadcastChan: make(chan *rtp.Packet, 500),
broadcastStop: make(chan struct{}),
} }
// Start async broadcasters
go r.videoBroadcaster()
go r.audioBroadcaster()
return r
} }
// AddParticipant adds a Participant to a Room // AddParticipant adds a Participant to a Room
@@ -42,8 +58,8 @@ func (r *Room) AddParticipant(participant *Participant) {
r.Participants.Set(participant.ID, participant) r.Participants.Set(participant.ID, participant)
} }
// Removes a Participant from a Room by participant's ID // RemoveParticipantByID removes a Participant from a Room by participant's ID
func (r *Room) removeParticipantByID(pID ulid.ULID) { func (r *Room) RemoveParticipantByID(pID ulid.ULID) {
if _, ok := r.Participants.Get(pID); ok { if _, ok := r.Participants.Get(pID); ok {
r.Participants.Delete(pID) r.Participants.Delete(pID)
} }
@@ -64,3 +80,92 @@ func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalS
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType) slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
} }
} }
// BroadcastPacket enqueues packet for async broadcast (non-blocking)
func (r *Room) BroadcastPacket(kind webrtc.RTPCodecType, pkt *rtp.Packet) {
start := time.Now()
if kind == webrtc.RTPCodecTypeVideo {
select {
case r.videoBroadcastChan <- pkt:
duration := time.Since(start)
if duration > 10*time.Millisecond {
slog.Warn("Slow video broadcast enqueue", "duration", duration, "room", r.Name)
}
default:
// Broadcast queue full - system overload, drop packet globally
slog.Warn("Video broadcast queue full, dropping packet", "room", r.Name)
}
} else {
select {
case r.audioBroadcastChan <- pkt:
duration := time.Since(start)
if duration > 10*time.Millisecond {
slog.Warn("Slow audio broadcast enqueue", "duration", duration, "room", r.Name)
}
default:
slog.Warn("Audio broadcast queue full, dropping packet", "room", r.Name)
}
}
}
// Close stops the broadcasters
func (r *Room) Close() {
close(r.broadcastStop)
close(r.videoBroadcastChan)
close(r.audioBroadcastChan)
}
// videoBroadcaster runs async fan-out for video packets
func (r *Room) videoBroadcaster() {
for {
select {
case pkt := <-r.videoBroadcastChan:
// Fan out to all participants without blocking
r.Participants.Range(func(_ ulid.ULID, participant *Participant) bool {
if participant.VideoChan != nil {
// Clone packet for each participant to avoid shared pointer issues
clonedPkt := pkt.Clone()
select {
case participant.VideoChan <- clonedPkt:
// Sent
default:
// Participant slow, drop packet
slog.Debug("Dropped video packet for slow participant",
"room", r.Name,
"participant", participant.ID)
}
}
return true
})
case <-r.broadcastStop:
return
}
}
}
// audioBroadcaster runs async fan-out for audio packets
func (r *Room) audioBroadcaster() {
for {
select {
case pkt := <-r.audioBroadcastChan:
r.Participants.Range(func(_ ulid.ULID, participant *Participant) bool {
if participant.AudioChan != nil {
// Clone packet for each participant to avoid shared pointer issues
clonedPkt := pkt.Clone()
select {
case participant.AudioChan <- clonedPkt:
// Sent
default:
// Participant slow, drop packet
slog.Debug("Dropped audio packet for slow participant",
"room", r.Name,
"participant", participant.ID)
}
}
return true
})
case <-r.broadcastStop:
return
}
}
}

View File

@@ -3151,6 +3151,7 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"unsigned-varint 0.8.0",
"vimputti", "vimputti",
"webrtc", "webrtc",
] ]

View File

@@ -40,3 +40,4 @@ libp2p-tcp = { version = "0.44", features = ["tokio"] }
libp2p-websocket = "0.45" libp2p-websocket = "0.45"
dashmap = "6.1" dashmap = "6.1"
anyhow = "1.0" anyhow = "1.0"
unsigned-varint = "0.8"

View File

@@ -1,7 +1,5 @@
use crate::proto::proto::proto_input::InputType::{ use crate::proto::proto::ProtoControllerAttach;
ControllerAttach, ControllerAxis, ControllerButton, ControllerDetach, ControllerRumble, use crate::proto::proto::proto_message::Payload;
ControllerStick, ControllerTrigger,
};
use anyhow::Result; use anyhow::Result;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@@ -48,157 +46,234 @@ impl ControllerInput {
pub struct ControllerManager { pub struct ControllerManager {
vimputti_client: Arc<vimputti::client::VimputtiClient>, vimputti_client: Arc<vimputti::client::VimputtiClient>,
cmd_tx: mpsc::Sender<crate::proto::proto::ProtoInput>, cmd_tx: mpsc::Sender<Payload>,
rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>, // (slot, strong, weak, duration_ms) rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>, // (slot, strong, weak, duration_ms)
attach_tx: mpsc::Sender<ProtoControllerAttach>,
} }
impl ControllerManager { impl ControllerManager {
pub fn new( pub fn new(
vimputti_client: Arc<vimputti::client::VimputtiClient>, vimputti_client: Arc<vimputti::client::VimputtiClient>,
) -> Result<(Self, mpsc::Receiver<(u32, u16, u16, u16)>)> { ) -> Result<(
let (cmd_tx, cmd_rx) = mpsc::channel(100); Self,
let (rumble_tx, rumble_rx) = mpsc::channel(100); mpsc::Receiver<(u32, u16, u16, u16)>,
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( tokio::spawn(command_loop(
cmd_rx, cmd_rx,
vimputti_client.clone(), vimputti_client.clone(),
rumble_tx.clone(), rumble_tx.clone(),
attach_tx.clone(),
)); ));
Ok(( Ok((
Self { Self {
vimputti_client, vimputti_client,
cmd_tx, cmd_tx,
rumble_tx, rumble_tx,
attach_tx,
}, },
rumble_rx, rumble_rx,
attach_rx,
)) ))
} }
pub async fn send_command(&self, input: crate::proto::proto::ProtoInput) -> Result<()> { pub async fn send_command(&self, payload: Payload) -> Result<()> {
self.cmd_tx.send(input).await?; self.cmd_tx.send(payload).await?;
Ok(()) Ok(())
} }
} }
struct ControllerSlot {
controller: ControllerInput,
session_id: String,
last_activity: std::time::Instant,
}
// Returns first free controller slot from 0-7
fn get_free_slot(controllers: &HashMap<u32, ControllerSlot>) -> Option<u32> {
for slot in 0..8 {
if !controllers.contains_key(&slot) {
return Some(slot);
}
}
None
}
async fn command_loop( 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>, vimputti_client: Arc<vimputti::client::VimputtiClient>,
rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>, rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>,
attach_tx: mpsc::Sender<ProtoControllerAttach>,
) { ) {
let mut controllers: HashMap<u32, ControllerInput> = HashMap::new(); let mut controllers: HashMap<u32, ControllerSlot> = HashMap::new();
while let Some(input) = cmd_rx.recv().await { while let Some(payload) = cmd_rx.recv().await {
if let Some(input_type) = input.input_type { match payload {
match input_type { Payload::ControllerAttach(data) => {
ControllerAttach(data) => { let session_id = data.session_id.clone();
// Check if controller already exists in the slot, if so, ignore
if controllers.contains_key(&(data.slot as u32)) { // Check if this session already has a slot (reconnection)
tracing::warn!( let existing_slot = controllers
"Controller slot {} already occupied, ignoring attach", .iter()
data.slot .find(|(_, slot)| slot.session_id == session_id && !session_id.is_empty())
.map(|(slot_num, _)| *slot_num);
let slot = existing_slot.or_else(|| get_free_slot(&controllers));
if let Some(slot) = slot {
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));
})
.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(),
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(),
last_activity: std::time::Instant::now(),
},
);
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.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);
}
}
Payload::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.controller.device();
device.button(button, data.pressed);
device.sync();
}
} else {
tracing::warn!("Controller slot {} not found for button event", data.slot);
}
}
Payload::ControllerStick(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) {
let device = controller.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);
}
device.sync();
} else {
tracing::warn!("Controller slot {} not found for stick event", data.slot);
}
}
Payload::ControllerTrigger(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) {
let device = controller.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);
}
}
Payload::ControllerAxis(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) {
let device = controller.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();
}
}
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 { } else {
if let Ok(mut controller) = tracing::warn!(
ControllerInput::new(data.id.clone(), &vimputti_client).await "No controller found in slot {} to cleanup (client session: {})",
{ slot,
let slot = data.slot as u32; data.session_id
let rumble_tx = rumble_tx.clone(); );
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
);
}
} }
} }
ControllerDetach(data) => { }
if controllers.remove(&(data.slot as u32)).is_some() { _ => {
tracing::info!("Controller detached from slot {}", data.slot); //no-op
} 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();
}
} 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);
}
device.sync();
} else {
tracing::warn!("Controller slot {} not found for stick event", data.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
}
} }
} }
} }

View File

@@ -3,7 +3,6 @@ mod enc_helper;
mod gpu; mod gpu;
mod input; mod input;
mod latency; mod latency;
mod messages;
mod nestrisink; mod nestrisink;
mod p2p; mod p2p;
mod proto; mod proto;
@@ -257,11 +256,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
None None
} }
}; };
let (controller_manager, rumble_rx) = if let Some(vclient) = vimputti_client { let (controller_manager, rumble_rx, attach_rx) = if let Some(vclient) = vimputti_client {
let (controller_manager, rumble_rx) = ControllerManager::new(vclient)?; let (controller_manager, rumble_rx, attach_rx) = ControllerManager::new(vclient)?;
(Some(Arc::new(controller_manager)), Some(rumble_rx)) (
Some(Arc::new(controller_manager)),
Some(rumble_rx),
Some(attach_rx),
)
} else { } else {
(None, None) (None, None, None)
}; };
/*** PIPELINE CREATION ***/ /*** PIPELINE CREATION ***/
@@ -416,6 +419,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
video_source.clone(), video_source.clone(),
controller_manager, controller_manager,
rumble_rx, rumble_rx,
attach_rx,
) )
.await?; .await?;
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone())); let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone()));
@@ -550,7 +554,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
} }
// Make sure QOS is disabled to avoid latency // Make sure QOS is disabled to avoid latency
video_encoder.set_property("qos", false); video_encoder.set_property("qos", true);
// Optimize latency of pipeline // Optimize latency of pipeline
video_source video_source

View File

@@ -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,
}

View File

@@ -1,11 +1,11 @@
use crate::input::controller::ControllerManager; use crate::input::controller::ControllerManager;
use crate::messages::{MessageBase, MessageICE, MessageRaw, MessageSDP};
use crate::p2p::p2p::NestriConnection; use crate::p2p::p2p::NestriConnection;
use crate::p2p::p2p_protocol_stream::NestriStreamProtocol; use crate::p2p::p2p_protocol_stream::NestriStreamProtocol;
use crate::proto::proto::proto_input::InputType::{ use crate::proto::proto::proto_message::Payload;
KeyDown, KeyUp, MouseKeyDown, MouseKeyUp, MouseMove, MouseMoveAbs, MouseWheel, use crate::proto::proto::{
ProtoControllerAttach, ProtoControllerRumble, ProtoIce, ProtoMessage, ProtoSdp,
ProtoServerPushStream, RtcIceCandidateInit, RtcSessionDescriptionInit,
}; };
use crate::proto::proto::{ProtoInput, ProtoMessageInput};
use anyhow::Result; use anyhow::Result;
use glib::subclass::prelude::*; use glib::subclass::prelude::*;
use gstreamer::glib; use gstreamer::glib;
@@ -16,8 +16,6 @@ use parking_lot::RwLock as PLRwLock;
use prost::Message; use prost::Message;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use tokio::sync::{Mutex, mpsc}; use tokio::sync::{Mutex, mpsc};
use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit;
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
pub struct Signaller { pub struct Signaller {
stream_room: PLRwLock<Option<String>>, stream_room: PLRwLock<Option<String>>,
@@ -26,6 +24,7 @@ pub struct Signaller {
data_channel: PLRwLock<Option<Arc<gstreamer_webrtc::WebRTCDataChannel>>>, data_channel: PLRwLock<Option<Arc<gstreamer_webrtc::WebRTCDataChannel>>>,
controller_manager: PLRwLock<Option<Arc<ControllerManager>>>, 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)>>>,
attach_rx: Mutex<Option<mpsc::Receiver<ProtoControllerAttach>>>,
} }
impl Default for Signaller { impl Default for Signaller {
fn default() -> Self { fn default() -> Self {
@@ -36,6 +35,7 @@ impl Default for Signaller {
data_channel: PLRwLock::new(None), data_channel: PLRwLock::new(None),
controller_manager: PLRwLock::new(None), controller_manager: PLRwLock::new(None),
rumble_rx: Mutex::new(None), rumble_rx: Mutex::new(None),
attach_rx: Mutex::new(None),
} }
} }
} }
@@ -74,11 +74,23 @@ impl Signaller {
*self.rumble_rx.lock().await = Some(rumble_rx); *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)>> {
self.rumble_rx.lock().await.take() 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) { pub fn set_data_channel(&self, data_channel: gstreamer_webrtc::WebRTCDataChannel) {
*self.data_channel.write() = Some(Arc::new(data_channel)); *self.data_channel.write() = Some(Arc::new(data_channel));
} }
@@ -95,68 +107,85 @@ impl Signaller {
}; };
{ {
let self_obj = self.obj().clone(); let self_obj = self.obj().clone();
stream_protocol.register_callback("answer", move |data| { stream_protocol.register_callback("answer", move |msg| {
if let Ok(message) = serde_json::from_slice::<MessageSDP>(&data) { if let Some(payload) = msg.payload {
let sdp = gst_sdp::SDPMessage::parse_buffer(message.sdp.sdp.as_bytes()) match payload {
.map_err(|e| anyhow::anyhow!("Invalid SDP in 'answer': {e:?}"))?; Payload::Sdp(sdp) => {
let answer = WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp); if let Some(sdp) = sdp.sdp {
Ok(self_obj.emit_by_name::<()>( let sdp = gst_sdp::SDPMessage::parse_buffer(sdp.sdp.as_bytes())
"session-description", .map_err(|e| {
&[&"unique-session-id", &answer], 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 { } else {
anyhow::bail!("Failed to decode SDP message"); anyhow::bail!("Failed to decode answer message");
} }
Ok(())
}); });
} }
{ {
let self_obj = self.obj().clone(); let self_obj = self.obj().clone();
stream_protocol.register_callback("ice-candidate", move |data| { stream_protocol.register_callback("ice-candidate", move |msg| {
if let Ok(message) = serde_json::from_slice::<MessageICE>(&data) { if let Some(payload) = msg.payload {
let candidate = message.candidate; match payload {
let sdp_m_line_index = candidate.sdp_mline_index.unwrap_or(0) as u32; Payload::Ice(ice) => {
let sdp_mid = candidate.sdp_mid; if let Some(candidate) = ice.candidate {
Ok(self_obj.emit_by_name::<()>( let sdp_m_line_index = candidate.sdp_m_line_index.unwrap_or(0);
"handle-ice", return Ok(self_obj.emit_by_name::<()>(
&[ "handle-ice",
&"unique-session-id", &[
&sdp_m_line_index, &"unique-session-id",
&sdp_mid, &sdp_m_line_index,
&candidate.candidate, &candidate.sdp_mid,
], &candidate.candidate,
)) ],
));
}
}
_ => {
tracing::warn!("Unexpected payload type for ice-candidate");
return Ok(());
}
}
} else { } else {
anyhow::bail!("Failed to decode ICE message"); anyhow::bail!("Failed to decode ICE message");
} }
Ok(())
}); });
} }
{ {
let self_obj = self.obj().clone(); let self_obj = self.obj().clone();
stream_protocol.register_callback("push-stream-ok", move |data| { stream_protocol.register_callback("push-stream-ok", move |msg| {
if let Ok(answer) = serde_json::from_slice::<MessageRaw>(&data) { if let Some(payload) = msg.payload {
// Decode room name string return match payload {
if let Some(room_name) = answer.data.as_str() { Payload::ServerPushStream(_res) => {
gstreamer::info!( // Send our SDP offer
gstreamer::CAT_DEFAULT, Ok(self_obj.emit_by_name::<()>(
"Received OK answer for room: {}", "session-requested",
room_name &[
); &"unique-session-id",
} else { &"consumer-identifier",
gstreamer::error!( &None::<WebRTCSessionDescription>,
gstreamer::CAT_DEFAULT, ],
"Failed to decode room name from answer" ))
); }
} _ => {
tracing::warn!("Unexpected payload type for push-stream-ok");
// Send our SDP offer Ok(())
Ok(self_obj.emit_by_name::<()>( }
"session-requested", };
&[
&"unique-session-id",
&"consumer-identifier",
&None::<WebRTCSessionDescription>,
],
))
} else { } else {
anyhow::bail!("Failed to decode answer"); anyhow::bail!("Failed to decode answer");
} }
@@ -200,12 +229,14 @@ impl Signaller {
// Spawn async task to take the receiver and set up // Spawn async task to take the receiver and set up
tokio::spawn(async move { tokio::spawn(async move {
let rumble_rx = signaller.imp().take_rumble_rx().await; let rumble_rx = signaller.imp().take_rumble_rx().await;
let attach_rx = signaller.imp().take_attach_rx().await;
let controller_manager = let controller_manager =
signaller.imp().get_controller_manager(); signaller.imp().get_controller_manager();
setup_data_channel( setup_data_channel(
controller_manager, controller_manager,
rumble_rx, rumble_rx,
attach_rx,
data_channel, data_channel,
&wayland_src, &wayland_src,
); );
@@ -243,19 +274,18 @@ impl SignallableImpl for Signaller {
return; 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 { let Some(stream_protocol) = self.get_stream_protocol() else {
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set"); gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
return; 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) { if let Err(e) = stream_protocol.send_message(&push_msg) {
tracing::error!("Failed to send push stream room message: {:?}", e); 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) { 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 { let Some(stream_protocol) = self.get_stream_protocol() else {
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set"); gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
return; 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); tracing::error!("Failed to send SDP message: {:?}", e);
} }
} }
@@ -291,26 +323,25 @@ impl SignallableImpl for Signaller {
sdp_m_line_index: u32, sdp_m_line_index: u32,
sdp_mid: Option<String>, 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 { let Some(stream_protocol) = self.get_stream_protocol() else {
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set"); gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
return; 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); tracing::error!("Failed to send ICE candidate message: {:?}", e);
} }
} }
@@ -352,6 +383,7 @@ impl ObjectImpl for Signaller {
fn setup_data_channel( fn setup_data_channel(
controller_manager: Option<Arc<ControllerManager>>, 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)>>, // (slot, strong, weak, duration_ms)
attach_rx: Option<mpsc::Receiver<ProtoControllerAttach>>,
data_channel: Arc<gstreamer_webrtc::WebRTCDataChannel>, data_channel: Arc<gstreamer_webrtc::WebRTCDataChannel>,
wayland_src: &gstreamer::Element, wayland_src: &gstreamer::Element,
) { ) {
@@ -361,11 +393,11 @@ fn setup_data_channel(
// Spawn async processor // Spawn async processor
tokio::spawn(async move { tokio::spawn(async move {
while let Some(data) = rx.recv().await { while let Some(data) = rx.recv().await {
match ProtoMessageInput::decode(data.as_slice()) { match ProtoMessage::decode(data.as_slice()) {
Ok(message_input) => { Ok(msg_wrapper) => {
if let Some(message_base) = message_input.message_base { if let Some(message_base) = msg_wrapper.message_base {
if message_base.payload_type == "input" { 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) { if let Some(event) = handle_input_message(input_data) {
// Send the event to wayland source, result bool is ignored // Send the event to wayland source, result bool is ignored
let _ = wayland_src.send_event(event); let _ = wayland_src.send_event(event);
@@ -373,7 +405,7 @@ fn setup_data_channel(
} }
} else if message_base.payload_type == "controllerInput" { } else if message_base.payload_type == "controllerInput" {
if let Some(controller_manager) = &controller_manager { 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; let _ = controller_manager.send_command(input_data).await;
} }
} }
@@ -392,25 +424,16 @@ fn setup_data_channel(
let data_channel_clone = data_channel.clone(); let data_channel_clone = data_channel.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some((slot, strong, weak, duration_ms)) = rumble_rx.recv().await { while let Some((slot, strong, weak, duration_ms)) = rumble_rx.recv().await {
let rumble_msg = ProtoMessageInput { let rumble_msg = crate::proto::create_message(
message_base: Some(crate::proto::proto::ProtoMessageBase { Payload::ControllerRumble(ProtoControllerRumble {
payload_type: "controllerInput".to_string(), slot: slot as i32,
latency: None, low_frequency: weak as i32,
high_frequency: strong as i32,
duration: duration_ms as i32,
}), }),
data: Some(ProtoInput { "controllerInput",
input_type: Some( None,
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,
},
),
),
}),
};
let data = rumble_msg.encode_to_vec(); let data = rumble_msg.encode_to_vec();
let bytes = glib::Bytes::from_owned(data); let bytes = glib::Bytes::from_owned(data);
@@ -422,6 +445,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| { data_channel.connect_on_message_data(move |_data_channel, data| {
if let Some(data) = data { if let Some(data) = data {
let _ = tx.send(data.to_vec()); let _ = tx.send(data.to_vec());
@@ -429,68 +473,64 @@ fn setup_data_channel(
}); });
} }
fn handle_input_message(input_msg: ProtoInput) -> Option<gstreamer::Event> { fn handle_input_message(payload: Payload) -> Option<gstreamer::Event> {
if let Some(input_type) = input_msg.input_type { match payload {
match input_type { Payload::MouseMove(data) => {
MouseMove(data) => { let structure = gstreamer::Structure::builder("MouseMoveRelative")
let structure = gstreamer::Structure::builder("MouseMoveRelative") .field("pointer_x", data.x as f64)
.field("pointer_x", data.x as f64) .field("pointer_y", data.y as f64)
.field("pointer_y", data.y as f64) .build();
.build();
Some(gstreamer::event::CustomUpstream::new(structure)) 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,
} }
} else { Payload::MouseMoveAbs(data) => {
None 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,
} }
} }

View File

@@ -19,6 +19,7 @@ impl NestriSignaller {
wayland_src: Arc<gstreamer::Element>, wayland_src: Arc<gstreamer::Element>,
controller_manager: Option<Arc<ControllerManager>>, controller_manager: Option<Arc<ControllerManager>>,
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16)>>, rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16)>>,
attach_rx: Option<mpsc::Receiver<crate::proto::proto::ProtoControllerAttach>>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
let obj: Self = glib::Object::new(); let obj: Self = glib::Object::new();
obj.imp().set_stream_room(room); obj.imp().set_stream_room(room);
@@ -30,6 +31,9 @@ impl NestriSignaller {
if let Some(rumble_rx) = rumble_rx { if let Some(rumble_rx) = rumble_rx {
obj.imp().set_rumble_rx(rumble_rx).await; 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) Ok(obj)
} }
} }

View File

@@ -3,21 +3,22 @@ use crate::p2p::p2p_safestream::SafeStream;
use anyhow::Result; use anyhow::Result;
use dashmap::DashMap; use dashmap::DashMap;
use libp2p::StreamProtocol; use libp2p::StreamProtocol;
use prost::Message;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
// Cloneable callback type // 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>); pub struct Callback(Arc<CallbackInner>);
impl Callback { impl Callback {
pub fn new<F>(f: F) -> Self pub fn new<F>(f: F) -> Self
where where
F: Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static, F: Fn(crate::proto::proto::ProtoMessage) -> Result<()> + Send + Sync + 'static,
{ {
Callback(Arc::new(f)) 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) self.0(data)
} }
} }
@@ -104,26 +105,31 @@ impl NestriStreamProtocol {
} }
}; };
match serde_json::from_slice::<crate::messages::MessageBase>(&data) { match crate::proto::proto::ProtoMessage::decode(data.as_slice()) {
Ok(base_message) => { Ok(message) => {
let response_type = base_message.payload_type; 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 // With DashMap, we don't need explicit locking
// we just get the callback directly if it exists // we just get the callback directly if it exists
if let Some(callback) = callbacks.get(&response_type) { if let Some(callback) = callbacks.get(&response_type) {
// Execute the callback // Execute the callback
if let Err(e) = callback.call(data.clone()) { if let Err(e) = callback.call(message) {
tracing::error!( tracing::error!(
"Callback for response type '{}' errored: {:?}", "Callback for response type '{}' errored: {:?}",
response_type, response_type,
e e
);
}
} else {
tracing::warn!(
"No callback registered for response type: {}",
response_type
); );
} }
} else { } else {
tracing::warn!( tracing::error!("No base message in decoded protobuf message",);
"No callback registered for response type: {}",
response_type
);
} }
} }
Err(e) => { Err(e) => {
@@ -154,8 +160,9 @@ impl NestriStreamProtocol {
}) })
} }
pub fn send_message<M: serde::Serialize>(&self, message: &M) -> Result<()> { pub fn send_message(&self, message: &crate::proto::proto::ProtoMessage) -> Result<()> {
let json_data = serde_json::to_vec(message)?; let mut buf = Vec::new();
message.encode(&mut buf)?;
let Some(tx) = &self.tx else { let Some(tx) = &self.tx else {
return Err(anyhow::Error::msg( return Err(anyhow::Error::msg(
if self.read_handle.is_none() && self.write_handle.is_none() { 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(()) Ok(())
} }
pub fn register_callback<F>(&self, response_type: &str, callback: F) pub fn register_callback<F>(&self, response_type: &str, callback: F)
where where
F: Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static, F: Fn(crate::proto::proto::ProtoMessage) -> Result<()> + Send + Sync + 'static,
{ {
self.callbacks self.callbacks
.insert(response_type.to_string(), Callback::new(callback)); .insert(response_type.to_string(), Callback::new(callback));

View File

@@ -1,11 +1,9 @@
use anyhow::Result; use anyhow::Result;
use byteorder::{BigEndian, ByteOrder};
use libp2p::futures::io::{ReadHalf, WriteHalf}; use libp2p::futures::io::{ReadHalf, WriteHalf};
use libp2p::futures::{AsyncReadExt, AsyncWriteExt}; use libp2p::futures::{AsyncReadExt, AsyncWriteExt};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use unsigned_varint::{decode, encode};
const MAX_SIZE: usize = 1024 * 1024; // 1MB
pub struct SafeStream { pub struct SafeStream {
stream_read: Arc<Mutex<ReadHalf<libp2p::Stream>>>, stream_read: Arc<Mutex<ReadHalf<libp2p::Stream>>>,
@@ -29,34 +27,52 @@ impl SafeStream {
} }
async fn send_with_length_prefix(&self, data: &[u8]) -> Result<()> { 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; 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?; stream_write.flush().await?;
Ok(()) Ok(())
} }
async fn receive_with_length_prefix(&self) -> Result<Vec<u8>> { async fn receive_with_length_prefix(&self) -> Result<Vec<u8>> {
let mut stream_read = self.stream_read.lock().await; let mut stream_read = self.stream_read.lock().await;
// Read length prefix + data in one syscall // Read varint length prefix (up to 10 bytes for u64)
let mut length_prefix = [0u8; 4]; let mut length_buf = Vec::new();
stream_read.read_exact(&mut length_prefix).await?; let mut temp_byte = [0u8; 1];
let length = BigEndian::read_u32(&length_prefix) as usize;
if length > MAX_SIZE { loop {
anyhow::bail!("Received data exceeds maximum size"); 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]; let mut buffer = vec![0u8; length];
stream_read.read_exact(&mut buffer).await?; stream_read.read_exact(&mut buffer).await?;
Ok(buffer) Ok(buffer)
} }
} }

View File

@@ -1 +1,35 @@
pub mod proto; 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),
}
}

View File

@@ -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)

View File

@@ -20,80 +20,59 @@ pub struct ProtoLatencyTracker {
/// MouseMove message /// MouseMove message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseMove { pub struct ProtoMouseMove {
/// Fixed value "MouseMove" #[prost(int32, tag="1")]
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub y: i32, pub y: i32,
} }
/// MouseMoveAbs message /// MouseMoveAbs message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseMoveAbs { pub struct ProtoMouseMoveAbs {
/// Fixed value "MouseMoveAbs" #[prost(int32, tag="1")]
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub y: i32, pub y: i32,
} }
/// MouseWheel message /// MouseWheel message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseWheel { pub struct ProtoMouseWheel {
/// Fixed value "MouseWheel" #[prost(int32, tag="1")]
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub y: i32, pub y: i32,
} }
/// MouseKeyDown message /// MouseKeyDown message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyDown { pub struct ProtoMouseKeyDown {
/// Fixed value "MouseKeyDown" #[prost(int32, tag="1")]
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")]
pub key: i32, pub key: i32,
} }
/// MouseKeyUp message /// MouseKeyUp message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyUp { pub struct ProtoMouseKeyUp {
/// Fixed value "MouseKeyUp" #[prost(int32, tag="1")]
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")]
pub key: i32, pub key: i32,
} }
// Keyboard messages // Keyboard messages
/// KeyDown message /// KeyDown message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoKeyDown { pub struct ProtoKeyDown {
/// Fixed value "KeyDown" #[prost(int32, tag="1")]
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")]
pub key: i32, pub key: i32,
} }
/// KeyUp message /// KeyUp message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoKeyUp { pub struct ProtoKeyUp {
/// Fixed value "KeyUp" #[prost(int32, tag="1")]
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")]
pub key: i32, pub key: i32,
} }
// Controller messages // Controller messages
@@ -102,159 +81,167 @@ pub struct ProtoKeyUp {
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerAttach { 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" /// One of the following enums: "ps", "xbox" or "switch"
#[prost(string, tag="2")] #[prost(string, tag="1")]
pub id: ::prost::alloc::string::String, pub id: ::prost::alloc::string::String,
/// Slot number (0-3) /// Slot number (0-3)
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub slot: i32, pub slot: i32,
/// Session ID of the client attaching the controller
#[prost(string, tag="3")]
pub session_id: ::prost::alloc::string::String,
} }
/// ControllerDetach message /// ControllerDetach message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoControllerDetach { pub struct ProtoControllerDetach {
/// Fixed value "ControllerDetach"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3) /// Slot number (0-3)
#[prost(int32, tag="2")] #[prost(int32, tag="1")]
pub slot: i32, pub slot: i32,
} }
/// ControllerButton message /// ControllerButton message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoControllerButton { pub struct ProtoControllerButton {
/// Fixed value "ControllerButtons"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3) /// Slot number (0-3)
#[prost(int32, tag="2")] #[prost(int32, tag="1")]
pub slot: i32, pub slot: i32,
/// Button code (linux input event code) /// Button code (linux input event code)
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub button: i32, pub button: i32,
/// true if pressed, false if released /// true if pressed, false if released
#[prost(bool, tag="4")] #[prost(bool, tag="3")]
pub pressed: bool, pub pressed: bool,
} }
/// ControllerTriggers message /// ControllerTriggers message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoControllerTrigger { pub struct ProtoControllerTrigger {
/// Fixed value "ControllerTriggers"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3) /// Slot number (0-3)
#[prost(int32, tag="2")] #[prost(int32, tag="1")]
pub slot: i32, pub slot: i32,
/// Trigger number (0 for left, 1 for right) /// Trigger number (0 for left, 1 for right)
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub trigger: i32, pub trigger: i32,
/// trigger value (-32768 to 32767) /// trigger value (-32768 to 32767)
#[prost(int32, tag="4")] #[prost(int32, tag="3")]
pub value: i32, pub value: i32,
} }
/// ControllerSticks message /// ControllerSticks message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoControllerStick { pub struct ProtoControllerStick {
/// Fixed value "ControllerStick"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3) /// Slot number (0-3)
#[prost(int32, tag="2")] #[prost(int32, tag="1")]
pub slot: i32, pub slot: i32,
/// Stick number (0 for left, 1 for right) /// Stick number (0 for left, 1 for right)
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub stick: i32, pub stick: i32,
/// X axis value (-32768 to 32767) /// X axis value (-32768 to 32767)
#[prost(int32, tag="4")] #[prost(int32, tag="3")]
pub x: i32, pub x: i32,
/// Y axis value (-32768 to 32767) /// Y axis value (-32768 to 32767)
#[prost(int32, tag="5")] #[prost(int32, tag="4")]
pub y: i32, pub y: i32,
} }
/// ControllerAxis message /// ControllerAxis message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoControllerAxis { pub struct ProtoControllerAxis {
/// Fixed value "ControllerAxis"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3) /// Slot number (0-3)
#[prost(int32, tag="2")] #[prost(int32, tag="1")]
pub slot: i32, pub slot: i32,
/// Axis number (0 for d-pad horizontal, 1 for d-pad vertical) /// Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub axis: i32, pub axis: i32,
/// axis value (-1 to 1) /// axis value (-1 to 1)
#[prost(int32, tag="4")] #[prost(int32, tag="3")]
pub value: i32, pub value: i32,
} }
/// ControllerRumble message /// ControllerRumble message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoControllerRumble { pub struct ProtoControllerRumble {
/// Fixed value "ControllerRumble"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3) /// Slot number (0-3)
#[prost(int32, tag="2")] #[prost(int32, tag="1")]
pub slot: i32, pub slot: i32,
/// Low frequency rumble (0-65535) /// Low frequency rumble (0-65535)
#[prost(int32, tag="3")] #[prost(int32, tag="2")]
pub low_frequency: i32, pub low_frequency: i32,
/// High frequency rumble (0-65535) /// High frequency rumble (0-65535)
#[prost(int32, tag="4")] #[prost(int32, tag="3")]
pub high_frequency: i32, pub high_frequency: i32,
/// Duration in milliseconds /// Duration in milliseconds
#[prost(int32, tag="5")] #[prost(int32, tag="4")]
pub duration: i32, pub duration: i32,
} }
/// Union of all Input types // WebRTC + signaling
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoInput { pub struct RtcIceCandidateInit {
#[prost(oneof="proto_input::InputType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14")] #[prost(string, tag="1")]
pub input_type: ::core::option::Option<proto_input::InputType>, 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>,
} }
/// Nested message and enum types in `ProtoInput`. #[allow(clippy::derive_partial_eq_without_eq)]
pub mod proto_input { #[derive(Clone, PartialEq, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)] pub struct RtcSessionDescriptionInit {
#[derive(Clone, PartialEq, ::prost::Oneof)] #[prost(string, tag="1")]
pub enum InputType { pub sdp: ::prost::alloc::string::String,
#[prost(message, tag="1")] #[prost(string, tag="2")]
MouseMove(super::ProtoMouseMove), pub r#type: ::prost::alloc::string::String,
#[prost(message, tag="2")] }
MouseMoveAbs(super::ProtoMouseMoveAbs), /// ProtoICE message
#[prost(message, tag="3")] #[allow(clippy::derive_partial_eq_without_eq)]
MouseWheel(super::ProtoMouseWheel), #[derive(Clone, PartialEq, ::prost::Message)]
#[prost(message, tag="4")] pub struct ProtoIce {
MouseKeyDown(super::ProtoMouseKeyDown), #[prost(message, optional, tag="1")]
#[prost(message, tag="5")] pub candidate: ::core::option::Option<RtcIceCandidateInit>,
MouseKeyUp(super::ProtoMouseKeyUp), }
#[prost(message, tag="6")] /// ProtoSDP message
KeyDown(super::ProtoKeyDown), #[allow(clippy::derive_partial_eq_without_eq)]
#[prost(message, tag="7")] #[derive(Clone, PartialEq, ::prost::Message)]
KeyUp(super::ProtoKeyUp), pub struct ProtoSdp {
#[prost(message, tag="8")] #[prost(message, optional, tag="1")]
ControllerAttach(super::ProtoControllerAttach), pub sdp: ::core::option::Option<RtcSessionDescriptionInit>,
#[prost(message, tag="9")] }
ControllerDetach(super::ProtoControllerDetach), /// ProtoRaw message
#[prost(message, tag="10")] #[allow(clippy::derive_partial_eq_without_eq)]
ControllerButton(super::ProtoControllerButton), #[derive(Clone, PartialEq, ::prost::Message)]
#[prost(message, tag="11")] pub struct ProtoRaw {
ControllerTrigger(super::ProtoControllerTrigger), #[prost(string, tag="1")]
#[prost(message, tag="12")] pub data: ::prost::alloc::string::String,
ControllerStick(super::ProtoControllerStick), }
#[prost(message, tag="13")] /// ProtoClientRequestRoomStream message
ControllerAxis(super::ProtoControllerAxis), #[allow(clippy::derive_partial_eq_without_eq)]
#[prost(message, tag="14")] #[derive(Clone, PartialEq, ::prost::Message)]
ControllerRumble(super::ProtoControllerRumble), 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)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
@@ -266,10 +253,59 @@ pub struct ProtoMessageBase {
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessageInput { pub struct ProtoMessage {
#[prost(message, optional, tag="1")] #[prost(message, optional, tag="1")]
pub message_base: ::core::option::Option<ProtoMessageBase>, pub message_base: ::core::option::Option<ProtoMessageBase>,
#[prost(message, optional, tag="2")] #[prost(oneof="proto_message::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25")]
pub data: ::core::option::Option<ProtoInput>, 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),
#[prost(message, tag="9")]
ControllerAttach(super::ProtoControllerAttach),
#[prost(message, tag="10")]
ControllerDetach(super::ProtoControllerDetach),
#[prost(message, tag="11")]
ControllerButton(super::ProtoControllerButton),
#[prost(message, tag="12")]
ControllerTrigger(super::ProtoControllerTrigger),
#[prost(message, tag="13")]
ControllerStick(super::ProtoControllerStick),
#[prost(message, tag="14")]
ControllerAxis(super::ProtoControllerAxis),
#[prost(message, tag="15")]
ControllerRumble(super::ProtoControllerRumble),
/// 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) // @@protoc_insertion_point(module)

View File

@@ -12,7 +12,31 @@ message ProtoMessageBase {
ProtoLatencyTracker latency = 2; ProtoLatencyTracker latency = 2;
} }
message ProtoMessageInput { message ProtoMessage {
ProtoMessageBase message_base = 1; ProtoMessageBase message_base = 1;
ProtoInput data = 2; 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;
ProtoControllerAttach controller_attach = 9;
ProtoControllerDetach controller_detach = 10;
ProtoControllerButton controller_button = 11;
ProtoControllerTrigger controller_trigger = 12;
ProtoControllerStick controller_stick = 13;
ProtoControllerAxis controller_axis = 14;
ProtoControllerRumble controller_rumble = 15;
// 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;
}
} }

View File

@@ -8,124 +8,137 @@ package proto;
// MouseMove message // MouseMove message
message ProtoMouseMove { message ProtoMouseMove {
string type = 1; // Fixed value "MouseMove" int32 x = 1;
int32 x = 2; int32 y = 2;
int32 y = 3;
} }
// MouseMoveAbs message // MouseMoveAbs message
message ProtoMouseMoveAbs { message ProtoMouseMoveAbs {
string type = 1; // Fixed value "MouseMoveAbs" int32 x = 1;
int32 x = 2; int32 y = 2;
int32 y = 3;
} }
// MouseWheel message // MouseWheel message
message ProtoMouseWheel { message ProtoMouseWheel {
string type = 1; // Fixed value "MouseWheel" int32 x = 1;
int32 x = 2; int32 y = 2;
int32 y = 3;
} }
// MouseKeyDown message // MouseKeyDown message
message ProtoMouseKeyDown { message ProtoMouseKeyDown {
string type = 1; // Fixed value "MouseKeyDown" int32 key = 1;
int32 key = 2;
} }
// MouseKeyUp message // MouseKeyUp message
message ProtoMouseKeyUp { message ProtoMouseKeyUp {
string type = 1; // Fixed value "MouseKeyUp" int32 key = 1;
int32 key = 2;
} }
/* Keyboard messages */ /* Keyboard messages */
// KeyDown message // KeyDown message
message ProtoKeyDown { message ProtoKeyDown {
string type = 1; // Fixed value "KeyDown" int32 key = 1;
int32 key = 2;
} }
// KeyUp message // KeyUp message
message ProtoKeyUp { message ProtoKeyUp {
string type = 1; // Fixed value "KeyUp" int32 key = 1;
int32 key = 2;
} }
/* Controller messages */ /* Controller messages */
// ControllerAttach message // ControllerAttach message
message ProtoControllerAttach { message ProtoControllerAttach {
string type = 1; // Fixed value "ControllerAttach" string id = 1; // One of the following enums: "ps", "xbox" or "switch"
string id = 2; // One of the following enums: "ps", "xbox" or "switch" int32 slot = 2; // Slot number (0-3)
int32 slot = 3; // Slot number (0-3) string session_id = 3; // Session ID of the client attaching the controller
} }
// ControllerDetach message // ControllerDetach message
message ProtoControllerDetach { message ProtoControllerDetach {
string type = 1; // Fixed value "ControllerDetach" int32 slot = 1; // Slot number (0-3)
int32 slot = 2; // Slot number (0-3)
} }
// ControllerButton message // ControllerButton message
message ProtoControllerButton { message ProtoControllerButton {
string type = 1; // Fixed value "ControllerButtons" int32 slot = 1; // Slot number (0-3)
int32 slot = 2; // Slot number (0-3) int32 button = 2; // Button code (linux input event code)
int32 button = 3; // Button code (linux input event code) bool pressed = 3; // true if pressed, false if released
bool pressed = 4; // true if pressed, false if released
} }
// ControllerTriggers message // ControllerTriggers message
message ProtoControllerTrigger { message ProtoControllerTrigger {
string type = 1; // Fixed value "ControllerTriggers" int32 slot = 1; // Slot number (0-3)
int32 slot = 2; // Slot number (0-3) int32 trigger = 2; // Trigger number (0 for left, 1 for right)
int32 trigger = 3; // Trigger number (0 for left, 1 for right) int32 value = 3; // trigger value (-32768 to 32767)
int32 value = 4; // trigger value (-32768 to 32767)
} }
// ControllerSticks message // ControllerSticks message
message ProtoControllerStick { message ProtoControllerStick {
string type = 1; // Fixed value "ControllerStick" int32 slot = 1; // Slot number (0-3)
int32 slot = 2; // Slot number (0-3) int32 stick = 2; // Stick number (0 for left, 1 for right)
int32 stick = 3; // Stick number (0 for left, 1 for right) int32 x = 3; // X axis value (-32768 to 32767)
int32 x = 4; // X axis value (-32768 to 32767) int32 y = 4; // Y axis value (-32768 to 32767)
int32 y = 5; // Y axis value (-32768 to 32767)
} }
// ControllerAxis message // ControllerAxis message
message ProtoControllerAxis { message ProtoControllerAxis {
string type = 1; // Fixed value "ControllerAxis" int32 slot = 1; // Slot number (0-3)
int32 slot = 2; // Slot number (0-3) int32 axis = 2; // Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
int32 axis = 3; // Axis number (0 for d-pad horizontal, 1 for d-pad vertical) int32 value = 3; // axis value (-1 to 1)
int32 value = 4; // axis value (-1 to 1)
} }
// ControllerRumble message // ControllerRumble message
message ProtoControllerRumble { message ProtoControllerRumble {
string type = 1; // Fixed value "ControllerRumble" int32 slot = 1; // Slot number (0-3)
int32 slot = 2; // Slot number (0-3) int32 low_frequency = 2; // Low frequency rumble (0-65535)
int32 low_frequency = 3; // Low frequency rumble (0-65535) int32 high_frequency = 3; // High frequency rumble (0-65535)
int32 high_frequency = 4; // High frequency rumble (0-65535) int32 duration = 4; // Duration in milliseconds
int32 duration = 5; // Duration in milliseconds
} }
// Union of all Input types /* WebRTC + signaling */
message ProtoInput {
oneof input_type { message RTCIceCandidateInit {
ProtoMouseMove mouse_move = 1; string candidate = 1;
ProtoMouseMoveAbs mouse_move_abs = 2; optional uint32 sdpMLineIndex = 2;
ProtoMouseWheel mouse_wheel = 3; optional string sdpMid = 3;
ProtoMouseKeyDown mouse_key_down = 4; optional string usernameFragment = 4;
ProtoMouseKeyUp mouse_key_up = 5; }
ProtoKeyDown key_down = 6;
ProtoKeyUp key_up = 7; message RTCSessionDescriptionInit {
ProtoControllerAttach controller_attach = 8; string sdp = 1;
ProtoControllerDetach controller_detach = 9; string type = 2;
ProtoControllerButton controller_button = 10; }
ProtoControllerTrigger controller_trigger = 11;
ProtoControllerStick controller_stick = 12; // ProtoICE message
ProtoControllerAxis controller_axis = 13; message ProtoICE {
ProtoControllerRumble controller_rumble = 14; 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;
} }