Fixed multi-controllers, optimize and improve code in relay and nestri-server

This commit is contained in:
DatCaptainHorse
2025-10-25 03:57:26 +03:00
parent 67f9a7d0a0
commit a54cf759fa
27 changed files with 837 additions and 644 deletions

View File

@@ -41,7 +41,7 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm lib32-gcc-libs pacman -Sy --noconfirm lib32-gcc-libs
# Clone repository # Clone repository
RUN git clone --depth 1 --rev "9e8bfd0217eeab011c5afc368d3ea67a4c239e81" https://github.com/DatCaptainHorse/vimputti.git RUN git clone --depth 1 --rev "f2f21561ddcb814d74455311969d3e8934b052c6" https://github.com/DatCaptainHorse/vimputti.git
#-------------------------------------------------------------------- #--------------------------------------------------------------------
FROM vimputti-manager-deps AS vimputti-manager-planner FROM vimputti-manager-deps AS vimputti-manager-planner
@@ -129,23 +129,8 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
RUN --mount=type=cache,target=${CARGO_HOME}/registry \ RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo install cargo-c cargo install cargo-c
# Grab cudart from NVIDIA..
RUN wget https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-13.0.96-archive.tar.xz -O cuda_cudart.tar.xz && \
mkdir cuda_cudart && tar -xf cuda_cudart.tar.xz -C cuda_cudart --strip-components=1 && \
cp cuda_cudart/lib/libcudart.so cuda_cudart/lib/libcudart.so.* /usr/lib/ && \
rm -r cuda_cudart && \
rm cuda_cudart.tar.xz
# Grab cuda lib from NVIDIA (it's in driver package of all things..)
RUN wget https://developer.download.nvidia.com/compute/cuda/redist/nvidia_driver/linux-x86_64/nvidia_driver-linux-x86_64-580.95.05-archive.tar.xz -O nvidia_driver.tar.xz && \
mkdir nvidia_driver && tar -xf nvidia_driver.tar.xz -C nvidia_driver --strip-components=1 && \
cp nvidia_driver/lib/libcuda.so.* /usr/lib/libcuda.so && \
ln -s /usr/lib/libcuda.so /usr/lib/libcuda.so.1 && \
rm -r nvidia_driver && \
rm nvidia_driver.tar.xz
# Clone repository # Clone repository
RUN git clone --depth 1 --rev "afa853fa03e8403c83bbb3bc0cf39147ad46c266" https://github.com/games-on-whales/gst-wayland-display.git RUN git clone --depth 1 --rev "a4abcfe2cffe2d33b564d1308b58504a5e3012b1" https://github.com/games-on-whales/gst-wayland-display.git
#-------------------------------------------------------------------- #--------------------------------------------------------------------
FROM gst-wayland-deps AS gst-wayland-planner FROM gst-wayland-deps AS gst-wayland-planner
@@ -214,5 +199,4 @@ COPY --from=gst-wayland-cached-builder /artifacts/include/ /artifacts/include/
COPY --from=vimputti-manager-cached-builder /artifacts/vimputti-manager /artifacts/bin/ COPY --from=vimputti-manager-cached-builder /artifacts/vimputti-manager /artifacts/bin/
COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_64.so /artifacts/lib64/libvimputti_shim.so COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_64.so /artifacts/lib64/libvimputti_shim.so
COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_32.so /artifacts/lib32/libvimputti_shim.so COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_32.so /artifacts/lib32/libvimputti_shim.so
COPY --from=gst-wayland-deps /usr/lib/libcuda.so /usr/lib/libcuda.so.* /artifacts/lib/
COPY --from=bubblewrap-builder /artifacts/bin/bwrap /artifacts/bin/ COPY --from=bubblewrap-builder /artifacts/bin/bwrap /artifacts/bin/

View File

@@ -32,7 +32,6 @@ interface GamepadState {
export class Controller { export class Controller {
protected wrtc: WebRTCStream; protected wrtc: WebRTCStream;
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 = {
@@ -50,6 +49,14 @@ 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 isIdle: boolean = true;
private lastInputTime: number = Date.now();
private idleUpdateInterval: number = 150.0; // ~6-7 updates per second for keep-alive packets
private inputDetected: boolean = false;
private lastFullStateSend: number = Date.now();
private fullStateSendInterval: number = 500.0; // send full state every 0.5 seconds (helps packet loss)
private forceFullStateSend: boolean = false;
private _dcHandler: ((data: ArrayBuffer) => void) | null = null; private _dcHandler: ((data: ArrayBuffer) => void) | null = null;
constructor({ webrtc, e }: Props) { constructor({ webrtc, e }: Props) {
@@ -79,9 +86,8 @@ export class Controller {
const attachMsg = messageWrapper.payload.value; const attachMsg = messageWrapper.payload.value;
// Gamepad connected succesfully // Gamepad connected succesfully
this.gamepad = e.gamepad; this.gamepad = e.gamepad;
this.slotMap.set(e.gamepad.index, attachMsg.slot);
console.log( console.log(
`Gamepad connected: ${e.gamepad.id} assigned to slot ${attachMsg.slot} on server, local slot ${e.gamepad.index}`, `Gamepad connected: ${e.gamepad.id}, local slot ${e.gamepad.index}, msg: ${attachMsg.sessionSlot}`,
); );
} }
} catch (err) { } catch (err) {
@@ -93,6 +99,7 @@ export class Controller {
const attachMsg = createMessage( const attachMsg = createMessage(
create(ProtoControllerAttachSchema, { create(ProtoControllerAttachSchema, {
id: this.vendor_id_to_controller(vendorId, productId), id: this.vendor_id_to_controller(vendorId, productId),
sessionSlot: e.gamepad.index,
sessionId: this.wrtc.getSessionID(), sessionId: this.wrtc.getSessionID(),
}), }),
"controllerInput", "controllerInput",
@@ -102,6 +109,10 @@ export class Controller {
this.run(); this.run();
} }
public getSlot(): number {
return this.gamepad.index;
}
// Maps vendor id and product id to supported controller type // Maps vendor id and product id to supported controller type
// Currently supported: Sony (ps4, ps5), Microsoft (xbox360, xboxone), Nintendo (switchpro) // Currently supported: Sony (ps4, ps5), Microsoft (xbox360, xboxone), Nintendo (switchpro)
// Default fallback to xbox360 // Default fallback to xbox360
@@ -154,6 +165,13 @@ export class Controller {
private pollGamepad() { private pollGamepad() {
// Get updated gamepad state // Get updated gamepad state
const gamepads = navigator.getGamepads(); const gamepads = navigator.getGamepads();
// Periodically force send full state to clear stuck inputs
if (Date.now() - this.lastFullStateSend > this.fullStateSendInterval) {
this.forceFullStateSend = true;
this.lastFullStateSend = Date.now();
}
if (this.gamepad) { if (this.gamepad) {
if (gamepads[this.gamepad.index]) { if (gamepads[this.gamepad.index]) {
this.gamepad = gamepads[this.gamepad!.index]; this.gamepad = gamepads[this.gamepad!.index];
@@ -164,7 +182,7 @@ export class Controller {
// ignore trigger buttons (6-7) as we handle those as axis // ignore trigger buttons (6-7) as we handle those as axis
if (index === 6 || index === 7) return; if (index === 6 || index === 7) return;
// If state differs, send // If state differs, send
if (button.pressed !== this.lastState.buttonState.get(index)) { if (button.pressed !== this.lastState.buttonState.get(index) || this.forceFullStateSend) {
const linuxCode = this.controllerButtonToVirtualKeyCode(index); const linuxCode = this.controllerButtonToVirtualKeyCode(index);
if (linuxCode === undefined) { if (linuxCode === undefined) {
// Skip unmapped button index // Skip unmapped button index
@@ -174,13 +192,15 @@ export class Controller {
const buttonMessage = createMessage( const buttonMessage = createMessage(
create(ProtoControllerButtonSchema, { create(ProtoControllerButtonSchema, {
slot: this.getServerSlot(), sessionSlot: this.gamepad.index,
sessionId: this.wrtc.getSessionID(),
button: linuxCode, button: linuxCode,
pressed: button.pressed, pressed: button.pressed,
}), }),
"controllerInput", "controllerInput",
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, buttonMessage)); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, buttonMessage));
this.inputDetected = true;
// Store button state // Store button state
this.lastState.buttonState.set(index, button.pressed); this.lastState.buttonState.set(index, button.pressed);
} }
@@ -198,16 +218,18 @@ export class Controller {
), ),
); );
// If state differs, send // If state differs, send
if (leftTrigger !== this.lastState.leftTrigger) { if (leftTrigger !== this.lastState.leftTrigger || this.forceFullStateSend) {
const triggerMessage = createMessage( const triggerMessage = createMessage(
create(ProtoControllerTriggerSchema, { create(ProtoControllerTriggerSchema, {
slot: this.getServerSlot(), sessionSlot: this.gamepad.index,
sessionId: this.wrtc.getSessionID(),
trigger: 0, // 0 = left, 1 = right trigger: 0, // 0 = left, 1 = right
value: leftTrigger, value: leftTrigger,
}), }),
"controllerInput", "controllerInput",
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, triggerMessage)); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, triggerMessage));
this.inputDetected = true;
this.lastState.leftTrigger = leftTrigger; this.lastState.leftTrigger = leftTrigger;
} }
const rightTrigger = Math.round( const rightTrigger = Math.round(
@@ -220,16 +242,18 @@ export class Controller {
), ),
); );
// If state differs, send // If state differs, send
if (rightTrigger !== this.lastState.rightTrigger) { if (rightTrigger !== this.lastState.rightTrigger || this.forceFullStateSend) {
const triggerMessage = createMessage( const triggerMessage = createMessage(
create(ProtoControllerTriggerSchema, { create(ProtoControllerTriggerSchema, {
slot: this.getServerSlot(), sessionSlot: this.gamepad.index,
sessionId: this.wrtc.getSessionID(),
trigger: 1, // 0 = left, 1 = right trigger: 1, // 0 = left, 1 = right
value: rightTrigger, value: rightTrigger,
}), }),
"controllerInput", "controllerInput",
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, triggerMessage)); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, triggerMessage));
this.inputDetected = true;
this.lastState.rightTrigger = rightTrigger; this.lastState.rightTrigger = rightTrigger;
} }
@@ -238,32 +262,36 @@ export class Controller {
const dpadLeft = this.gamepad.buttons[14]?.pressed ? 1 : 0; const dpadLeft = this.gamepad.buttons[14]?.pressed ? 1 : 0;
const dpadRight = this.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 || this.forceFullStateSend) {
const dpadMessage = createMessage( const dpadMessage = createMessage(
create(ProtoControllerAxisSchema, { create(ProtoControllerAxisSchema, {
slot: this.getServerSlot(), sessionSlot: this.gamepad.index,
sessionId: this.wrtc.getSessionID(),
axis: 0, // 0 = dpadX, 1 = dpadY axis: 0, // 0 = dpadX, 1 = dpadY
value: dpadX, value: dpadX,
}), }),
"controllerInput", "controllerInput",
); );
this.lastState.dpadX = dpadX;
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, dpadMessage)); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, dpadMessage));
this.inputDetected = true;
this.lastState.dpadX = dpadX;
} }
const dpadUp = this.gamepad.buttons[12]?.pressed ? 1 : 0; const dpadUp = this.gamepad.buttons[12]?.pressed ? 1 : 0;
const dpadDown = this.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 || this.forceFullStateSend) {
const dpadMessage = createMessage( const dpadMessage = createMessage(
create(ProtoControllerAxisSchema, { create(ProtoControllerAxisSchema, {
slot: this.getServerSlot(), sessionSlot: this.gamepad.index,
sessionId: this.wrtc.getSessionID(),
axis: 1, // 0 = dpadX, 1 = dpadY axis: 1, // 0 = dpadX, 1 = dpadY
value: dpadY, value: dpadY,
}), }),
"controllerInput", "controllerInput",
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, dpadMessage)); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, dpadMessage));
this.inputDetected = true;
this.lastState.dpadY = dpadY; this.lastState.dpadY = dpadY;
} }
@@ -292,10 +320,12 @@ export class Controller {
// if moves inside deadzone, zero it if not inside deadzone last time // if moves inside deadzone, zero it if not inside deadzone last time
if ( if (
sendLeftX !== this.lastState.leftX || sendLeftX !== this.lastState.leftX ||
sendLeftY !== this.lastState.leftY sendLeftY !== this.lastState.leftY || this.forceFullStateSend
) { ) {
const stickMessage = createMessage( const stickMessage = createMessage(
create(ProtoControllerStickSchema, { create(ProtoControllerStickSchema, {
sessionSlot: this.gamepad.index,
sessionId: this.wrtc.getSessionID(),
stick: 0, // 0 = left, 1 = right stick: 0, // 0 = left, 1 = right
x: sendLeftX, x: sendLeftX,
y: sendLeftY, y: sendLeftY,
@@ -303,6 +333,7 @@ export class Controller {
"controllerInput", "controllerInput",
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, stickMessage)); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, stickMessage));
this.inputDetected = true;
this.lastState.leftX = sendLeftX; this.lastState.leftX = sendLeftX;
this.lastState.leftY = sendLeftY; this.lastState.leftY = sendLeftY;
} }
@@ -328,10 +359,12 @@ export class Controller {
Math.abs(rightY) > this.stickDeadzone ? Math.round(rightY) : 0; Math.abs(rightY) > this.stickDeadzone ? Math.round(rightY) : 0;
if ( if (
sendRightX !== this.lastState.rightX || sendRightX !== this.lastState.rightX ||
sendRightY !== this.lastState.rightY sendRightY !== this.lastState.rightY || this.forceFullStateSend
) { ) {
const stickMessage = createMessage( const stickMessage = createMessage(
create(ProtoControllerStickSchema, { create(ProtoControllerStickSchema, {
sessionSlot: this.gamepad.index,
sessionId: this.wrtc.getSessionID(),
stick: 1, // 0 = left, 1 = right stick: 1, // 0 = left, 1 = right
x: sendRightX, x: sendRightX,
y: sendRightY, y: sendRightY,
@@ -339,11 +372,14 @@ export class Controller {
"controllerInput", "controllerInput",
); );
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, stickMessage)); this.wrtc.sendBinary(toBinary(ProtoMessageSchema, stickMessage));
this.inputDetected = true;
this.lastState.rightX = sendRightX; this.lastState.rightX = sendRightX;
this.lastState.rightY = sendRightY; this.lastState.rightY = sendRightY;
} }
} }
} }
this.forceFullStateSend = false;
} }
private loopInterval: any = null; private loopInterval: any = null;
@@ -352,10 +388,34 @@ export class Controller {
if (this.connected) this.stop(); if (this.connected) this.stop();
this.connected = true; this.connected = true;
// Poll gamepads in setInterval loop this.isIdle = true;
this.lastInputTime = Date.now();
this.loopInterval = setInterval(() => { this.loopInterval = setInterval(() => {
if (this.connected) this.pollGamepad(); if (this.connected) {
}, this.updateInterval); this.inputDetected = false; // Reset before poll
this.pollGamepad();
// Switch polling rate based on input
if (this.inputDetected) {
this.lastInputTime = Date.now();
if (this.isIdle) {
this.isIdle = false;
clearInterval(this.loopInterval);
this.loopInterval = setInterval(() => {
if (this.connected) this.pollGamepad();
}, this.updateInterval);
}
} else if (!this.isIdle && Date.now() - this.lastInputTime > 200) {
// Switch to idle polling after 200ms of no input
this.isIdle = true;
clearInterval(this.loopInterval);
this.loopInterval = setInterval(() => {
if (this.connected) this.pollGamepad();
}, this.idleUpdateInterval);
}
}
}, this.isIdle ? this.idleUpdateInterval : this.updateInterval);
} }
public stop() { public stop() {
@@ -366,21 +426,6 @@ export class Controller {
this.connected = false; this.connected = false;
} }
public getLocalSlot(): number {
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
@@ -391,7 +436,7 @@ export class Controller {
// Gamepad disconnected // Gamepad disconnected
const detachMsg = createMessage( const detachMsg = createMessage(
create(ProtoControllerDetachSchema, { create(ProtoControllerDetachSchema, {
slot: this.getServerSlot(), sessionSlot: this.gamepad.index,
}), }),
"controllerInput", "controllerInput",
); );
@@ -407,7 +452,9 @@ export class Controller {
if (!this.connected) return; if (!this.connected) return;
// Check if aimed at this controller slot // Check if aimed at this controller slot
if (rumbleMsg.slot !== this.getServerSlot()) return; if (rumbleMsg.sessionId !== this.wrtc.getSessionID() &&
rumbleMsg.sessionSlot !== this.gamepad.index)
return;
// Trigger actual rumble // Trigger actual rumble
// Need to remap from 0-65535 to 0.0-1.0 ranges // Need to remap from 0-65535 to 0.0-1.0 ranges

View File

@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated from file latency_tracker.proto (package proto, syntax proto3) // @generated from file latency_tracker.proto (package proto, syntax proto3)
/* eslint-disable */ /* eslint-disable */

View File

@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated from file messages.proto (package proto, syntax proto3) // @generated from file messages.proto (package proto, syntax proto3)
/* eslint-disable */ /* eslint-disable */

View File

@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated from file types.proto (package proto, syntax proto3) // @generated from file types.proto (package proto, syntax proto3)
/* eslint-disable */ /* eslint-disable */
@@ -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("Cgt0eXBlcy5wcm90bxIFcHJvdG8iJgoOUHJvdG9Nb3VzZU1vdmUSCQoBeBgBIAEoBRIJCgF5GAIgASgFIikKEVByb3RvTW91c2VNb3ZlQWJzEgkKAXgYASABKAUSCQoBeRgCIAEoBSInCg9Qcm90b01vdXNlV2hlZWwSCQoBeBgBIAEoBRIJCgF5GAIgASgFIiAKEVByb3RvTW91c2VLZXlEb3duEgsKA2tleRgBIAEoBSIeCg9Qcm90b01vdXNlS2V5VXASCwoDa2V5GAEgASgFIhsKDFByb3RvS2V5RG93bhILCgNrZXkYASABKAUiGQoKUHJvdG9LZXlVcBILCgNrZXkYASABKAUiRQoVUHJvdG9Db250cm9sbGVyQXR0YWNoEgoKAmlkGAEgASgJEgwKBHNsb3QYAiABKAUSEgoKc2Vzc2lvbl9pZBgDIAEoCSIlChVQcm90b0NvbnRyb2xsZXJEZXRhY2gSDAoEc2xvdBgBIAEoBSJGChVQcm90b0NvbnRyb2xsZXJCdXR0b24SDAoEc2xvdBgBIAEoBRIOCgZidXR0b24YAiABKAUSDwoHcHJlc3NlZBgDIAEoCCJGChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHNsb3QYASABKAUSDwoHdHJpZ2dlchgCIAEoBRINCgV2YWx1ZRgDIAEoBSJJChRQcm90b0NvbnRyb2xsZXJTdGljaxIMCgRzbG90GAEgASgFEg0KBXN0aWNrGAIgASgFEgkKAXgYAyABKAUSCQoBeRgEIAEoBSJAChNQcm90b0NvbnRyb2xsZXJBeGlzEgwKBHNsb3QYASABKAUSDAoEYXhpcxgCIAEoBRINCgV2YWx1ZRgDIAEoBSJmChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEc2xvdBgBIAEoBRIVCg1sb3dfZnJlcXVlbmN5GAIgASgFEhYKDmhpZ2hfZnJlcXVlbmN5GAMgASgFEhAKCGR1cmF0aW9uGAQgASgFIqoBChNSVENJY2VDYW5kaWRhdGVJbml0EhEKCWNhbmRpZGF0ZRgBIAEoCRIaCg1zZHBNTGluZUluZGV4GAIgASgNSACIAQESEwoGc2RwTWlkGAMgASgJSAGIAQESHQoQdXNlcm5hbWVGcmFnbWVudBgEIAEoCUgCiAEBQhAKDl9zZHBNTGluZUluZGV4QgkKB19zZHBNaWRCEwoRX3VzZXJuYW1lRnJhZ21lbnQiNgoZUlRDU2Vzc2lvbkRlc2NyaXB0aW9uSW5pdBILCgNzZHAYASABKAkSDAoEdHlwZRgCIAEoCSI5CghQcm90b0lDRRItCgljYW5kaWRhdGUYASABKAsyGi5wcm90by5SVENJY2VDYW5kaWRhdGVJbml0IjkKCFByb3RvU0RQEi0KA3NkcBgBIAEoCzIgLnByb3RvLlJUQ1Nlc3Npb25EZXNjcmlwdGlvbkluaXQiGAoIUHJvdG9SYXcSDAoEZGF0YRgBIAEoCSJFChxQcm90b0NsaWVudFJlcXVlc3RSb29tU3RyZWFtEhEKCXJvb21fbmFtZRgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJIkcKF1Byb3RvQ2xpZW50RGlzY29ubmVjdGVkEhIKCnNlc3Npb25faWQYASABKAkSGAoQY29udHJvbGxlcl9zbG90cxgCIAMoBSIqChVQcm90b1NlcnZlclB1c2hTdHJlYW0SEQoJcm9vbV9uYW1lGAEgASgJQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM"); fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iJgoOUHJvdG9Nb3VzZU1vdmUSCQoBeBgBIAEoBRIJCgF5GAIgASgFIikKEVByb3RvTW91c2VNb3ZlQWJzEgkKAXgYASABKAUSCQoBeRgCIAEoBSInCg9Qcm90b01vdXNlV2hlZWwSCQoBeBgBIAEoBRIJCgF5GAIgASgFIiAKEVByb3RvTW91c2VLZXlEb3duEgsKA2tleRgBIAEoBSIeCg9Qcm90b01vdXNlS2V5VXASCwoDa2V5GAEgASgFIhsKDFByb3RvS2V5RG93bhILCgNrZXkYASABKAUiGQoKUHJvdG9LZXlVcBILCgNrZXkYASABKAUiTQoVUHJvdG9Db250cm9sbGVyQXR0YWNoEgoKAmlkGAEgASgJEhQKDHNlc3Npb25fc2xvdBgCIAEoBRISCgpzZXNzaW9uX2lkGAMgASgJIkEKFVByb3RvQ29udHJvbGxlckRldGFjaBIUCgxzZXNzaW9uX3Nsb3QYASABKAUSEgoKc2Vzc2lvbl9pZBgCIAEoCSJiChVQcm90b0NvbnRyb2xsZXJCdXR0b24SFAoMc2Vzc2lvbl9zbG90GAEgASgFEhIKCnNlc3Npb25faWQYAiABKAkSDgoGYnV0dG9uGAMgASgFEg8KB3ByZXNzZWQYBCABKAgiYgoWUHJvdG9Db250cm9sbGVyVHJpZ2dlchIUCgxzZXNzaW9uX3Nsb3QYASABKAUSEgoKc2Vzc2lvbl9pZBgCIAEoCRIPCgd0cmlnZ2VyGAMgASgFEg0KBXZhbHVlGAQgASgFImUKFFByb3RvQ29udHJvbGxlclN0aWNrEhQKDHNlc3Npb25fc2xvdBgBIAEoBRISCgpzZXNzaW9uX2lkGAIgASgJEg0KBXN0aWNrGAMgASgFEgkKAXgYBCABKAUSCQoBeRgFIAEoBSJcChNQcm90b0NvbnRyb2xsZXJBeGlzEhQKDHNlc3Npb25fc2xvdBgBIAEoBRISCgpzZXNzaW9uX2lkGAIgASgJEgwKBGF4aXMYAyABKAUSDQoFdmFsdWUYBCABKAUiggEKFVByb3RvQ29udHJvbGxlclJ1bWJsZRIUCgxzZXNzaW9uX3Nsb3QYASABKAUSEgoKc2Vzc2lvbl9pZBgCIAEoCRIVCg1sb3dfZnJlcXVlbmN5GAMgASgFEhYKDmhpZ2hfZnJlcXVlbmN5GAQgASgFEhAKCGR1cmF0aW9uGAUgASgFIqoBChNSVENJY2VDYW5kaWRhdGVJbml0EhEKCWNhbmRpZGF0ZRgBIAEoCRIaCg1zZHBNTGluZUluZGV4GAIgASgNSACIAQESEwoGc2RwTWlkGAMgASgJSAGIAQESHQoQdXNlcm5hbWVGcmFnbWVudBgEIAEoCUgCiAEBQhAKDl9zZHBNTGluZUluZGV4QgkKB19zZHBNaWRCEwoRX3VzZXJuYW1lRnJhZ21lbnQiNgoZUlRDU2Vzc2lvbkRlc2NyaXB0aW9uSW5pdBILCgNzZHAYASABKAkSDAoEdHlwZRgCIAEoCSI5CghQcm90b0lDRRItCgljYW5kaWRhdGUYASABKAsyGi5wcm90by5SVENJY2VDYW5kaWRhdGVJbml0IjkKCFByb3RvU0RQEi0KA3NkcBgBIAEoCzIgLnByb3RvLlJUQ1Nlc3Npb25EZXNjcmlwdGlvbkluaXQiGAoIUHJvdG9SYXcSDAoEZGF0YRgBIAEoCSJFChxQcm90b0NsaWVudFJlcXVlc3RSb29tU3RyZWFtEhEKCXJvb21fbmFtZRgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJIkcKF1Byb3RvQ2xpZW50RGlzY29ubmVjdGVkEhIKCnNlc3Npb25faWQYASABKAkSGAoQY29udHJvbGxlcl9zbG90cxgCIAMoBSIqChVQcm90b1NlcnZlclB1c2hTdHJlYW0SEQoJcm9vbV9uYW1lGAEgASgJQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM");
/** /**
* MouseMove message * MouseMove message
@@ -174,14 +174,14 @@ export type ProtoControllerAttach = Message<"proto.ProtoControllerAttach"> & {
id: string; id: string;
/** /**
* Slot number (0-3) * Session specific slot number (0-3)
* *
* @generated from field: int32 slot = 2; * @generated from field: int32 session_slot = 2;
*/ */
slot: number; sessionSlot: number;
/** /**
* Session ID of the client attaching the controller * Session ID of the client
* *
* @generated from field: string session_id = 3; * @generated from field: string session_id = 3;
*/ */
@@ -202,11 +202,18 @@ export const ProtoControllerAttachSchema: GenMessage<ProtoControllerAttach> = /*
*/ */
export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & { export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & {
/** /**
* Slot number (0-3) * Session specific slot number (0-3)
* *
* @generated from field: int32 slot = 1; * @generated from field: int32 session_slot = 1;
*/ */
slot: number; sessionSlot: number;
/**
* Session ID of the client
*
* @generated from field: string session_id = 2;
*/
sessionId: string;
}; };
/** /**
@@ -223,23 +230,30 @@ export const ProtoControllerDetachSchema: GenMessage<ProtoControllerDetach> = /*
*/ */
export type ProtoControllerButton = Message<"proto.ProtoControllerButton"> & { export type ProtoControllerButton = Message<"proto.ProtoControllerButton"> & {
/** /**
* Slot number (0-3) * Session specific slot number (0-3)
* *
* @generated from field: int32 slot = 1; * @generated from field: int32 session_slot = 1;
*/ */
slot: number; sessionSlot: number;
/**
* Session ID of the client
*
* @generated from field: string session_id = 2;
*/
sessionId: string;
/** /**
* Button code (linux input event code) * Button code (linux input event code)
* *
* @generated from field: int32 button = 2; * @generated from field: int32 button = 3;
*/ */
button: number; button: number;
/** /**
* true if pressed, false if released * true if pressed, false if released
* *
* @generated from field: bool pressed = 3; * @generated from field: bool pressed = 4;
*/ */
pressed: boolean; pressed: boolean;
}; };
@@ -258,23 +272,30 @@ export const ProtoControllerButtonSchema: GenMessage<ProtoControllerButton> = /*
*/ */
export type ProtoControllerTrigger = Message<"proto.ProtoControllerTrigger"> & { export type ProtoControllerTrigger = Message<"proto.ProtoControllerTrigger"> & {
/** /**
* Slot number (0-3) * Session specific slot number (0-3)
* *
* @generated from field: int32 slot = 1; * @generated from field: int32 session_slot = 1;
*/ */
slot: number; sessionSlot: number;
/**
* Session ID of the client
*
* @generated from field: string session_id = 2;
*/
sessionId: string;
/** /**
* Trigger number (0 for left, 1 for right) * Trigger number (0 for left, 1 for right)
* *
* @generated from field: int32 trigger = 2; * @generated from field: int32 trigger = 3;
*/ */
trigger: number; trigger: number;
/** /**
* trigger value (-32768 to 32767) * trigger value (-32768 to 32767)
* *
* @generated from field: int32 value = 3; * @generated from field: int32 value = 4;
*/ */
value: number; value: number;
}; };
@@ -293,30 +314,37 @@ export const ProtoControllerTriggerSchema: GenMessage<ProtoControllerTrigger> =
*/ */
export type ProtoControllerStick = Message<"proto.ProtoControllerStick"> & { export type ProtoControllerStick = Message<"proto.ProtoControllerStick"> & {
/** /**
* Slot number (0-3) * Session specific slot number (0-3)
* *
* @generated from field: int32 slot = 1; * @generated from field: int32 session_slot = 1;
*/ */
slot: number; sessionSlot: number;
/**
* Session ID of the client
*
* @generated from field: string session_id = 2;
*/
sessionId: string;
/** /**
* Stick number (0 for left, 1 for right) * Stick number (0 for left, 1 for right)
* *
* @generated from field: int32 stick = 2; * @generated from field: int32 stick = 3;
*/ */
stick: number; stick: number;
/** /**
* X axis value (-32768 to 32767) * X axis value (-32768 to 32767)
* *
* @generated from field: int32 x = 3; * @generated from field: int32 x = 4;
*/ */
x: number; x: number;
/** /**
* Y axis value (-32768 to 32767) * Y axis value (-32768 to 32767)
* *
* @generated from field: int32 y = 4; * @generated from field: int32 y = 5;
*/ */
y: number; y: number;
}; };
@@ -335,23 +363,30 @@ export const ProtoControllerStickSchema: GenMessage<ProtoControllerStick> = /*@_
*/ */
export type ProtoControllerAxis = Message<"proto.ProtoControllerAxis"> & { export type ProtoControllerAxis = Message<"proto.ProtoControllerAxis"> & {
/** /**
* Slot number (0-3) * Session specific slot number (0-3)
* *
* @generated from field: int32 slot = 1; * @generated from field: int32 session_slot = 1;
*/ */
slot: number; sessionSlot: number;
/**
* Session ID of the client
*
* @generated from field: string session_id = 2;
*/
sessionId: string;
/** /**
* 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 = 2; * @generated from field: int32 axis = 3;
*/ */
axis: number; axis: number;
/** /**
* axis value (-1 to 1) * axis value (-1 to 1)
* *
* @generated from field: int32 value = 3; * @generated from field: int32 value = 4;
*/ */
value: number; value: number;
}; };
@@ -370,30 +405,37 @@ export const ProtoControllerAxisSchema: GenMessage<ProtoControllerAxis> = /*@__P
*/ */
export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & { export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & {
/** /**
* Slot number (0-3) * Session specific slot number (0-3)
* *
* @generated from field: int32 slot = 1; * @generated from field: int32 session_slot = 1;
*/ */
slot: number; sessionSlot: number;
/**
* Session ID of the client
*
* @generated from field: string session_id = 2;
*/
sessionId: string;
/** /**
* Low frequency rumble (0-65535) * Low frequency rumble (0-65535)
* *
* @generated from field: int32 low_frequency = 2; * @generated from field: int32 low_frequency = 3;
*/ */
lowFrequency: number; lowFrequency: number;
/** /**
* High frequency rumble (0-65535) * High frequency rumble (0-65535)
* *
* @generated from field: int32 high_frequency = 3; * @generated from field: int32 high_frequency = 4;
*/ */
highFrequency: number; highFrequency: number;
/** /**
* Duration in milliseconds * Duration in milliseconds
* *
* @generated from field: int32 duration = 4; * @generated from field: int32 duration = 5;
*/ */
duration: number; duration: number;
}; };

View File

@@ -22,6 +22,7 @@ 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 _sessionId: string | null = null;
private _p2p: Libp2p | undefined = undefined; private _p2p: Libp2p | undefined = undefined;
private _p2pConn: Connection | undefined = undefined; private _p2pConn: Connection | undefined = undefined;
private _msgStream: P2PMessageStream | undefined = undefined; private _msgStream: P2PMessageStream | undefined = undefined;
@@ -128,9 +129,9 @@ export class WebRTCStream {
}); });
this._msgStream.on("session-assigned", (data: ProtoClientRequestRoomStream) => { this._msgStream.on("session-assigned", (data: ProtoClientRequestRoomStream) => {
const sessionId = data.sessionId; this._sessionId = data.sessionId;
localStorage.setItem("nestri-session-id", sessionId); localStorage.setItem("nestri-session-id", this._sessionId);
console.log("Session ID assigned:", sessionId, "for room:", data.roomName); console.log("Session ID assigned:", this._sessionId, "for room:", data.roomName);
}); });
this._msgStream.on("offer", async (data: ProtoSDP) => { this._msgStream.on("offer", async (data: ProtoSDP) => {
@@ -162,7 +163,7 @@ export class WebRTCStream {
this._onConnected?.(null); this._onConnected?.(null);
}); });
const clientId = localStorage.getItem("nestri-session-id"); const clientId = this.getSessionID();
if (clientId) { if (clientId) {
console.debug("Using existing session ID:", clientId); console.debug("Using existing session ID:", clientId);
} }
@@ -180,8 +181,10 @@ export class WebRTCStream {
} }
} }
public getSessionID(): string { public getSessionID(): string | null {
return localStorage.getItem("nestri-session-id") || ""; if (this._sessionId === null)
this._sessionId = localStorage.getItem("nestri-session-id");
return this._sessionId;
} }
// Forces opus to stereo in Chromium browsers, because of course // Forces opus to stereo in Chromium browsers, because of course
@@ -298,7 +301,7 @@ export class WebRTCStream {
// @ts-ignore // @ts-ignore
receiver.jitterBufferTarget = receiver.jitterBufferDelayHint = receiver.playoutDelayHint = 0; receiver.jitterBufferTarget = receiver.jitterBufferDelayHint = receiver.playoutDelayHint = 0;
} }
}, 15); }, 50);
}); });
} }
} }

View File

@@ -90,11 +90,7 @@ if (envs_map.size > 0) {
let nestriControllers: Controller[] = []; let nestriControllers: Controller[] = [];
window.addEventListener("gamepadconnected", (e) => { window.addEventListener("gamepadconnected", (e) => {
// Ignore gamepads with id including "nestri"
console.log("Gamepad connected:", e.gamepad); console.log("Gamepad connected:", e.gamepad);
if (e.gamepad.id.toLowerCase().includes("nestri"))
return;
const controller = new Controller({ const controller = new Controller({
webrtc: stream, webrtc: stream,
e: e, e: e,
@@ -106,7 +102,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.getLocalSlot() === e.gamepad.index); let disconnected = nestriControllers.find((c) => c.getSlot() === 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

@@ -26,7 +26,7 @@ func InitWebRTCAPI() error {
mediaEngine := &webrtc.MediaEngine{} mediaEngine := &webrtc.MediaEngine{}
// Register our extensions // Register our extensions
if err := RegisterExtensions(mediaEngine); err != nil { if err = RegisterExtensions(mediaEngine); err != nil {
return fmt.Errorf("failed to register extensions: %w", err) return fmt.Errorf("failed to register extensions: %w", err)
} }

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"math"
"relay/internal/common" "relay/internal/common"
"relay/internal/connections" "relay/internal/connections"
"relay/internal/shared" "relay/internal/shared"
@@ -176,7 +177,7 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
// Create participant for this viewer // Create participant for this viewer
participant, err := shared.NewParticipant( participant, err := shared.NewParticipant(
"", // session ID will be set if this is a client session "",
stream.Conn().RemotePeer(), stream.Conn().RemotePeer(),
) )
if err != nil { if err != nil {
@@ -189,102 +190,37 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
participant.SessionID = session.SessionID participant.SessionID = session.SessionID
} }
// Assign peer connection
participant.PeerConnection = pc participant.PeerConnection = pc
// Create per-participant tracks // Add audio/video tracks
if room.VideoTrack != nil { {
participant.VideoTrack, err = webrtc.NewTrackLocalStaticRTP( localTrack, err := webrtc.NewTrackLocalStaticRTP(
room.VideoTrack.Codec(), room.AudioCodec,
"video-"+participant.ID.String(), "participant-"+participant.ID.String(),
"nestri-"+reqMsg.RoomName+"-video", "participant-"+participant.ID.String()+"-audio",
) )
if err != nil { if err != nil {
slog.Error("Failed to create participant video track", "room", reqMsg.RoomName, "err", err) slog.Error("Failed to create track for stream request", "err", err)
continue return
} }
participant.SetTrack(webrtc.RTPCodecTypeAudio, localTrack)
rtpSender, err := pc.AddTrack(participant.VideoTrack) slog.Debug("Set audio track for requested stream", "room", room.Name)
if err != nil {
slog.Error("Failed to add participant video track", "room", reqMsg.RoomName, "err", err)
continue
}
slog.Info("Added video track for participant",
"room", reqMsg.RoomName,
"participant", participant.ID,
"sender_id", fmt.Sprintf("%p", rtpSender))
// Relay packets from channel to track (VIDEO)
go func() {
for pkt := range participant.VideoChan {
// Use a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
done := make(chan error, 1)
go func() {
done <- participant.VideoTrack.WriteRTP(pkt)
}()
select {
case err := <-done:
cancel()
if err != nil {
if !errors.Is(err, io.ErrClosedPipe) {
slog.Debug("Failed to write video", "room", reqMsg.RoomName, "err", err)
}
return
}
case <-ctx.Done():
cancel()
slog.Error("WriteRTP BLOCKED for >100ms!",
"participant", participant.ID,
"room", reqMsg.RoomName)
// Don't return, continue processing
}
}
}()
} }
if room.AudioTrack != nil { {
participant.AudioTrack, err = webrtc.NewTrackLocalStaticRTP( localTrack, err := webrtc.NewTrackLocalStaticRTP(
room.AudioTrack.Codec(), room.VideoCodec,
"audio-"+participant.ID.String(), "participant-"+participant.ID.String(),
"nestri-"+reqMsg.RoomName+"-audio", "participant-"+participant.ID.String()+"-video",
) )
if err != nil { if err != nil {
slog.Error("Failed to create participant audio track", "room", reqMsg.RoomName, "err", err) slog.Error("Failed to create track for stream request", "err", err)
continue return
} }
participant.SetTrack(webrtc.RTPCodecTypeVideo, localTrack)
_, err := pc.AddTrack(participant.AudioTrack) slog.Debug("Set video track for requested stream", "room", room.Name)
if err != nil {
slog.Error("Failed to add participant audio track", "room", reqMsg.RoomName, "err", err)
continue
}
// Relay packets from channel to track (AUDIO)
go func() {
for pkt := range participant.AudioChan {
start := time.Now()
if err := participant.AudioTrack.WriteRTP(pkt); err != nil {
if !errors.Is(err, io.ErrClosedPipe) {
slog.Debug("Failed to write audio to participant", "room", reqMsg.RoomName, "err", err)
}
return
}
duration := time.Since(start)
if duration > 50*time.Millisecond {
slog.Warn("Slow audio WriteRTP detected",
"duration", duration,
"participant", participant.ID,
"room", reqMsg.RoomName)
}
}
}()
} }
// Add participant to room
room.AddParticipant(participant)
// Cleanup on disconnect // Cleanup on disconnect
cleanupParticipantID := participant.ID cleanupParticipantID := participant.ID
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
@@ -294,6 +230,9 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
slog.Info("Participant disconnected from room", "room", reqMsg.RoomName, "participant", cleanupParticipantID) slog.Info("Participant disconnected from room", "room", reqMsg.RoomName, "participant", cleanupParticipantID)
room.RemoveParticipantByID(cleanupParticipantID) room.RemoveParticipantByID(cleanupParticipantID)
participant.Close() participant.Close()
} else if state == webrtc.PeerConnectionStateConnected {
// Add participant to room when connection is established
room.AddParticipant(participant)
} }
}) })
@@ -334,33 +273,33 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
peerID := stream.Conn().RemotePeer() peerID := stream.Conn().RemotePeer()
// Check if it's a controller attach with assigned slot // Check if it's a controller attach with assigned slot
if attach := msgWrapper.GetControllerAttach(); attach != nil && attach.Slot >= 0 { if attach := msgWrapper.GetControllerAttach(); attach != nil && attach.SessionSlot >= 0 {
if session, ok := sp.relay.ClientSessions.Get(peerID); ok { if session, ok := sp.relay.ClientSessions.Get(peerID); ok {
// Check if slot already tracked // Check if slot already tracked
hasSlot := false hasSlot := false
for _, slot := range session.ControllerSlots { for _, slot := range session.ControllerSlots {
if slot == attach.Slot { if slot == attach.SessionSlot {
hasSlot = true hasSlot = true
break break
} }
} }
if !hasSlot { if !hasSlot {
session.ControllerSlots = append(session.ControllerSlots, attach.Slot) session.ControllerSlots = append(session.ControllerSlots, attach.SessionSlot)
session.LastActivity = time.Now() session.LastActivity = time.Now()
slog.Info("Controller slot assigned to client session", slog.Info("Controller slot assigned to client session",
"session", session.SessionID, "session", session.SessionID,
"slot", attach.Slot, "slot", attach.SessionSlot,
"total_slots", len(session.ControllerSlots)) "total_slots", len(session.ControllerSlots))
} }
} }
} }
// Check if it's a controller detach // Check if it's a controller detach
if detach := msgWrapper.GetControllerDetach(); detach != nil && detach.Slot >= 0 { if detach := msgWrapper.GetControllerDetach(); detach != nil && detach.SessionSlot >= 0 {
if session, ok := sp.relay.ClientSessions.Get(peerID); ok { if session, ok := sp.relay.ClientSessions.Get(peerID); ok {
newSlots := make([]int32, 0, len(session.ControllerSlots)) newSlots := make([]int32, 0, len(session.ControllerSlots))
for _, slot := range session.ControllerSlots { for _, slot := range session.ControllerSlots {
if slot != detach.Slot { if slot != detach.SessionSlot {
newSlots = append(newSlots, slot) newSlots = append(newSlots, slot)
} }
} }
@@ -368,7 +307,7 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
session.LastActivity = time.Now() session.LastActivity = time.Now()
slog.Info("Controller slot removed from client session", slog.Info("Controller slot removed from client session",
"session", session.SessionID, "session", session.SessionID,
"slot", detach.Slot, "slot", detach.SessionSlot,
"remaining_slots", len(session.ControllerSlots)) "remaining_slots", len(session.ControllerSlots))
} }
} }
@@ -537,19 +476,25 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
if err != nil { if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, network.ErrReset) { if errors.Is(err, io.EOF) || errors.Is(err, network.ErrReset) {
slog.Debug("Stream push connection closed by peer", "peer", stream.Conn().RemotePeer(), "error", err) slog.Debug("Stream push connection closed by peer", "peer", stream.Conn().RemotePeer(), "error", err)
if room != nil {
room.Close()
sp.incomingConns.Set(room.Name, nil)
}
return return
} }
slog.Error("Failed to receive data for stream push", "err", err) slog.Error("Failed to receive data for stream push", "err", err)
_ = stream.Reset() _ = stream.Reset()
if room != nil {
room.Close()
sp.incomingConns.Set(room.Name, nil)
}
return return
} }
if msgWrapper.MessageBase == nil { if msgWrapper.MessageBase == nil {
slog.Error("No MessageBase in stream push") slog.Error("No MessageBase in stream push")
_ = stream.Reset() continue
return
} }
switch msgWrapper.MessageBase.PayloadType { switch msgWrapper.MessageBase.PayloadType {
@@ -606,7 +551,7 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
slog.Error("Failed to add ICE candidate for pushed stream", "err", err) slog.Error("Failed to add ICE candidate for pushed stream", "err", err)
} }
for _, heldIce := range iceHolder { for _, heldIce := range iceHolder {
if err := conn.pc.AddICECandidate(heldIce); err != nil { if err = conn.pc.AddICECandidate(heldIce); err != nil {
slog.Error("Failed to add held ICE candidate for pushed stream", "err", err) slog.Error("Failed to add held ICE candidate for pushed stream", "err", err)
} }
} }
@@ -645,6 +590,9 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
continue continue
} }
// Assign room peer connection
room.PeerConnection = pc
pc.OnDataChannel(func(dc *webrtc.DataChannel) { pc.OnDataChannel(func(dc *webrtc.DataChannel) {
// TODO: Is this the best way to handle DataChannel? Should we just use the map directly? // TODO: Is this the best way to handle DataChannel? Should we just use the map directly?
room.DataChannel = connections.NewNestriDataChannel(dc) room.DataChannel = connections.NewNestriDataChannel(dc)
@@ -708,17 +656,6 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
}) })
pc.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { pc.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
localTrack, err := webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, remoteTrack.Kind().String(), fmt.Sprintf("nestri-%s-%s", room.Name, remoteTrack.Kind().String()))
if err != nil {
slog.Error("Failed to create local track for pushed stream", "room", room.Name, "track_kind", remoteTrack.Kind().String(), "err", err)
return
}
slog.Debug("Received track for pushed stream", "room", room.Name, "track_kind", remoteTrack.Kind().String())
// Set track for Room
room.SetTrack(remoteTrack.Kind(), localTrack)
// Prepare PlayoutDelayExtension so we don't need to recreate it for each packet // Prepare PlayoutDelayExtension so we don't need to recreate it for each packet
playoutExt := &rtp.PlayoutDelayExtension{ playoutExt := &rtp.PlayoutDelayExtension{
MinDelay: 0, MinDelay: 0,
@@ -730,6 +667,12 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
return return
} }
if remoteTrack.Kind() == webrtc.RTPCodecTypeAudio {
room.AudioCodec = remoteTrack.Codec().RTPCodecCapability
} else if remoteTrack.Kind() == webrtc.RTPCodecTypeVideo {
room.VideoCodec = remoteTrack.Codec().RTPCodecCapability
}
for { for {
rtpPacket, _, err := remoteTrack.ReadRTP() rtpPacket, _, err := remoteTrack.ReadRTP()
if err != nil { if err != nil {
@@ -741,19 +684,61 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
// Use PlayoutDelayExtension for low latency, if set for this track kind // Use PlayoutDelayExtension for low latency, if set for this track kind
if extID, ok := common.GetExtension(remoteTrack.Kind(), common.ExtensionPlayoutDelay); ok { if extID, ok := common.GetExtension(remoteTrack.Kind(), common.ExtensionPlayoutDelay); ok {
if err := rtpPacket.SetExtension(extID, playoutPayload); err != nil { if err = rtpPacket.SetExtension(extID, playoutPayload); err != nil {
slog.Error("Failed to set PlayoutDelayExtension for room", "room", room.Name, "err", err) slog.Error("Failed to set PlayoutDelayExtension for room", "room", room.Name, "err", err)
continue continue
} }
} }
room.BroadcastPacket(remoteTrack.Kind(), rtpPacket) // Calculate differences
var timeDiff int64
var sequenceDiff int
if remoteTrack.Kind() == webrtc.RTPCodecTypeVideo {
timeDiff = int64(rtpPacket.Timestamp) - int64(room.LastVideoTimestamp)
if !room.VideoTimestampSet {
timeDiff = 0
room.VideoTimestampSet = true
} else if timeDiff < -(math.MaxUint32 / 10) {
timeDiff += math.MaxUint32 + 1
}
sequenceDiff = int(rtpPacket.SequenceNumber) - int(room.LastVideoSequenceNumber)
if !room.VideoSequenceSet {
sequenceDiff = 0
room.VideoSequenceSet = true
} else if sequenceDiff < -(math.MaxUint16 / 10) {
sequenceDiff += math.MaxUint16 + 1
}
room.LastVideoTimestamp = rtpPacket.Timestamp
room.LastVideoSequenceNumber = rtpPacket.SequenceNumber
} else { // Audio
timeDiff = int64(rtpPacket.Timestamp) - int64(room.LastAudioTimestamp)
if !room.AudioTimestampSet {
timeDiff = 0
room.AudioTimestampSet = true
} else if timeDiff < -(math.MaxUint32 / 10) {
timeDiff += math.MaxUint32 + 1
}
sequenceDiff = int(rtpPacket.SequenceNumber) - int(room.LastAudioSequenceNumber)
if !room.AudioSequenceSet {
sequenceDiff = 0
room.AudioSequenceSet = true
} else if sequenceDiff < -(math.MaxUint16 / 10) {
sequenceDiff += math.MaxUint16 + 1
}
room.LastAudioTimestamp = rtpPacket.Timestamp
room.LastAudioSequenceNumber = rtpPacket.SequenceNumber
}
// Broadcast with differences
room.BroadcastPacketRetimed(remoteTrack.Kind(), rtpPacket, timeDiff, sequenceDiff)
} }
slog.Debug("Track closed for room", "room", room.Name, "track_kind", remoteTrack.Kind().String()) slog.Debug("Track closed for room", "room", room.Name, "track_kind", remoteTrack.Kind().String())
// Cleanup the track from the room
room.SetTrack(remoteTrack.Kind(), nil)
}) })
// Set the remote description // Set the remote description

View File

@@ -45,7 +45,7 @@ func (r *Relay) DeleteRoomIfEmpty(room *shared.Room) {
if room == nil { if room == nil {
return return
} }
if room.Participants.Len() == 0 && r.LocalRooms.Has(room.ID) { if len(room.Participants) <= 0 && r.LocalRooms.Has(room.ID) {
slog.Debug("Deleting empty room without participants", "room", room.Name) slog.Debug("Deleting empty room without participants", "room", room.Name)
r.LocalRooms.Delete(room.ID) r.LocalRooms.Delete(room.ID)
err := room.PeerConnection.Close() err := room.PeerConnection.Close()

View File

@@ -195,18 +195,18 @@ func (r *Relay) updateMeshRoomStates(peerID peer.ID, states []shared.RoomInfo) {
} }
// If previously did not exist, but does now, request a connection if participants exist for our room // If previously did not exist, but does now, request a connection if participants exist for our room
existed := r.Rooms.Has(state.ID.String()) /*existed := r.Rooms.Has(state.ID.String())
if !existed { if !existed {
// Request connection to this peer if we have participants in our local room // Request connection to this peer if we have participants in our local room
if room, ok := r.LocalRooms.Get(state.ID); ok { if room, ok := r.LocalRooms.Get(state.ID); ok {
if room.Participants.Len() > 0 { if len(room.Participants) > 0 {
slog.Debug("Got new remote room state, we locally have participants for, requesting stream", "room_name", room.Name, "peer", peerID) slog.Debug("Got new remote room state, we locally have participants for, requesting stream", "room_name", room.Name, "peer", peerID)
if err := r.StreamProtocol.RequestStream(context.Background(), room, peerID); err != nil { if err := r.StreamProtocol.RequestStream(context.Background(), room, peerID); err != nil {
slog.Error("Failed to request stream for new remote room state", "room_name", room.Name, "peer", peerID, "err", err) slog.Error("Failed to request stream for new remote room state", "room_name", room.Name, "peer", peerID, "err", err)
} }
} }
} }
} }*/
r.Rooms.Set(state.ID.String(), state) r.Rooms.Set(state.ID.String(), state)
} }

View File

@@ -363,9 +363,9 @@ func (x *ProtoKeyUp) GetKey() int32 {
// ControllerAttach message // ControllerAttach message
type ProtoControllerAttach struct { type ProtoControllerAttach struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // One of the following enums: "ps", "xbox" or "switch" Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // One of the following enums: "ps", "xbox" or "switch"
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3) SessionSlot int32 `protobuf:"varint,2,opt,name=session_slot,json=sessionSlot,proto3" json:"session_slot,omitempty"` // Session specific slot number (0-3)
SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Session ID of the client attaching the controller SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Session ID of the client
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -407,9 +407,9 @@ func (x *ProtoControllerAttach) GetId() string {
return "" return ""
} }
func (x *ProtoControllerAttach) GetSlot() int32 { func (x *ProtoControllerAttach) GetSessionSlot() int32 {
if x != nil { if x != nil {
return x.Slot return x.SessionSlot
} }
return 0 return 0
} }
@@ -424,7 +424,8 @@ func (x *ProtoControllerAttach) GetSessionId() string {
// ControllerDetach message // ControllerDetach message
type ProtoControllerDetach struct { type ProtoControllerDetach struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Slot int32 `protobuf:"varint,1,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3) SessionSlot int32 `protobuf:"varint,1,opt,name=session_slot,json=sessionSlot,proto3" json:"session_slot,omitempty"` // Session specific slot number (0-3)
SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Session ID of the client
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -459,19 +460,27 @@ func (*ProtoControllerDetach) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{8} return file_types_proto_rawDescGZIP(), []int{8}
} }
func (x *ProtoControllerDetach) GetSlot() int32 { func (x *ProtoControllerDetach) GetSessionSlot() int32 {
if x != nil { if x != nil {
return x.Slot return x.SessionSlot
} }
return 0 return 0
} }
func (x *ProtoControllerDetach) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
// ControllerButton message // ControllerButton message
type ProtoControllerButton struct { type ProtoControllerButton struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Slot int32 `protobuf:"varint,1,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3) SessionSlot int32 `protobuf:"varint,1,opt,name=session_slot,json=sessionSlot,proto3" json:"session_slot,omitempty"` // Session specific slot number (0-3)
Button int32 `protobuf:"varint,2,opt,name=button,proto3" json:"button,omitempty"` // Button code (linux input event code) SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Session ID of the client
Pressed bool `protobuf:"varint,3,opt,name=pressed,proto3" json:"pressed,omitempty"` // true if pressed, false if released Button int32 `protobuf:"varint,3,opt,name=button,proto3" json:"button,omitempty"` // Button code (linux input event code)
Pressed bool `protobuf:"varint,4,opt,name=pressed,proto3" json:"pressed,omitempty"` // true if pressed, false if released
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -506,13 +515,20 @@ func (*ProtoControllerButton) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{9} return file_types_proto_rawDescGZIP(), []int{9}
} }
func (x *ProtoControllerButton) GetSlot() int32 { func (x *ProtoControllerButton) GetSessionSlot() int32 {
if x != nil { if x != nil {
return x.Slot return x.SessionSlot
} }
return 0 return 0
} }
func (x *ProtoControllerButton) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *ProtoControllerButton) GetButton() int32 { func (x *ProtoControllerButton) GetButton() int32 {
if x != nil { if x != nil {
return x.Button return x.Button
@@ -530,9 +546,10 @@ func (x *ProtoControllerButton) GetPressed() bool {
// ControllerTriggers message // ControllerTriggers message
type ProtoControllerTrigger struct { type ProtoControllerTrigger struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Slot int32 `protobuf:"varint,1,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3) SessionSlot int32 `protobuf:"varint,1,opt,name=session_slot,json=sessionSlot,proto3" json:"session_slot,omitempty"` // Session specific slot number (0-3)
Trigger int32 `protobuf:"varint,2,opt,name=trigger,proto3" json:"trigger,omitempty"` // Trigger number (0 for left, 1 for right) SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Session ID of the client
Value int32 `protobuf:"varint,3,opt,name=value,proto3" json:"value,omitempty"` // trigger value (-32768 to 32767) Trigger int32 `protobuf:"varint,3,opt,name=trigger,proto3" json:"trigger,omitempty"` // Trigger number (0 for left, 1 for right)
Value int32 `protobuf:"varint,4,opt,name=value,proto3" json:"value,omitempty"` // trigger value (-32768 to 32767)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -567,13 +584,20 @@ func (*ProtoControllerTrigger) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{10} return file_types_proto_rawDescGZIP(), []int{10}
} }
func (x *ProtoControllerTrigger) GetSlot() int32 { func (x *ProtoControllerTrigger) GetSessionSlot() int32 {
if x != nil { if x != nil {
return x.Slot return x.SessionSlot
} }
return 0 return 0
} }
func (x *ProtoControllerTrigger) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *ProtoControllerTrigger) GetTrigger() int32 { func (x *ProtoControllerTrigger) GetTrigger() int32 {
if x != nil { if x != nil {
return x.Trigger return x.Trigger
@@ -591,10 +615,11 @@ func (x *ProtoControllerTrigger) GetValue() int32 {
// ControllerSticks message // ControllerSticks message
type ProtoControllerStick struct { type ProtoControllerStick struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Slot int32 `protobuf:"varint,1,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3) SessionSlot int32 `protobuf:"varint,1,opt,name=session_slot,json=sessionSlot,proto3" json:"session_slot,omitempty"` // Session specific slot number (0-3)
Stick int32 `protobuf:"varint,2,opt,name=stick,proto3" json:"stick,omitempty"` // Stick number (0 for left, 1 for right) SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Session ID of the client
X int32 `protobuf:"varint,3,opt,name=x,proto3" json:"x,omitempty"` // X axis value (-32768 to 32767) Stick int32 `protobuf:"varint,3,opt,name=stick,proto3" json:"stick,omitempty"` // Stick number (0 for left, 1 for right)
Y int32 `protobuf:"varint,4,opt,name=y,proto3" json:"y,omitempty"` // Y axis value (-32768 to 32767) X int32 `protobuf:"varint,4,opt,name=x,proto3" json:"x,omitempty"` // X axis value (-32768 to 32767)
Y int32 `protobuf:"varint,5,opt,name=y,proto3" json:"y,omitempty"` // Y axis value (-32768 to 32767)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -629,13 +654,20 @@ func (*ProtoControllerStick) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{11} return file_types_proto_rawDescGZIP(), []int{11}
} }
func (x *ProtoControllerStick) GetSlot() int32 { func (x *ProtoControllerStick) GetSessionSlot() int32 {
if x != nil { if x != nil {
return x.Slot return x.SessionSlot
} }
return 0 return 0
} }
func (x *ProtoControllerStick) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *ProtoControllerStick) GetStick() int32 { func (x *ProtoControllerStick) GetStick() int32 {
if x != nil { if x != nil {
return x.Stick return x.Stick
@@ -660,9 +692,10 @@ func (x *ProtoControllerStick) GetY() int32 {
// ControllerAxis message // ControllerAxis message
type ProtoControllerAxis struct { type ProtoControllerAxis struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Slot int32 `protobuf:"varint,1,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3) SessionSlot int32 `protobuf:"varint,1,opt,name=session_slot,json=sessionSlot,proto3" json:"session_slot,omitempty"` // Session specific slot number (0-3)
Axis int32 `protobuf:"varint,2,opt,name=axis,proto3" json:"axis,omitempty"` // Axis number (0 for d-pad horizontal, 1 for d-pad vertical) SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Session ID of the client
Value int32 `protobuf:"varint,3,opt,name=value,proto3" json:"value,omitempty"` // axis value (-1 to 1) Axis int32 `protobuf:"varint,3,opt,name=axis,proto3" json:"axis,omitempty"` // Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
Value int32 `protobuf:"varint,4,opt,name=value,proto3" json:"value,omitempty"` // axis value (-1 to 1)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -697,13 +730,20 @@ func (*ProtoControllerAxis) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{12} return file_types_proto_rawDescGZIP(), []int{12}
} }
func (x *ProtoControllerAxis) GetSlot() int32 { func (x *ProtoControllerAxis) GetSessionSlot() int32 {
if x != nil { if x != nil {
return x.Slot return x.SessionSlot
} }
return 0 return 0
} }
func (x *ProtoControllerAxis) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *ProtoControllerAxis) GetAxis() int32 { func (x *ProtoControllerAxis) GetAxis() int32 {
if x != nil { if x != nil {
return x.Axis return x.Axis
@@ -721,10 +761,11 @@ func (x *ProtoControllerAxis) GetValue() int32 {
// ControllerRumble message // ControllerRumble message
type ProtoControllerRumble struct { type ProtoControllerRumble struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Slot int32 `protobuf:"varint,1,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3) SessionSlot int32 `protobuf:"varint,1,opt,name=session_slot,json=sessionSlot,proto3" json:"session_slot,omitempty"` // Session specific slot number (0-3)
LowFrequency int32 `protobuf:"varint,2,opt,name=low_frequency,json=lowFrequency,proto3" json:"low_frequency,omitempty"` // Low frequency rumble (0-65535) SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Session ID of the client
HighFrequency int32 `protobuf:"varint,3,opt,name=high_frequency,json=highFrequency,proto3" json:"high_frequency,omitempty"` // High frequency rumble (0-65535) LowFrequency int32 `protobuf:"varint,3,opt,name=low_frequency,json=lowFrequency,proto3" json:"low_frequency,omitempty"` // Low frequency rumble (0-65535)
Duration int32 `protobuf:"varint,4,opt,name=duration,proto3" json:"duration,omitempty"` // Duration in milliseconds HighFrequency int32 `protobuf:"varint,4,opt,name=high_frequency,json=highFrequency,proto3" json:"high_frequency,omitempty"` // High frequency rumble (0-65535)
Duration int32 `protobuf:"varint,5,opt,name=duration,proto3" json:"duration,omitempty"` // Duration in milliseconds
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -759,13 +800,20 @@ func (*ProtoControllerRumble) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{13} return file_types_proto_rawDescGZIP(), []int{13}
} }
func (x *ProtoControllerRumble) GetSlot() int32 { func (x *ProtoControllerRumble) GetSessionSlot() int32 {
if x != nil { if x != nil {
return x.Slot return x.SessionSlot
} }
return 0 return 0
} }
func (x *ProtoControllerRumble) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *ProtoControllerRumble) GetLowFrequency() int32 { func (x *ProtoControllerRumble) GetLowFrequency() int32 {
if x != nil { if x != nil {
return x.LowFrequency return x.LowFrequency
@@ -1215,36 +1263,48 @@ const file_types_proto_rawDesc = "" +
"\x03key\x18\x01 \x01(\x05R\x03key\"\x1e\n" + "\x03key\x18\x01 \x01(\x05R\x03key\"\x1e\n" +
"\n" + "\n" +
"ProtoKeyUp\x12\x10\n" + "ProtoKeyUp\x12\x10\n" +
"\x03key\x18\x01 \x01(\x05R\x03key\"Z\n" + "\x03key\x18\x01 \x01(\x05R\x03key\"i\n" +
"\x15ProtoControllerAttach\x12\x0e\n" + "\x15ProtoControllerAttach\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12!\n" +
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x1d\n" + "\fsession_slot\x18\x02 \x01(\x05R\vsessionSlot\x12\x1d\n" +
"\n" + "\n" +
"session_id\x18\x03 \x01(\tR\tsessionId\"+\n" + "session_id\x18\x03 \x01(\tR\tsessionId\"Y\n" +
"\x15ProtoControllerDetach\x12\x12\n" + "\x15ProtoControllerDetach\x12!\n" +
"\x04slot\x18\x01 \x01(\x05R\x04slot\"]\n" + "\fsession_slot\x18\x01 \x01(\x05R\vsessionSlot\x12\x1d\n" +
"\x15ProtoControllerButton\x12\x12\n" + "\n" +
"\x04slot\x18\x01 \x01(\x05R\x04slot\x12\x16\n" + "session_id\x18\x02 \x01(\tR\tsessionId\"\x8b\x01\n" +
"\x06button\x18\x02 \x01(\x05R\x06button\x12\x18\n" + "\x15ProtoControllerButton\x12!\n" +
"\apressed\x18\x03 \x01(\bR\apressed\"\\\n" + "\fsession_slot\x18\x01 \x01(\x05R\vsessionSlot\x12\x1d\n" +
"\x16ProtoControllerTrigger\x12\x12\n" + "\n" +
"\x04slot\x18\x01 \x01(\x05R\x04slot\x12\x18\n" + "session_id\x18\x02 \x01(\tR\tsessionId\x12\x16\n" +
"\atrigger\x18\x02 \x01(\x05R\atrigger\x12\x14\n" + "\x06button\x18\x03 \x01(\x05R\x06button\x12\x18\n" +
"\x05value\x18\x03 \x01(\x05R\x05value\"\\\n" + "\apressed\x18\x04 \x01(\bR\apressed\"\x8a\x01\n" +
"\x14ProtoControllerStick\x12\x12\n" + "\x16ProtoControllerTrigger\x12!\n" +
"\x04slot\x18\x01 \x01(\x05R\x04slot\x12\x14\n" + "\fsession_slot\x18\x01 \x01(\x05R\vsessionSlot\x12\x1d\n" +
"\x05stick\x18\x02 \x01(\x05R\x05stick\x12\f\n" + "\n" +
"\x01x\x18\x03 \x01(\x05R\x01x\x12\f\n" + "session_id\x18\x02 \x01(\tR\tsessionId\x12\x18\n" +
"\x01y\x18\x04 \x01(\x05R\x01y\"S\n" + "\atrigger\x18\x03 \x01(\x05R\atrigger\x12\x14\n" +
"\x13ProtoControllerAxis\x12\x12\n" + "\x05value\x18\x04 \x01(\x05R\x05value\"\x8a\x01\n" +
"\x04slot\x18\x01 \x01(\x05R\x04slot\x12\x12\n" + "\x14ProtoControllerStick\x12!\n" +
"\x04axis\x18\x02 \x01(\x05R\x04axis\x12\x14\n" + "\fsession_slot\x18\x01 \x01(\x05R\vsessionSlot\x12\x1d\n" +
"\x05value\x18\x03 \x01(\x05R\x05value\"\x93\x01\n" + "\n" +
"\x15ProtoControllerRumble\x12\x12\n" + "session_id\x18\x02 \x01(\tR\tsessionId\x12\x14\n" +
"\x04slot\x18\x01 \x01(\x05R\x04slot\x12#\n" + "\x05stick\x18\x03 \x01(\x05R\x05stick\x12\f\n" +
"\rlow_frequency\x18\x02 \x01(\x05R\flowFrequency\x12%\n" + "\x01x\x18\x04 \x01(\x05R\x01x\x12\f\n" +
"\x0ehigh_frequency\x18\x03 \x01(\x05R\rhighFrequency\x12\x1a\n" + "\x01y\x18\x05 \x01(\x05R\x01y\"\x81\x01\n" +
"\bduration\x18\x04 \x01(\x05R\bduration\"\xde\x01\n" + "\x13ProtoControllerAxis\x12!\n" +
"\fsession_slot\x18\x01 \x01(\x05R\vsessionSlot\x12\x1d\n" +
"\n" +
"session_id\x18\x02 \x01(\tR\tsessionId\x12\x12\n" +
"\x04axis\x18\x03 \x01(\x05R\x04axis\x12\x14\n" +
"\x05value\x18\x04 \x01(\x05R\x05value\"\xc1\x01\n" +
"\x15ProtoControllerRumble\x12!\n" +
"\fsession_slot\x18\x01 \x01(\x05R\vsessionSlot\x12\x1d\n" +
"\n" +
"session_id\x18\x02 \x01(\tR\tsessionId\x12#\n" +
"\rlow_frequency\x18\x03 \x01(\x05R\flowFrequency\x12%\n" +
"\x0ehigh_frequency\x18\x04 \x01(\x05R\rhighFrequency\x12\x1a\n" +
"\bduration\x18\x05 \x01(\x05R\bduration\"\xde\x01\n" +
"\x13RTCIceCandidateInit\x12\x1c\n" + "\x13RTCIceCandidateInit\x12\x1c\n" +
"\tcandidate\x18\x01 \x01(\tR\tcandidate\x12)\n" + "\tcandidate\x18\x01 \x01(\tR\tcandidate\x12)\n" +
"\rsdpMLineIndex\x18\x02 \x01(\rH\x00R\rsdpMLineIndex\x88\x01\x01\x12\x1b\n" + "\rsdpMLineIndex\x18\x02 \x01(\rH\x00R\rsdpMLineIndex\x88\x01\x01\x12\x1b\n" +

View File

@@ -1,14 +1,16 @@
package shared package shared
import ( import (
"errors"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"relay/internal/common" "relay/internal/common"
"relay/internal/connections" "relay/internal/connections"
"sync"
"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"
) )
@@ -22,8 +24,15 @@ type Participant struct {
// Per-viewer tracks and channels // Per-viewer tracks and channels
VideoTrack *webrtc.TrackLocalStaticRTP VideoTrack *webrtc.TrackLocalStaticRTP
AudioTrack *webrtc.TrackLocalStaticRTP AudioTrack *webrtc.TrackLocalStaticRTP
VideoChan chan *rtp.Packet
AudioChan chan *rtp.Packet // Per-viewer RTP state for retiming
VideoSequenceNumber uint16
VideoTimestamp uint32
AudioSequenceNumber uint16
AudioTimestamp uint32
packetQueue chan *participantPacket
closeOnce sync.Once
} }
func NewParticipant(sessionID string, peerID peer.ID) (*Participant, error) { func NewParticipant(sessionID string, peerID peer.ID) (*Participant, error) {
@@ -31,24 +40,50 @@ func NewParticipant(sessionID string, peerID peer.ID) (*Participant, error) {
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{ p := &Participant{
ID: id, ID: id,
SessionID: sessionID, SessionID: sessionID,
PeerID: peerID, PeerID: peerID,
VideoChan: make(chan *rtp.Packet, 500), VideoSequenceNumber: 0,
AudioChan: make(chan *rtp.Packet, 100), VideoTimestamp: 0,
}, nil AudioSequenceNumber: 0,
AudioTimestamp: 0,
packetQueue: make(chan *participantPacket, 1000),
}
go p.packetWriter()
return p, nil
}
// SetTrack sets audio/video track for Participant
func (p *Participant) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
switch trackType {
case webrtc.RTPCodecTypeAudio:
p.AudioTrack = track
_, err := p.PeerConnection.AddTrack(track)
if err != nil {
slog.Error("Failed to add Participant audio track", err)
}
case webrtc.RTPCodecTypeVideo:
p.VideoTrack = track
_, err := p.PeerConnection.AddTrack(track)
if err != nil {
slog.Error("Failed to add Participant video track", err)
}
default:
slog.Warn("Unknown track type", "participant", p.ID, "trackType", trackType)
}
} }
// Close cleans up participant resources // Close cleans up participant resources
func (p *Participant) Close() { func (p *Participant) Close() {
if p.VideoChan != nil { if p.DataChannel != nil {
close(p.VideoChan) err := p.DataChannel.Close()
p.VideoChan = nil if err != nil {
} slog.Error("Failed to close Participant DataChannel", err)
if p.AudioChan != nil { }
close(p.AudioChan) p.DataChannel = nil
p.AudioChan = nil
} }
if p.PeerConnection != nil { if p.PeerConnection != nil {
err := p.PeerConnection.Close() err := p.PeerConnection.Close()
@@ -57,4 +92,45 @@ func (p *Participant) Close() {
} }
p.PeerConnection = nil p.PeerConnection = nil
} }
if p.VideoTrack != nil {
p.VideoTrack = nil
}
if p.AudioTrack != nil {
p.AudioTrack = nil
}
}
func (p *Participant) packetWriter() {
for pkt := range p.packetQueue {
var track *webrtc.TrackLocalStaticRTP
var sequenceNumber uint16
var timestamp uint32
// No mutex needed - only this goroutine modifies these
if pkt.kind == webrtc.RTPCodecTypeAudio {
track = p.AudioTrack
p.AudioSequenceNumber = uint16(int(p.AudioSequenceNumber) + pkt.sequenceDiff)
p.AudioTimestamp = uint32(int64(p.AudioTimestamp) + pkt.timeDiff)
sequenceNumber = p.AudioSequenceNumber
timestamp = p.AudioTimestamp
} else {
track = p.VideoTrack
p.VideoSequenceNumber = uint16(int(p.VideoSequenceNumber) + pkt.sequenceDiff)
p.VideoTimestamp = uint32(int64(p.VideoTimestamp) + pkt.timeDiff)
sequenceNumber = p.VideoSequenceNumber
timestamp = p.VideoTimestamp
}
if track != nil {
pkt.packet.SequenceNumber = sequenceNumber
pkt.packet.Timestamp = timestamp
if err := track.WriteRTP(pkt.packet); err != nil && !errors.Is(err, io.ErrClosedPipe) {
slog.Error("WriteRTP failed", "participant", p.ID, "kind", pkt.kind, "err", err)
}
}
// Return packet struct to pool
participantPacketPool.Put(pkt)
}
} }

View File

@@ -2,9 +2,9 @@ package shared
import ( import (
"log/slog" "log/slog"
"relay/internal/common"
"relay/internal/connections" "relay/internal/connections"
"time" "sync"
"sync/atomic"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
@@ -12,6 +12,19 @@ import (
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
) )
var participantPacketPool = sync.Pool{
New: func() interface{} {
return &participantPacket{}
},
}
type participantPacket struct {
kind webrtc.RTPCodecType
packet *rtp.Packet
timeDiff int64
sequenceDiff int
}
type RoomInfo struct { type RoomInfo struct {
ID ulid.ULID `json:"id"` ID ulid.ULID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -20,16 +33,27 @@ type RoomInfo struct {
type Room struct { type Room struct {
RoomInfo RoomInfo
AudioCodec webrtc.RTPCodecCapability
VideoCodec webrtc.RTPCodecCapability
PeerConnection *webrtc.PeerConnection PeerConnection *webrtc.PeerConnection
AudioTrack *webrtc.TrackLocalStaticRTP
VideoTrack *webrtc.TrackLocalStaticRTP
DataChannel *connections.NestriDataChannel DataChannel *connections.NestriDataChannel
Participants *common.SafeMap[ulid.ULID, *Participant]
// Broadcast queues (unbuffered, fan-out happens async) // Atomic pointer to slice of participant channels
videoBroadcastChan chan *rtp.Packet participantChannels atomic.Pointer[[]chan<- *participantPacket]
audioBroadcastChan chan *rtp.Packet participantsMtx sync.Mutex // Use only for add/remove
broadcastStop chan struct{}
Participants map[ulid.ULID]*Participant // Keep general track of Participant(s)
// Track last seen values to calculate diffs
LastVideoTimestamp uint32
LastVideoSequenceNumber uint16
LastAudioTimestamp uint32
LastAudioSequenceNumber uint16
VideoTimestampSet bool
VideoSequenceSet bool
AudioTimestampSet bool
AudioSequenceSet bool
} }
func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room { func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room {
@@ -39,133 +63,109 @@ func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room {
Name: name, Name: name,
OwnerID: ownerID, OwnerID: ownerID,
}, },
Participants: common.NewSafeMap[ulid.ULID, *Participant](), PeerConnection: nil,
videoBroadcastChan: make(chan *rtp.Packet, 1000), // Large buffer for incoming packets DataChannel: nil,
audioBroadcastChan: make(chan *rtp.Packet, 500), Participants: make(map[ulid.ULID]*Participant),
broadcastStop: make(chan struct{}),
} }
// Start async broadcasters emptyChannels := make([]chan<- *participantPacket, 0)
go r.videoBroadcaster() r.participantChannels.Store(&emptyChannels)
go r.audioBroadcaster()
return r return r
} }
// Close closes up Room (stream ended)
func (r *Room) Close() {
if r.DataChannel != nil {
err := r.DataChannel.Close()
if err != nil {
slog.Error("Failed to close Room DataChannel", err)
}
r.DataChannel = nil
}
if r.PeerConnection != nil {
err := r.PeerConnection.Close()
if err != nil {
slog.Error("Failed to close Room PeerConnection", err)
}
r.PeerConnection = nil
}
}
// AddParticipant adds a Participant to a Room // AddParticipant adds a Participant to a Room
func (r *Room) AddParticipant(participant *Participant) { func (r *Room) AddParticipant(participant *Participant) {
slog.Debug("Adding participant to room", "participant", participant.ID, "room", r.Name) r.participantsMtx.Lock()
r.Participants.Set(participant.ID, participant) defer r.participantsMtx.Unlock()
r.Participants[participant.ID] = participant
// Update channel slice atomically
current := r.participantChannels.Load()
newChannels := make([]chan<- *participantPacket, len(*current)+1)
copy(newChannels, *current)
newChannels[len(*current)] = participant.packetQueue
r.participantChannels.Store(&newChannels)
slog.Debug("Added participant", "participant", participant.ID, "room", r.Name)
} }
// RemoveParticipantByID 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 { r.participantsMtx.Lock()
r.Participants.Delete(pID) defer r.participantsMtx.Unlock()
participant, ok := r.Participants[pID]
if !ok {
return
} }
delete(r.Participants, pID)
// Update channel slice
current := r.participantChannels.Load()
newChannels := make([]chan<- *participantPacket, 0, len(*current)-1)
for _, ch := range *current {
if ch != participant.packetQueue {
newChannels = append(newChannels, ch)
}
}
r.participantChannels.Store(&newChannels)
slog.Debug("Removed participant", "participant", pID, "room", r.Name)
} }
// IsOnline checks if the room is online (has both audio and video tracks) // IsOnline checks if the room is online
func (r *Room) IsOnline() bool { func (r *Room) IsOnline() bool {
return r.AudioTrack != nil && r.VideoTrack != nil return r.PeerConnection != nil
} }
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) { func (r *Room) BroadcastPacketRetimed(kind webrtc.RTPCodecType, pkt *rtp.Packet, timeDiff int64, sequenceDiff int) {
switch trackType { // Lock-free load of channel slice
case webrtc.RTPCodecTypeAudio: channels := r.participantChannels.Load()
r.AudioTrack = track
case webrtc.RTPCodecTypeVideo: // no participants..
r.VideoTrack = track if len(*channels) == 0 {
default: return
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
} }
}
// BroadcastPacket enqueues packet for async broadcast (non-blocking) // Send to each participant channel (non-blocking)
func (r *Room) BroadcastPacket(kind webrtc.RTPCodecType, pkt *rtp.Packet) { for i, ch := range *channels {
start := time.Now() // Get packet struct from pool
if kind == webrtc.RTPCodecTypeVideo { pp := participantPacketPool.Get().(*participantPacket)
pp.kind = kind
pp.packet = pkt.Clone()
pp.timeDiff = timeDiff
pp.sequenceDiff = sequenceDiff
select { select {
case r.videoBroadcastChan <- pkt: case ch <- pp:
duration := time.Since(start) // Sent successfully
if duration > 10*time.Millisecond {
slog.Warn("Slow video broadcast enqueue", "duration", duration, "room", r.Name)
}
default: default:
// Broadcast queue full - system overload, drop packet globally // Channel full, drop packet, log?
slog.Warn("Video broadcast queue full, dropping packet", "room", r.Name) slog.Warn("Channel full, dropping packet", "channel_index", i)
} participantPacketPool.Put(pp)
} 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

@@ -15,13 +15,13 @@ NVIDIA_INSTALLER_DIR="/tmp"
TIMEOUT_SECONDS=10 TIMEOUT_SECONDS=10
ENTCMD_PREFIX="" ENTCMD_PREFIX=""
# Ensures user directory ownership # Ensures user ownership across directories
chown_user_directory() { handle_user_permissions() {
if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" 2>/dev/null; then if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" 2>/dev/null; then
echo "Error: Failed to change ownership of ${NESTRI_HOME} to ${NESTRI_USER}:${NESTRI_USER}" >&2 echo "Error: Failed to change ownership of ${NESTRI_HOME} to ${NESTRI_USER}:${NESTRI_USER}" >&2
return 1 return 1
fi fi
# Also apply to .cache separately # Also apply to .cache
if [[ -d "${NESTRI_HOME}/.cache" ]]; then if [[ -d "${NESTRI_HOME}/.cache" ]]; then
if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}/.cache" 2>/dev/null; then if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}/.cache" 2>/dev/null; then
echo "Error: Failed to change ownership of ${NESTRI_HOME}/.cache to ${NESTRI_USER}:${NESTRI_USER}" >&2 echo "Error: Failed to change ownership of ${NESTRI_HOME}/.cache to ${NESTRI_USER}:${NESTRI_USER}" >&2
@@ -324,9 +324,23 @@ main() {
log "Skipping CAP_SYS_NICE for gamescope, capability not available" log "Skipping CAP_SYS_NICE for gamescope, capability not available"
fi fi
# Handle user directory permissions # Make sure /tmp/.X11-unix exists..
log "Ensuring user directory permissions..." if [[ ! -d "/tmp/.X11-unix" ]]; then
chown_user_directory || exit 1 log "Creating /tmp/.X11-unix directory.."
$ENTCMD_PREFIX mkdir -p /tmp/.X11-unix || {
log "Error: Failed to create /tmp/.X11-unix directory"
exit 1
}
# Set required perms..
$ENTCMD_PREFIX chmod 1777 /tmp/.X11-unix || {
log "Error: Failed to chmod /tmp/.X11-unix to 1777"
exit 1
}
fi
# Handle user permissions
log "Ensuring user permissions..."
handle_user_permissions || exit 1
# Setup namespaceless env if needed for container runtime # Setup namespaceless env if needed for container runtime
if [[ "$container_runtime" != "podman" ]]; then if [[ "$container_runtime" != "podman" ]]; then
@@ -336,7 +350,7 @@ main() {
# Make sure /run/udev/ directory exists with /run/udev/control, needed for virtual controller support # Make sure /run/udev/ directory exists with /run/udev/control, needed for virtual controller support
if [[ ! -d "/run/udev" || ! -e "/run/udev/control" ]]; then if [[ ! -d "/run/udev" || ! -e "/run/udev/control" ]]; then
log "Creating /run/udev directory and control file..." log "Creating /run/udev directory and control file.."
$ENTCMD_PREFIX mkdir -p /run/udev || { $ENTCMD_PREFIX mkdir -p /run/udev || {
log "Error: Failed to create /run/udev directory" log "Error: Failed to create /run/udev directory"
exit 1 exit 1

View File

@@ -187,7 +187,7 @@ start_compositor() {
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
log "Starting application: $NESTRI_LAUNCH_CMD" log "Starting application: $NESTRI_LAUNCH_CMD"
WAYLAND_DISPLAY=wayland-0 /bin/bash -c "$NESTRI_LAUNCH_CMD" & WAYLAND_DISPLAY="$COMPOSITOR_SOCKET" /bin/bash -c "$NESTRI_LAUNCH_CMD" &
APP_PID=$! APP_PID=$!
fi fi
else else

View File

@@ -181,7 +181,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
"synstructure", "synstructure",
] ]
@@ -193,7 +193,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
"synstructure", "synstructure",
] ]
@@ -205,7 +205,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -246,7 +246,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -257,7 +257,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -395,7 +395,7 @@ version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.10.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.13.0", "itertools 0.13.0",
@@ -406,7 +406,7 @@ dependencies = [
"regex", "regex",
"rustc-hash", "rustc-hash",
"shlex", "shlex",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -417,9 +417,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.4" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]] [[package]]
name = "blake2" name = "blake2"
@@ -603,9 +603,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.49" version = "4.5.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -613,9 +613,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.49" version = "4.5.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -632,7 +632,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -839,7 +839,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -879,7 +879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
dependencies = [ dependencies = [
"data-encoding", "data-encoding",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -956,7 +956,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -1090,7 +1090,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -1262,7 +1262,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -1396,7 +1396,7 @@ version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1f2cbc4577536c849335878552f42086bfd25a8dcd6f54a18655cf818b20c8f" checksum = "e1f2cbc4577536c849335878552f42086bfd25a8dcd6f54a18655cf818b20c8f"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.10.0",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor", "futures-executor",
@@ -1421,7 +1421,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -2299,9 +2299,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.11.4" version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.0", "hashbrown 0.16.0",
@@ -2368,9 +2368,9 @@ dependencies = [
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]] [[package]]
name = "itertools" name = "itertools"
@@ -2828,7 +2828,7 @@ checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c"
dependencies = [ dependencies = [
"heck", "heck",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -3239,7 +3239,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.10.0",
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"libc", "libc",
@@ -3355,9 +3355,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell_polyfill" name = "once_cell_polyfill"
version = "1.70.1" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@@ -3499,7 +3499,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -3604,7 +3604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -3627,9 +3627,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.101" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -3654,7 +3654,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -3677,7 +3677,7 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -3861,7 +3861,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.10.0",
] ]
[[package]] [[package]]
@@ -4038,7 +4038,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.10.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@@ -4047,9 +4047,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.33" version = "0.23.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"log", "log",
@@ -4180,7 +4180,7 @@ version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.10.0",
"core-foundation 0.10.1", "core-foundation 0.10.1",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -4240,7 +4240,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -4481,9 +4481,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.106" version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -4507,7 +4507,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -4516,7 +4516,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.10.0",
"core-foundation 0.9.4", "core-foundation 0.9.4",
"system-configuration-sys", "system-configuration-sys",
] ]
@@ -4574,7 +4574,7 @@ checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -4603,7 +4603,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -4614,7 +4614,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -4707,7 +4707,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -4817,7 +4817,7 @@ version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.10.0",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@@ -4861,7 +4861,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -4986,9 +4986,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.19" version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]] [[package]]
name = "universal-hash" name = "universal-hash"
@@ -5079,9 +5079,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "vimputti" name = "vimputti"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5839a89185ccec572f746ccc02e37702cc6c0b62a6aa0d9bcd6e5921edba12" checksum = "ffb370ee43e3ee4ca5329886e64dc5b27c83dc8cced5a63c2418777dac9a41a8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",
@@ -5177,7 +5177,7 @@ dependencies = [
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -5212,7 +5212,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -5502,7 +5502,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -5513,7 +5513,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -5959,7 +5959,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
"synstructure", "synstructure",
] ]
@@ -5980,7 +5980,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -6000,7 +6000,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
"synstructure", "synstructure",
] ]
@@ -6021,7 +6021,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]
[[package]] [[package]]
@@ -6054,5 +6054,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.108",
] ]

View File

@@ -22,7 +22,7 @@ rand = "0.9"
rustls = { version = "0.23", features = ["ring"] } rustls = { version = "0.23", features = ["ring"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
vimputti = "0.1.3" vimputti = "0.1.4"
chrono = "0.4" chrono = "0.4"
prost = "0.14" prost = "0.14"
prost-types = "0.14" prost-types = "0.14"

View File

@@ -211,6 +211,14 @@ impl Args {
.value_parser(value_parser!(u32).range(1..)) .value_parser(value_parser!(u32).range(1..))
.default_value("192"), .default_value("192"),
) )
.arg(
Arg::new("software-render")
.long("software-render")
.env("SOFTWARE_RENDER")
.help("Use software rendering for wayland")
.value_parser(BoolishValueParser::new())
.default_value("false"),
)
.arg( .arg(
Arg::new("zero-copy") Arg::new("zero-copy")
.long("zero-copy") .long("zero-copy")

View File

@@ -15,6 +15,9 @@ pub struct AppArgs {
/// vimputti socket path /// vimputti socket path
pub vimputti_path: Option<String>, pub vimputti_path: Option<String>,
/// Use software rendering for wayland display
pub software_render: bool,
/// Experimental zero-copy pipeline support /// Experimental zero-copy pipeline support
/// TODO: Move to video encoding flags /// TODO: Move to video encoding flags
pub zero_copy: bool, pub zero_copy: bool,
@@ -51,6 +54,10 @@ impl AppArgs {
vimputti_path: matches vimputti_path: matches
.get_one::<String>("vimputti-path") .get_one::<String>("vimputti-path")
.map(|s| s.clone()), .map(|s| s.clone()),
software_render: matches
.get_one::<bool>("software-render")
.unwrap_or(&false)
.clone(),
zero_copy: matches zero_copy: matches
.get_one::<bool>("zero-copy") .get_one::<bool>("zero-copy")
.unwrap_or(&false) .unwrap_or(&false)
@@ -73,6 +80,7 @@ impl AppArgs {
"> vimputti_path: '{}'", "> vimputti_path: '{}'",
self.vimputti_path.as_ref().map_or("None", |s| s.as_str()) self.vimputti_path.as_ref().map_or("None", |s| s.as_str())
); );
tracing::info!("> software_render: {}", self.software_render);
tracing::info!("> zero_copy: {}", self.zero_copy); tracing::info!("> zero_copy: {}", self.zero_copy);
} }
} }

View File

@@ -585,7 +585,6 @@ pub fn get_best_working_encoder(
encoders: &Vec<VideoEncoderInfo>, encoders: &Vec<VideoEncoderInfo>,
codec: &Codec, codec: &Codec,
encoder_type: &EncoderType, encoder_type: &EncoderType,
zero_copy: bool,
) -> Result<VideoEncoderInfo, Box<dyn Error>> { ) -> Result<VideoEncoderInfo, Box<dyn Error>> {
let mut candidates = get_encoders_by_videocodec( let mut candidates = get_encoders_by_videocodec(
encoders, encoders,
@@ -601,7 +600,7 @@ pub fn get_best_working_encoder(
while !candidates.is_empty() { while !candidates.is_empty() {
let best = get_best_compatible_encoder(&candidates, codec, encoder_type)?; let best = get_best_compatible_encoder(&candidates, codec, encoder_type)?;
tracing::info!("Testing encoder: {}", best.name,); tracing::info!("Testing encoder: {}", best.name,);
if test_encoder(&best, zero_copy).is_ok() { if test_encoder(&best).is_ok() {
return Ok(best); return Ok(best);
} else { } else {
// Remove this encoder and try next best // Remove this encoder and try next best
@@ -613,25 +612,10 @@ pub fn get_best_working_encoder(
} }
/// Test if a pipeline with the given encoder can be created and set to Playing /// Test if a pipeline with the given encoder can be created and set to Playing
pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), Box<dyn Error>> { pub fn test_encoder(encoder: &VideoEncoderInfo) -> Result<(), Box<dyn Error>> {
let src = gstreamer::ElementFactory::make("waylanddisplaysrc").build()?; let src = gstreamer::ElementFactory::make("videotestsrc").build()?;
if let Some(gpu_info) = &encoder.gpu_info {
src.set_property_from_str("render-node", gpu_info.render_path());
}
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?; let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
let caps = gstreamer::Caps::from_str(&format!( let caps = gstreamer::Caps::from_str("video/x-raw,width=1280,height=720,framerate=30/1")?;
"{},width=1280,height=720,framerate=30/1{}",
if zero_copy {
if encoder.encoder_api == EncoderAPI::NVENC {
"video/x-raw(memory:CUDAMemory)"
} else {
"video/x-raw(memory:DMABuf)"
}
} else {
"video/x-raw"
},
if zero_copy { "" } else { ",format=RGBx" }
))?;
caps_filter.set_property("caps", &caps); caps_filter.set_property("caps", &caps);
let enc = gstreamer::ElementFactory::make(&encoder.name).build()?; let enc = gstreamer::ElementFactory::make(&encoder.name).build()?;
@@ -642,41 +626,9 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), B
// Create pipeline and link elements // Create pipeline and link elements
let pipeline = gstreamer::Pipeline::new(); let pipeline = gstreamer::Pipeline::new();
if zero_copy { let videoconvert = gstreamer::ElementFactory::make("videoconvert").build()?;
if encoder.encoder_api == EncoderAPI::NVENC { pipeline.add_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
// NVENC zero-copy path gstreamer::Element::link_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
pipeline.add_many(&[&src, &caps_filter, &enc, &sink])?;
gstreamer::Element::link_many(&[&src, &caps_filter, &enc, &sink])?;
} else {
// VA-API/QSV zero-copy path
let vapostproc = gstreamer::ElementFactory::make("vapostproc").build()?;
let va_caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
let va_caps = gstreamer::Caps::from_str("video/x-raw(memory:VAMemory),format=NV12")?;
va_caps_filter.set_property("caps", &va_caps);
pipeline.add_many(&[
&src,
&caps_filter,
&vapostproc,
&va_caps_filter,
&enc,
&sink,
])?;
gstreamer::Element::link_many(&[
&src,
&caps_filter,
&vapostproc,
&va_caps_filter,
&enc,
&sink,
])?;
}
} else {
// Non-zero-copy path for all encoders - needs videoconvert
let videoconvert = gstreamer::ElementFactory::make("videoconvert").build()?;
pipeline.add_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
gstreamer::Element::link_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
}
let bus = pipeline.bus().ok_or("Pipeline has no bus")?; let bus = pipeline.bus().ok_or("Pipeline has no bus")?;
pipeline.set_state(gstreamer::State::Playing)?; pipeline.set_state(gstreamer::State::Playing)?;

View File

@@ -47,7 +47,7 @@ impl ControllerInput {
pub struct ControllerManager { pub struct ControllerManager {
vimputti_client: Arc<vimputti::client::VimputtiClient>, vimputti_client: Arc<vimputti::client::VimputtiClient>,
cmd_tx: mpsc::Sender<Payload>, 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, String)>, // (slot, strong, weak, duration_ms, session_id)
attach_tx: mpsc::Sender<ProtoControllerAttach>, attach_tx: mpsc::Sender<ProtoControllerAttach>,
} }
impl ControllerManager { impl ControllerManager {
@@ -55,7 +55,7 @@ impl ControllerManager {
vimputti_client: Arc<vimputti::client::VimputtiClient>, vimputti_client: Arc<vimputti::client::VimputtiClient>,
) -> Result<( ) -> Result<(
Self, Self,
mpsc::Receiver<(u32, u16, u16, u16)>, mpsc::Receiver<(u32, u16, u16, u16, String)>,
mpsc::Receiver<ProtoControllerAttach>, mpsc::Receiver<ProtoControllerAttach>,
)> { )> {
let (cmd_tx, cmd_rx) = mpsc::channel(512); let (cmd_tx, cmd_rx) = mpsc::channel(512);
@@ -88,12 +88,12 @@ impl ControllerManager {
struct ControllerSlot { struct ControllerSlot {
controller: ControllerInput, controller: ControllerInput,
session_id: String, session_id: String,
last_activity: std::time::Instant, session_slot: u32,
} }
// Returns first free controller slot from 0-7 // Returns first free controller slot from 0-16
fn get_free_slot(controllers: &HashMap<u32, ControllerSlot>) -> Option<u32> { fn get_free_slot(controllers: &HashMap<u32, ControllerSlot>) -> Option<u32> {
for slot in 0..8 { for slot in 0..17 {
if !controllers.contains_key(&slot) { if !controllers.contains_key(&slot) {
return Some(slot); return Some(slot);
} }
@@ -104,7 +104,7 @@ fn get_free_slot(controllers: &HashMap<u32, ControllerSlot>) -> Option<u32> {
async fn command_loop( async fn command_loop(
mut cmd_rx: mpsc::Receiver<Payload>, 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, String)>,
attach_tx: mpsc::Sender<ProtoControllerAttach>, attach_tx: mpsc::Sender<ProtoControllerAttach>,
) { ) {
let mut controllers: HashMap<u32, ControllerSlot> = HashMap::new(); let mut controllers: HashMap<u32, ControllerSlot> = HashMap::new();
@@ -112,13 +112,15 @@ async fn command_loop(
match payload { match payload {
Payload::ControllerAttach(data) => { Payload::ControllerAttach(data) => {
let session_id = data.session_id.clone(); let session_id = data.session_id.clone();
let session_slot = data.session_slot.clone();
// Check if this session already has a slot (reconnection) // Check if this session already has a slot (reconnection)
let existing_slot = controllers let existing_slot = controllers
.iter() .iter()
.find(|(_, slot)| slot.session_id == session_id && !session_id.is_empty()) .find(|(_, slot)| {
slot.session_id == session_id && slot.session_slot == session_slot as u32
})
.map(|(slot_num, _)| *slot_num); .map(|(slot_num, _)| *slot_num);
let slot = existing_slot.or_else(|| get_free_slot(&controllers)); let slot = existing_slot.or_else(|| get_free_slot(&controllers));
if let Some(slot) = slot { if let Some(slot) = slot {
@@ -131,7 +133,7 @@ async fn command_loop(
controller controller
.device_mut() .device_mut()
.on_rumble(move |strong, weak, duration_ms| { .on_rumble(move |strong, weak, duration_ms| {
let _ = rumble_tx.try_send((slot, strong, weak, duration_ms)); let _ = rumble_tx.try_send((slot, strong, weak, duration_ms, data.session_id.clone()));
}) })
.await .await
.map_err(|e| { .map_err(|e| {
@@ -146,7 +148,7 @@ async fn command_loop(
// Return to attach_tx what slot was assigned // Return to attach_tx what slot was assigned
let attach_info = ProtoControllerAttach { let attach_info = ProtoControllerAttach {
id: data.id.clone(), id: data.id.clone(),
slot: slot as i32, session_slot: slot as i32,
session_id: session_id.clone(), session_id: session_id.clone(),
}; };
@@ -157,7 +159,7 @@ async fn command_loop(
ControllerSlot { ControllerSlot {
controller, controller,
session_id: session_id.clone(), session_id: session_id.clone(),
last_activity: std::time::Instant::now(), session_slot: session_slot.clone() as u32,
}, },
); );
tracing::info!( tracing::info!(
@@ -185,25 +187,25 @@ async fn command_loop(
} }
} }
Payload::ControllerDetach(data) => { Payload::ControllerDetach(data) => {
if controllers.remove(&(data.slot as u32)).is_some() { if controllers.remove(&(data.session_slot as u32)).is_some() {
tracing::info!("Controller detached from slot {}", data.slot); tracing::info!("Controller detached from slot {}", data.session_slot);
} else { } else {
tracing::warn!("No controller found in slot {} to detach", data.slot); tracing::warn!("No controller found in slot {} to detach", data.session_slot);
} }
} }
Payload::ControllerButton(data) => { Payload::ControllerButton(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) { if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
if let Some(button) = vimputti::Button::from_ev_code(data.button as u16) { if let Some(button) = vimputti::Button::from_ev_code(data.button as u16) {
let device = controller.controller.device(); let device = controller.controller.device();
device.button(button, data.pressed); device.button(button, data.pressed);
device.sync(); device.sync();
} }
} else { } else {
tracing::warn!("Controller slot {} not found for button event", data.slot); tracing::warn!("Controller slot {} not found for button event", data.session_slot);
} }
} }
Payload::ControllerStick(data) => { Payload::ControllerStick(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) { if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
let device = controller.controller.device(); let device = controller.controller.device();
if data.stick == 0 { if data.stick == 0 {
// Left stick // Left stick
@@ -218,11 +220,11 @@ async fn command_loop(
} }
device.sync(); device.sync();
} else { } else {
tracing::warn!("Controller slot {} not found for stick event", data.slot); tracing::warn!("Controller slot {} not found for stick event", data.session_slot);
} }
} }
Payload::ControllerTrigger(data) => { Payload::ControllerTrigger(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) { if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
let device = controller.controller.device(); let device = controller.controller.device();
if data.trigger == 0 { if data.trigger == 0 {
// Left trigger // Left trigger
@@ -233,11 +235,11 @@ async fn command_loop(
} }
device.sync(); device.sync();
} else { } else {
tracing::warn!("Controller slot {} not found for trigger event", data.slot); tracing::warn!("Controller slot {} not found for trigger event", data.session_slot);
} }
} }
Payload::ControllerAxis(data) => { Payload::ControllerAxis(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) { if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
let device = controller.controller.device(); let device = controller.controller.device();
if data.axis == 0 { if data.axis == 0 {
// dpad x // dpad x

View File

@@ -24,7 +24,7 @@ use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::filter::LevelFilter;
// Handles gathering GPU information and selecting the most suitable GPU // Handles gathering GPU information and selecting the most suitable GPU
fn handle_gpus(args: &args::Args) -> Result<Vec<gpu::GPUInfo>, Box<dyn Error>> { fn handle_gpus(args: &args::Args) -> Result<Vec<GPUInfo>, Box<dyn Error>> {
tracing::info!("Gathering GPU information.."); tracing::info!("Gathering GPU information..");
let mut gpus = gpu::get_gpus()?; let mut gpus = gpu::get_gpus()?;
if gpus.is_empty() { if gpus.is_empty() {
@@ -119,7 +119,6 @@ fn handle_encoder_video(
&video_encoders, &video_encoders,
&args.encoding.video.codec, &args.encoding.video.codec,
&args.encoding.video.encoder_type, &args.encoding.video.encoder_type,
args.app.zero_copy,
)?; )?;
} }
tracing::info!("Selected video encoder: '{}'", video_encoder.name); tracing::info!("Selected video encoder: '{}'", video_encoder.name);
@@ -323,7 +322,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
/* Video */ /* Video */
// Video Source Element // Video Source Element
let video_source = Arc::new(gstreamer::ElementFactory::make("waylanddisplaysrc").build()?); let video_source = Arc::new(gstreamer::ElementFactory::make("waylanddisplaysrc").build()?);
if let Some(gpu_info) = &video_encoder_info.gpu_info { if args.app.software_render {
video_source.set_property_from_str("render-node", "software");
} else if let Some(gpu_info) = &video_encoder_info.gpu_info {
video_source.set_property_from_str("render-node", gpu_info.render_path()); video_source.set_property_from_str("render-node", gpu_info.render_path());
} }
@@ -428,20 +429,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
webrtcsink.set_property("do-retransmission", false); webrtcsink.set_property("do-retransmission", false);
/* Queues */ /* Queues */
let video_source_queue = gstreamer::ElementFactory::make("queue")
.property("max-size-buffers", 5u32)
.build()?;
let audio_source_queue = gstreamer::ElementFactory::make("queue")
.property("max-size-buffers", 5u32)
.build()?;
let video_queue = gstreamer::ElementFactory::make("queue") let video_queue = gstreamer::ElementFactory::make("queue")
.property("max-size-buffers", 5u32) .property("max-size-buffers", 2u32)
.property("max-size-time", 0u64)
.property("max-size-bytes", 0u32)
.build()?; .build()?;
let audio_queue = gstreamer::ElementFactory::make("queue") let audio_queue = gstreamer::ElementFactory::make("queue")
.property("max-size-buffers", 5u32) .property("max-size-buffers", 2u32)
.property("max-size-time", 0u64)
.property("max-size-bytes", 0u32)
.build()?; .build()?;
/* Clock Sync */ /* Clock Sync */
@@ -460,7 +457,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
&caps_filter, &caps_filter,
&video_queue, &video_queue,
&video_clocksync, &video_clocksync,
&video_source_queue,
&video_source, &video_source,
&audio_encoder, &audio_encoder,
&audio_capsfilter, &audio_capsfilter,
@@ -468,7 +464,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
&audio_clocksync, &audio_clocksync,
&audio_rate, &audio_rate,
&audio_converter, &audio_converter,
&audio_source_queue,
&audio_source, &audio_source,
])?; ])?;
@@ -495,7 +490,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Link main audio branch // Link main audio branch
gstreamer::Element::link_many(&[ gstreamer::Element::link_many(&[
&audio_source, &audio_source,
&audio_source_queue,
&audio_converter, &audio_converter,
&audio_rate, &audio_rate,
&audio_capsfilter, &audio_capsfilter,
@@ -517,7 +511,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) { if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) {
gstreamer::Element::link_many(&[ gstreamer::Element::link_many(&[
&video_source, &video_source,
&video_source_queue,
&caps_filter, &caps_filter,
&video_queue, &video_queue,
&video_clocksync, &video_clocksync,
@@ -529,7 +522,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
// NVENC pipeline // NVENC pipeline
gstreamer::Element::link_many(&[ gstreamer::Element::link_many(&[
&video_source, &video_source,
&video_source_queue,
&caps_filter, &caps_filter,
&video_encoder, &video_encoder,
])?; ])?;
@@ -537,7 +529,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
} else { } else {
gstreamer::Element::link_many(&[ gstreamer::Element::link_many(&[
&video_source, &video_source,
&video_source_queue,
&caps_filter, &caps_filter,
&video_queue, &video_queue,
&video_clocksync, &video_clocksync,

View File

@@ -23,7 +23,7 @@ pub struct Signaller {
wayland_src: PLRwLock<Option<Arc<gstreamer::Element>>>, wayland_src: PLRwLock<Option<Arc<gstreamer::Element>>>,
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, String)>>>,
attach_rx: Mutex<Option<mpsc::Receiver<ProtoControllerAttach>>>, attach_rx: Mutex<Option<mpsc::Receiver<ProtoControllerAttach>>>,
} }
impl Default for Signaller { impl Default for Signaller {
@@ -70,11 +70,11 @@ impl Signaller {
self.controller_manager.read().clone() self.controller_manager.read().clone()
} }
pub async fn set_rumble_rx(&self, rumble_rx: mpsc::Receiver<(u32, u16, u16, u16)>) { pub async fn set_rumble_rx(&self, rumble_rx: mpsc::Receiver<(u32, u16, u16, u16, String)>) {
*self.rumble_rx.lock().await = Some(rumble_rx); *self.rumble_rx.lock().await = Some(rumble_rx);
} }
pub async fn take_rumble_rx(&self) -> Option<mpsc::Receiver<(u32, u16, u16, u16)>> { pub async fn take_rumble_rx(&self) -> Option<mpsc::Receiver<(u32, u16, u16, u16, String)>> {
self.rumble_rx.lock().await.take() self.rumble_rx.lock().await.take()
} }
@@ -382,7 +382,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, String)>>, // (slot, strong, weak, duration_ms, session_id)
attach_rx: Option<mpsc::Receiver<ProtoControllerAttach>>, 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,
@@ -423,10 +423,11 @@ fn setup_data_channel(
if let Some(mut rumble_rx) = rumble_rx { if let Some(mut rumble_rx) = rumble_rx {
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, session_id)) = rumble_rx.recv().await {
let rumble_msg = crate::proto::create_message( let rumble_msg = crate::proto::create_message(
Payload::ControllerRumble(ProtoControllerRumble { Payload::ControllerRumble(ProtoControllerRumble {
slot: slot as i32, session_slot: slot as i32,
session_id: session_id,
low_frequency: weak as i32, low_frequency: weak as i32,
high_frequency: strong as i32, high_frequency: strong as i32,
duration: duration_ms as i32, duration: duration_ms as i32,

View File

@@ -18,7 +18,7 @@ impl NestriSignaller {
nestri_conn: NestriConnection, nestri_conn: NestriConnection,
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, String)>>,
attach_rx: Option<mpsc::Receiver<crate::proto::proto::ProtoControllerAttach>>, 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();

View File

@@ -84,95 +84,113 @@ pub struct ProtoControllerAttach {
/// One of the following enums: "ps", "xbox" or "switch" /// One of the following enums: "ps", "xbox" or "switch"
#[prost(string, tag="1")] #[prost(string, tag="1")]
pub id: ::prost::alloc::string::String, pub id: ::prost::alloc::string::String,
/// Slot number (0-3) /// Session specific slot number (0-3)
#[prost(int32, tag="2")] #[prost(int32, tag="2")]
pub slot: i32, pub session_slot: i32,
/// Session ID of the client attaching the controller /// Session ID of the client
#[prost(string, tag="3")] #[prost(string, tag="3")]
pub session_id: ::prost::alloc::string::String, 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, Copy, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerDetach { pub struct ProtoControllerDetach {
/// Slot number (0-3) /// Session specific slot number (0-3)
#[prost(int32, tag="1")] #[prost(int32, tag="1")]
pub slot: i32, pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
} }
/// ControllerButton message /// ControllerButton message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerButton { pub struct ProtoControllerButton {
/// Slot number (0-3) /// Session specific slot number (0-3)
#[prost(int32, tag="1")] #[prost(int32, tag="1")]
pub slot: i32, pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// Button code (linux input event code) /// Button code (linux input event code)
#[prost(int32, tag="2")] #[prost(int32, tag="3")]
pub button: i32, pub button: i32,
/// true if pressed, false if released /// true if pressed, false if released
#[prost(bool, tag="3")] #[prost(bool, tag="4")]
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, Copy, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerTrigger { pub struct ProtoControllerTrigger {
/// Slot number (0-3) /// Session specific slot number (0-3)
#[prost(int32, tag="1")] #[prost(int32, tag="1")]
pub slot: i32, pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// Trigger number (0 for left, 1 for right) /// Trigger number (0 for left, 1 for right)
#[prost(int32, tag="2")] #[prost(int32, tag="3")]
pub trigger: i32, pub trigger: i32,
/// trigger value (-32768 to 32767) /// trigger value (-32768 to 32767)
#[prost(int32, tag="3")] #[prost(int32, tag="4")]
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, Copy, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerStick { pub struct ProtoControllerStick {
/// Slot number (0-3) /// Session specific slot number (0-3)
#[prost(int32, tag="1")] #[prost(int32, tag="1")]
pub slot: i32, pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// Stick number (0 for left, 1 for right) /// Stick number (0 for left, 1 for right)
#[prost(int32, tag="2")] #[prost(int32, tag="3")]
pub stick: i32, pub stick: i32,
/// X axis value (-32768 to 32767) /// X axis value (-32768 to 32767)
#[prost(int32, tag="3")] #[prost(int32, tag="4")]
pub x: i32, pub x: i32,
/// Y axis value (-32768 to 32767) /// Y axis value (-32768 to 32767)
#[prost(int32, tag="4")] #[prost(int32, tag="5")]
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, Copy, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerAxis { pub struct ProtoControllerAxis {
/// Slot number (0-3) /// Session specific slot number (0-3)
#[prost(int32, tag="1")] #[prost(int32, tag="1")]
pub slot: i32, pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// 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="2")] #[prost(int32, tag="3")]
pub axis: i32, pub axis: i32,
/// axis value (-1 to 1) /// axis value (-1 to 1)
#[prost(int32, tag="3")] #[prost(int32, tag="4")]
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, Copy, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerRumble { pub struct ProtoControllerRumble {
/// Slot number (0-3) /// Session specific slot number (0-3)
#[prost(int32, tag="1")] #[prost(int32, tag="1")]
pub slot: i32, pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// Low frequency rumble (0-65535) /// Low frequency rumble (0-65535)
#[prost(int32, tag="2")] #[prost(int32, tag="3")]
pub low_frequency: i32, pub low_frequency: i32,
/// High frequency rumble (0-65535) /// High frequency rumble (0-65535)
#[prost(int32, tag="3")] #[prost(int32, tag="4")]
pub high_frequency: i32, pub high_frequency: i32,
/// Duration in milliseconds /// Duration in milliseconds
#[prost(int32, tag="4")] #[prost(int32, tag="5")]
pub duration: i32, pub duration: i32,
} }
// WebRTC + signaling // WebRTC + signaling

View File

@@ -51,50 +51,56 @@ message ProtoKeyUp {
// ControllerAttach message // ControllerAttach message
message ProtoControllerAttach { message ProtoControllerAttach {
string id = 1; // One of the following enums: "ps", "xbox" or "switch" string id = 1; // One of the following enums: "ps", "xbox" or "switch"
int32 slot = 2; // Slot number (0-3) int32 session_slot = 2; // Session specific slot number (0-3)
string session_id = 3; // Session ID of the client attaching the controller string session_id = 3; // Session ID of the client
} }
// ControllerDetach message // ControllerDetach message
message ProtoControllerDetach { message ProtoControllerDetach {
int32 slot = 1; // Slot number (0-3) int32 session_slot = 1; // Session specific slot number (0-3)
string session_id = 2; // Session ID of the client
} }
// ControllerButton message // ControllerButton message
message ProtoControllerButton { message ProtoControllerButton {
int32 slot = 1; // Slot number (0-3) int32 session_slot = 1; // Session specific slot number (0-3)
int32 button = 2; // Button code (linux input event code) string session_id = 2; // Session ID of the client
bool pressed = 3; // true if pressed, false if released int32 button = 3; // Button code (linux input event code)
bool pressed = 4; // true if pressed, false if released
} }
// ControllerTriggers message // ControllerTriggers message
message ProtoControllerTrigger { message ProtoControllerTrigger {
int32 slot = 1; // Slot number (0-3) int32 session_slot = 1; // Session specific slot number (0-3)
int32 trigger = 2; // Trigger number (0 for left, 1 for right) string session_id = 2; // Session ID of the client
int32 value = 3; // trigger value (-32768 to 32767) int32 trigger = 3; // Trigger number (0 for left, 1 for right)
int32 value = 4; // trigger value (-32768 to 32767)
} }
// ControllerSticks message // ControllerSticks message
message ProtoControllerStick { message ProtoControllerStick {
int32 slot = 1; // Slot number (0-3) int32 session_slot = 1; // Session specific slot number (0-3)
int32 stick = 2; // Stick number (0 for left, 1 for right) string session_id = 2; // Session ID of the client
int32 x = 3; // X axis value (-32768 to 32767) int32 stick = 3; // Stick number (0 for left, 1 for right)
int32 y = 4; // Y axis value (-32768 to 32767) int32 x = 4; // X axis value (-32768 to 32767)
int32 y = 5; // Y axis value (-32768 to 32767)
} }
// ControllerAxis message // ControllerAxis message
message ProtoControllerAxis { message ProtoControllerAxis {
int32 slot = 1; // Slot number (0-3) int32 session_slot = 1; // Session specific slot number (0-3)
int32 axis = 2; // Axis number (0 for d-pad horizontal, 1 for d-pad vertical) string session_id = 2; // Session ID of the client
int32 value = 3; // axis value (-1 to 1) int32 axis = 3; // Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
int32 value = 4; // axis value (-1 to 1)
} }
// ControllerRumble message // ControllerRumble message
message ProtoControllerRumble { message ProtoControllerRumble {
int32 slot = 1; // Slot number (0-3) int32 session_slot = 1; // Session specific slot number (0-3)
int32 low_frequency = 2; // Low frequency rumble (0-65535) string session_id = 2; // Session ID of the client
int32 high_frequency = 3; // High frequency rumble (0-65535) int32 low_frequency = 3; // Low frequency rumble (0-65535)
int32 duration = 4; // Duration in milliseconds int32 high_frequency = 4; // High frequency rumble (0-65535)
int32 duration = 5; // Duration in milliseconds
} }
/* WebRTC + signaling */ /* WebRTC + signaling */