mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-11 00:05:36 +02:00
feat: Controller support, performance enchancements, multi-stage images, fixes (#304)
## Description Oops.. another massive PR 🥲 This PR contains multiple improvements and changes. Firstly, thanks gst-wayland-display's PR [here](https://github.com/games-on-whales/gst-wayland-display/pull/20). NVIDIA path is now way more efficient than before. Secondly, adding controller support was a massive hurdle, requiring me to start another project [vimputti](https://github.com/DatCaptainHorse/vimputti) - which allows simple virtual controller inputs in isolated containers. Well, it's not simple, it includes LD_PRELOAD shims and other craziness, but the library API is simple to use.. Thirdly, split runner image into 3 separate stages, base + build + runtime, should help keep things in check in future, also added GitHub Actions CI builds for v2 to v4 builds (hopefully they pass..). Fourth, replaced the runner's runtime Steam patching with better and simpler bubblewrap patch, massive thanks to `games-on-whales` to figuring it out better! Fifth, relay for once needed some changes, the new changes are still mostly WIP, but I'll deal with them next time I have energy.. I'm spent now. Needed to include these changes as relay needed a minor change to allow rumble events to flow back to client peer. Sixth.. tons of package updates, minor code improvements and the usual. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * End-to-end gamepad/controller support (attach/detach, buttons, sticks, triggers, rumble) with client/server integration and virtual controller plumbing. * Optional Prometheus metrics endpoint and WebTransport support. * Background vimputti manager process added for controller handling. * **Improvements** * Multi-variant container image builds and streamlined runtime images. * Zero-copy video pipeline and encoder improvements for lower latency. * Updated Steam compat mapping and dependency/toolchain refreshes. * **Bug Fixes** * More robust GPU detection, input/fullscreen lifecycle, startup/entrypoint, and container runtime fixes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
a3ee9aadd9
commit
c62a22b552
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -3,13 +3,13 @@
|
||||
/apps/ @victorpahuus @AquaWolf
|
||||
/packages/ui/ @wanjohiryan @victorpahuus @AquaWolf
|
||||
|
||||
/protobuf/ @AquaWolf
|
||||
/protobufs/ @AquaWolf @DatCaptainHorse
|
||||
|
||||
/infra/ @wanjohiryan
|
||||
/packages/core/ @wanjohiryan
|
||||
/packages/functions/ @wanjohiryan
|
||||
|
||||
/containers/ @DatCaptainHorse
|
||||
/containerfiles/ @DatCaptainHorse
|
||||
/packages/server/ @DatCaptainHorse
|
||||
/packages/relay/ @DatCaptainHorse
|
||||
/packages/scripts/ @DatCaptainHorse
|
||||
|
||||
48
.github/workflows/docker-bake.hcl
vendored
Normal file
48
.github/workflows/docker-bake.hcl
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
variable "BASE_IMAGE" {
|
||||
default = "docker.io/cachyos/cachyos:latest"
|
||||
}
|
||||
|
||||
group "default" {
|
||||
targets = ["runner"]
|
||||
}
|
||||
|
||||
target "runner-base" {
|
||||
dockerfile = "containerfiles/runner-base.Containerfile"
|
||||
context = "."
|
||||
args = {
|
||||
BASE_IMAGE = "${BASE_IMAGE}"
|
||||
}
|
||||
cache-from = ["type=gha,scope=runner-base-pr"]
|
||||
cache-to = ["type=gha,scope=runner-base-pr,mode=max"]
|
||||
tags = ["runner-base:latest"]
|
||||
}
|
||||
|
||||
target "runner-builder" {
|
||||
dockerfile = "containerfiles/runner-builder.Containerfile"
|
||||
context = "."
|
||||
args = {
|
||||
RUNNER_BASE_IMAGE = "runner-base:latest"
|
||||
}
|
||||
cache-from = ["type=gha,scope=runner-builder-pr"]
|
||||
cache-to = ["type=gha,scope=runner-builder-pr,mode=max"]
|
||||
tags = ["runner-builder:latest"]
|
||||
contexts = {
|
||||
runner-base = "target:runner-base"
|
||||
}
|
||||
}
|
||||
|
||||
target "runner" {
|
||||
dockerfile = "containerfiles/runner.Containerfile"
|
||||
context = "."
|
||||
args = {
|
||||
RUNNER_BASE_IMAGE = "runner-base:latest"
|
||||
RUNNER_BUILDER_IMAGE = "runner-builder:latest"
|
||||
}
|
||||
cache-from = ["type=gha,scope=runner-pr"]
|
||||
cache-to = ["type=gha,scope=runner-pr,mode=max"]
|
||||
tags = ["nestri-runner"]
|
||||
contexts = {
|
||||
runner-base = "target:runner-base"
|
||||
runner-builder = "target:runner-builder"
|
||||
}
|
||||
}
|
||||
101
.github/workflows/runner.yml
vendored
101
.github/workflows/runner.yml
vendored
@@ -1,11 +1,11 @@
|
||||
#Tabs not spaces, you moron :)
|
||||
|
||||
name: Build nestri:runner
|
||||
name: Build nestri-runner
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "containerfiles/runner.Containerfile"
|
||||
- "containerfiles/runner*.Containerfile"
|
||||
- "packages/scripts/**"
|
||||
- "packages/server/**"
|
||||
- ".github/workflows/runner.yml"
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
push:
|
||||
branches: [dev, production]
|
||||
paths:
|
||||
- "containerfiles/runner.Containerfile"
|
||||
- "containerfiles/runner*.Containerfile"
|
||||
- ".github/workflows/runner.yml"
|
||||
- "packages/scripts/**"
|
||||
- "packages/server/**"
|
||||
@@ -26,7 +26,6 @@ on:
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: nestrilabs/nestri
|
||||
BASE_TAG_PREFIX: runner
|
||||
BASE_IMAGE: docker.io/cachyos/cachyos:latest
|
||||
|
||||
# This makes our release ci quit prematurely
|
||||
@@ -36,43 +35,46 @@ env:
|
||||
|
||||
jobs:
|
||||
build-docker-pr:
|
||||
name: Build image on PR
|
||||
name: Build images on PR
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
-
|
||||
name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 20
|
||||
-
|
||||
name: Build Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
-
|
||||
name: Build images using bake
|
||||
uses: docker/bake-action@v6
|
||||
env:
|
||||
BASE_IMAGE: ${{ env.BASE_IMAGE }}
|
||||
with:
|
||||
file: containerfiles/runner.Containerfile
|
||||
context: ./
|
||||
files: |
|
||||
./.github/workflows/docker-bake.hcl
|
||||
targets: runner
|
||||
push: false
|
||||
load: true
|
||||
tags: nestri:runner
|
||||
cache-from: type=gha,mode=max
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-and-push-docker:
|
||||
name: Build and push image
|
||||
name: Build and push images
|
||||
if: ${{ github.ref == 'refs/heads/production' || github.ref == 'refs/heads/dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- { suffix: "", base: "docker.io/cachyos/cachyos:latest" }
|
||||
- { suffix: "-v3", base: "docker.io/cachyos/cachyos-v3:latest" }
|
||||
- { suffix: "-v4", base: "docker.io/cachyos/cachyos-v4:latest" }
|
||||
steps:
|
||||
-
|
||||
name: Checkout repo
|
||||
@@ -85,21 +87,19 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
-
|
||||
name: Extract Container metadata
|
||||
id: meta
|
||||
name: Extract runner metadata
|
||||
id: meta-runner
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.BASE_TAG_PREFIX }}
|
||||
#
|
||||
#tag on release, and a nightly build for 'dev'
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner
|
||||
tags: |
|
||||
type=raw,value=nightly,enable={{is_default_branch}}
|
||||
type=raw,value={{branch}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'production') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
-
|
||||
type=raw,value=nightly${{ matrix.variant.suffix }},enable={{is_default_branch}}
|
||||
type=raw,value={{branch}}${{ matrix.variant.suffix }}
|
||||
type=raw,value=latest${{ matrix.variant.suffix }},enable=${{ github.ref == format('refs/heads/{0}', 'production') }}
|
||||
type=semver,pattern={{version}}${{ matrix.variant.suffix }}
|
||||
type=semver,pattern={{major}}.{{minor}}${{ matrix.variant.suffix }}
|
||||
type=semver,pattern={{major}}${{ matrix.variant.suffix }}
|
||||
-
|
||||
name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
@@ -108,14 +108,41 @@ jobs:
|
||||
with:
|
||||
swap-size-gb: 20
|
||||
-
|
||||
name: Build Docker image
|
||||
name: Build and push runner-base image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner.Containerfile
|
||||
file: containerfiles/runner-base.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,mode=max
|
||||
cache-to: type=gha,mode=max
|
||||
pull: ${{ github.event_name == 'schedule' }} # Pull base image for scheduled builds
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }}
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ matrix.variant.base }}
|
||||
cache-from: type=gha,scope=runner-base${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner-base${{ matrix.variant.suffix }},mode=max
|
||||
pull: ${{ github.event_name == 'schedule' }}
|
||||
-
|
||||
name: Build and push runner-builder image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner-builder.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest${{ matrix.variant.suffix }}
|
||||
build-args: |
|
||||
RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }}
|
||||
cache-from: type=gha,scope=runner-builder${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner-builder${{ matrix.variant.suffix }},mode=max
|
||||
-
|
||||
name: Build and push runner image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ steps.meta-runner.outputs.tags }}
|
||||
labels: ${{ steps.meta-runner.outputs.labels }}
|
||||
build-args: |
|
||||
RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }}
|
||||
RUNNER_BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest${{ matrix.variant.suffix }}
|
||||
cache-from: type=gha,scope=runner${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner${{ matrix.variant.suffix }},mode=max
|
||||
|
||||
@@ -3,7 +3,6 @@ FROM docker.io/node:24-alpine AS base
|
||||
FROM base AS build
|
||||
WORKDIR /usr/src/app
|
||||
COPY package.json ./
|
||||
COPY patches ./patches
|
||||
COPY packages/input ./packages/input
|
||||
COPY packages/play-standalone ./packages/play-standalone
|
||||
RUN cd packages/play-standalone && npm install && npm run build
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM docker.io/golang:1.24-alpine AS go-build
|
||||
FROM docker.io/golang:1.25-alpine AS go-build
|
||||
WORKDIR /builder
|
||||
COPY packages/relay/ /builder/
|
||||
RUN go build
|
||||
|
||||
FROM docker.io/golang:1.24-alpine
|
||||
FROM docker.io/golang:1.25-alpine
|
||||
COPY --from=go-build /builder/relay /relay/relay
|
||||
WORKDIR /relay
|
||||
|
||||
@@ -22,8 +22,4 @@ ENV WEBRTC_NAT_IPS=""
|
||||
ENV AUTO_ADD_LOCAL_IP=true
|
||||
ENV PERSIST_DIR="./persist-data"
|
||||
|
||||
EXPOSE $ENDPOINT_PORT
|
||||
EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp
|
||||
EXPOSE $WEBRTC_UDP_MUX/udp
|
||||
|
||||
ENTRYPOINT ["/relay/relay"]
|
||||
13
containerfiles/runner-base.Containerfile
Normal file
13
containerfiles/runner-base.Containerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
# Container build arguments #
|
||||
ARG BASE_IMAGE=docker.io/cachyos/cachyos:latest
|
||||
|
||||
#*******************************************#
|
||||
# Base Stage - Simple with light essentials #
|
||||
#*******************************************#
|
||||
FROM ${BASE_IMAGE} AS bases
|
||||
|
||||
# Only lightweight stuff needed by both builder and runtime
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm \
|
||||
libssh2 curl wget libevdev libc++abi \
|
||||
gstreamer gst-plugins-base
|
||||
218
containerfiles/runner-builder.Containerfile
Normal file
218
containerfiles/runner-builder.Containerfile
Normal file
@@ -0,0 +1,218 @@
|
||||
# Container build arguments #
|
||||
ARG RUNNER_BASE_IMAGE=runner-base:latest
|
||||
|
||||
#**************#
|
||||
# builder base #
|
||||
#**************#
|
||||
FROM ${RUNNER_BASE_IMAGE} AS base-builder
|
||||
|
||||
ENV ARTIFACTS=/artifacts
|
||||
RUN mkdir -p "${ARTIFACTS}"
|
||||
|
||||
# Environment setup for Rust and Cargo
|
||||
ENV CARGO_HOME=/usr/local/cargo \
|
||||
PATH="${CARGO_HOME}/bin:${PATH}"
|
||||
|
||||
# Install build essentials and caching tools
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm rustup git base-devel mold \
|
||||
meson pkgconf cmake git gcc make
|
||||
|
||||
# Override various linker with symlink so mold is forcefully used (ld, ld.lld, lld)
|
||||
RUN ln -sf /usr/bin/mold /usr/bin/ld && \
|
||||
ln -sf /usr/bin/mold /usr/bin/ld.lld && \
|
||||
ln -sf /usr/bin/mold /usr/bin/lld
|
||||
|
||||
# Install latest Rust using rustup
|
||||
RUN rustup default stable
|
||||
|
||||
# Install cargo-chef with proper caching
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo install -j $(nproc) cargo-chef --locked
|
||||
|
||||
#*******************************#
|
||||
# vimputti manager build stages #
|
||||
#*******************************#
|
||||
FROM base-builder AS vimputti-manager-deps
|
||||
WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm lib32-gcc-libs
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 --rev "9e8bfd0217eeab011c5afc368d3ea67a4c239e81" https://github.com/DatCaptainHorse/vimputti.git
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM vimputti-manager-deps AS vimputti-manager-planner
|
||||
WORKDIR /builder/vimputti
|
||||
|
||||
# Prepare recipe for dependency caching
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM vimputti-manager-deps AS vimputti-manager-cached-builder
|
||||
WORKDIR /builder/vimputti
|
||||
|
||||
COPY --from=vimputti-manager-planner /builder/vimputti/recipe.json .
|
||||
|
||||
# Cache dependencies using cargo-chef
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
ENV CARGO_TARGET_DIR=/builder/target
|
||||
COPY --from=vimputti-manager-planner /builder/vimputti/ .
|
||||
|
||||
# Build and install directly to artifacts
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
--mount=type=cache,target=/builder/target \
|
||||
cargo build --release --package vimputti-manager && \
|
||||
cargo build --release --package vimputti-shim && \
|
||||
rustup target add i686-unknown-linux-gnu && \
|
||||
cargo build --release --package vimputti-shim --target i686-unknown-linux-gnu && \
|
||||
cp "${CARGO_TARGET_DIR}/release/vimputti-manager" "${ARTIFACTS}" && \
|
||||
cp "${CARGO_TARGET_DIR}/release/libvimputti_shim.so" "${ARTIFACTS}/libvimputti_shim_64.so" && \
|
||||
cp "${CARGO_TARGET_DIR}/i686-unknown-linux-gnu/release/libvimputti_shim.so" "${ARTIFACTS}/libvimputti_shim_32.so"
|
||||
|
||||
#****************************#
|
||||
# nestri-server build stages #
|
||||
#****************************#
|
||||
FROM base-builder AS nestri-server-deps
|
||||
WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm gst-plugins-good gst-plugin-rswebrtc
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM nestri-server-deps AS nestri-server-planner
|
||||
WORKDIR /builder/nestri
|
||||
|
||||
COPY packages/server/Cargo.toml packages/server/Cargo.lock ./
|
||||
|
||||
# Prepare recipe for dependency caching
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM nestri-server-deps AS nestri-server-cached-builder
|
||||
WORKDIR /builder/nestri
|
||||
|
||||
COPY --from=nestri-server-planner /builder/nestri/recipe.json .
|
||||
|
||||
# Cache dependencies using cargo-chef
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
|
||||
ENV CARGO_TARGET_DIR=/builder/target
|
||||
COPY packages/server/ ./
|
||||
|
||||
# Build and install directly to artifacts
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
--mount=type=cache,target=/builder/target \
|
||||
cargo build --release && \
|
||||
cp "${CARGO_TARGET_DIR}/release/nestri-server" "${ARTIFACTS}"
|
||||
|
||||
#**********************************#
|
||||
# gst-wayland-display build stages #
|
||||
#**********************************#
|
||||
FROM base-builder AS gst-wayland-deps
|
||||
WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm libxkbcommon wayland \
|
||||
gst-plugins-good gst-plugins-bad libinput
|
||||
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo install cargo-c
|
||||
|
||||
# Grab cudart from NVIDIA..
|
||||
RUN wget https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-13.0.96-archive.tar.xz -O cuda_cudart.tar.xz && \
|
||||
mkdir cuda_cudart && tar -xf cuda_cudart.tar.xz -C cuda_cudart --strip-components=1 && \
|
||||
cp cuda_cudart/lib/libcudart.so cuda_cudart/lib/libcudart.so.* /usr/lib/ && \
|
||||
rm -r cuda_cudart && \
|
||||
rm cuda_cudart.tar.xz
|
||||
|
||||
# Grab cuda lib from NVIDIA (it's in driver package of all things..)
|
||||
RUN wget https://developer.download.nvidia.com/compute/cuda/redist/nvidia_driver/linux-x86_64/nvidia_driver-linux-x86_64-580.95.05-archive.tar.xz -O nvidia_driver.tar.xz && \
|
||||
mkdir nvidia_driver && tar -xf nvidia_driver.tar.xz -C nvidia_driver --strip-components=1 && \
|
||||
cp nvidia_driver/lib/libcuda.so.* /usr/lib/libcuda.so && \
|
||||
ln -s /usr/lib/libcuda.so /usr/lib/libcuda.so.1 && \
|
||||
rm -r nvidia_driver && \
|
||||
rm nvidia_driver.tar.xz
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 --rev "afa853fa03e8403c83bbb3bc0cf39147ad46c266" https://github.com/games-on-whales/gst-wayland-display.git
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM gst-wayland-deps AS gst-wayland-planner
|
||||
WORKDIR /builder/gst-wayland-display
|
||||
|
||||
# Prepare recipe for dependency caching
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM gst-wayland-deps AS gst-wayland-cached-builder
|
||||
WORKDIR /builder/gst-wayland-display
|
||||
|
||||
COPY --from=gst-wayland-planner /builder/gst-wayland-display/recipe.json .
|
||||
|
||||
# Cache dependencies using cargo-chef
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
|
||||
ENV CARGO_TARGET_DIR=/builder/target
|
||||
|
||||
COPY --from=gst-wayland-planner /builder/gst-wayland-display/ .
|
||||
|
||||
# Build and install directly to artifacts
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
--mount=type=cache,target=/builder/target \
|
||||
cargo cinstall --prefix=${ARTIFACTS} --release
|
||||
|
||||
#*********************************#
|
||||
# Patched bubblewrap build stages #
|
||||
#*********************************#
|
||||
FROM base-builder AS bubblewrap-deps
|
||||
WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm libtool libcap libselinux
|
||||
|
||||
# Copy patch file from host
|
||||
COPY packages/patches/bubblewrap/ /builder/patches/
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 --rev "9ca3b05ec787acfb4b17bed37db5719fa777834f" https://github.com/containers/bubblewrap.git && \
|
||||
cd bubblewrap && \
|
||||
# Apply patch to fix user namespace issue
|
||||
git apply ../patches/bubbleunheck.patch
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM bubblewrap-deps AS bubblewrap-builder
|
||||
WORKDIR /builder/bubblewrap
|
||||
|
||||
# Build and install directly to artifacts
|
||||
RUN meson setup build --prefix=${ARTIFACTS} && \
|
||||
meson compile -C build && \
|
||||
meson install -C build
|
||||
|
||||
#*********************************************#
|
||||
# Final Export Stage - Collects all artifacts #
|
||||
#*********************************************#
|
||||
FROM scratch AS artifacts
|
||||
|
||||
COPY --from=nestri-server-cached-builder /artifacts/nestri-server /artifacts/bin/
|
||||
COPY --from=gst-wayland-cached-builder /artifacts/lib/ /artifacts/lib/
|
||||
COPY --from=gst-wayland-cached-builder /artifacts/include/ /artifacts/include/
|
||||
COPY --from=vimputti-manager-cached-builder /artifacts/vimputti-manager /artifacts/bin/
|
||||
COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_64.so /artifacts/lib64/libvimputti_shim.so
|
||||
COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_32.so /artifacts/lib32/libvimputti_shim.so
|
||||
COPY --from=gst-wayland-deps /usr/lib/libcuda.so /usr/lib/libcuda.so.* /artifacts/lib/
|
||||
COPY --from=bubblewrap-builder /artifacts/bin/bwrap /artifacts/bin/
|
||||
@@ -1,125 +1,13 @@
|
||||
# Container build arguments #
|
||||
ARG BASE_IMAGE=docker.io/cachyos/cachyos:latest
|
||||
ARG RUNNER_BASE_IMAGE=runner-base:latest
|
||||
ARG RUNNER_BUILDER_IMAGE=runner-builder:latest
|
||||
|
||||
#******************************************************************************
|
||||
# Base Stage - Updates system packages
|
||||
#******************************************************************************
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman --noconfirm -Syu
|
||||
|
||||
#******************************************************************************
|
||||
# Base Builder Stage - Prepares core build environment
|
||||
#******************************************************************************
|
||||
FROM base AS base-builder
|
||||
|
||||
# Environment setup for Rust and Cargo
|
||||
ENV CARGO_HOME=/usr/local/cargo \
|
||||
ARTIFACTS=/artifacts \
|
||||
PATH="${CARGO_HOME}/bin:${PATH}" \
|
||||
RUSTFLAGS="-C link-arg=-fuse-ld=mold"
|
||||
|
||||
# Install build essentials and caching tools
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm mold rustup && \
|
||||
mkdir -p "${ARTIFACTS}"
|
||||
|
||||
# Install latest Rust using rustup
|
||||
RUN rustup default stable
|
||||
|
||||
# Install cargo-chef with proper caching
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo install -j $(nproc) cargo-chef cargo-c --locked
|
||||
|
||||
#******************************************************************************
|
||||
# Nestri Server Build Stages
|
||||
#******************************************************************************
|
||||
FROM base-builder AS nestri-server-deps
|
||||
WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm meson pkgconf cmake git gcc make \
|
||||
gstreamer gst-plugins-base gst-plugins-good gst-plugin-rswebrtc
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM nestri-server-deps AS nestri-server-planner
|
||||
WORKDIR /builder/nestri
|
||||
|
||||
COPY packages/server/Cargo.toml packages/server/Cargo.lock ./
|
||||
|
||||
# Prepare recipe for dependency caching
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM nestri-server-deps AS nestri-server-cached-builder
|
||||
WORKDIR /builder/nestri
|
||||
|
||||
COPY --from=nestri-server-planner /builder/nestri/recipe.json .
|
||||
|
||||
# Cache dependencies using cargo-chef
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
|
||||
ENV CARGO_TARGET_DIR=/builder/target
|
||||
|
||||
COPY packages/server/ ./
|
||||
|
||||
# Build and install directly to artifacts
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
--mount=type=cache,target=/builder/target \
|
||||
cargo build --release && \
|
||||
cp "${CARGO_TARGET_DIR}/release/nestri-server" "${ARTIFACTS}"
|
||||
|
||||
#******************************************************************************
|
||||
# GST-Wayland Plugin Build Stages
|
||||
#******************************************************************************
|
||||
FROM base-builder AS gst-wayland-deps
|
||||
WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --noconfirm meson pkgconf cmake git gcc make \
|
||||
libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 --rev "dfeebb19b48f32207469e166a3955f5d65b5e6c6" https://github.com/games-on-whales/gst-wayland-display.git
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM gst-wayland-deps AS gst-wayland-planner
|
||||
WORKDIR /builder/gst-wayland-display
|
||||
|
||||
# Prepare recipe for dependency caching
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM gst-wayland-deps AS gst-wayland-cached-builder
|
||||
WORKDIR /builder/gst-wayland-display
|
||||
|
||||
COPY --from=gst-wayland-planner /builder/gst-wayland-display/recipe.json .
|
||||
|
||||
# Cache dependencies using cargo-chef
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
|
||||
ENV CARGO_TARGET_DIR=/builder/target
|
||||
|
||||
COPY --from=gst-wayland-planner /builder/gst-wayland-display/ .
|
||||
|
||||
# Build and install directly to artifacts
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
--mount=type=cache,target=/builder/target \
|
||||
cargo cinstall --prefix=${ARTIFACTS} --release
|
||||
|
||||
#******************************************************************************
|
||||
# Final Runtime Stage
|
||||
#******************************************************************************
|
||||
FROM base AS runtime
|
||||
#*********************#
|
||||
# Final Runtime Stage #
|
||||
#*********************#
|
||||
FROM ${RUNNER_BASE_IMAGE} AS runtime
|
||||
FROM ${RUNNER_BUILDER_IMAGE} AS builder
|
||||
FROM runtime
|
||||
|
||||
### Package Installation ###
|
||||
# Core system components
|
||||
@@ -127,20 +15,17 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --needed --noconfirm \
|
||||
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
|
||||
vulkan-radeon lib32-vulkan-radeon \
|
||||
mesa steam-native-runtime proton-cachyos lib32-mesa \
|
||||
mesa lib32-mesa \
|
||||
steam gtk3 lib32-gtk3 \
|
||||
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
|
||||
libssh2 curl wget \
|
||||
pipewire pipewire-pulse pipewire-alsa wireplumber \
|
||||
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib \
|
||||
noto-fonts-cjk supervisor jq pacman-contrib \
|
||||
hwdata openssh \
|
||||
# GStreamer stack
|
||||
gstreamer gst-plugins-base gst-plugins-good \
|
||||
gst-plugins-good \
|
||||
gst-plugins-bad gst-plugin-pipewire \
|
||||
gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \
|
||||
gst-plugin-va gst-plugin-qsv \
|
||||
# lib32 GStreamer stack to fix some games with videos
|
||||
lib32-gstreamer lib32-gst-plugins-base lib32-gst-plugins-good && \
|
||||
gst-plugin-va gst-plugin-qsv && \
|
||||
# Cleanup
|
||||
paccache -rk1 && \
|
||||
rm -rf /usr/share/{info,man,doc}/*
|
||||
@@ -153,6 +38,7 @@ ENV NESTRI_USER="nestri" \
|
||||
NESTRI_LANG=en_US.UTF-8 \
|
||||
NESTRI_XDG_RUNTIME_DIR=/run/user/1000 \
|
||||
NESTRI_HOME=/home/nestri \
|
||||
NESTRI_VIMPUTTI_PATH=/tmp/vimputti-1000 \
|
||||
NVIDIA_DRIVER_CAPABILITIES=all
|
||||
|
||||
RUN mkdir -p "/home/${NESTRI_USER}" && \
|
||||
@@ -174,29 +60,33 @@ RUN mkdir -p /run/dbus && \
|
||||
-e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \
|
||||
/usr/share/wireplumber/wireplumber.conf
|
||||
|
||||
### Audio Systems Configs - Latency optimizations + Loopback ###
|
||||
## Audio Systems Configs - Latency optimizations + Loopback ##
|
||||
RUN mkdir -p /etc/pipewire/pipewire.conf.d && \
|
||||
mkdir -p /etc/wireplumber/wireplumber.conf.d
|
||||
|
||||
COPY packages/configs/wireplumber.conf.d/* /etc/wireplumber/wireplumber.conf.d/
|
||||
COPY packages/configs/pipewire.conf.d/* /etc/pipewire/pipewire.conf.d/
|
||||
|
||||
## Steam Configs - Proton (CachyOS flavor) ##
|
||||
## Steam Configs - Proton (Experimental flavor) ##
|
||||
RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config"
|
||||
|
||||
COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/"
|
||||
|
||||
### Artifacts and Verification ###
|
||||
COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/
|
||||
COPY --from=gst-wayland-cached-builder /artifacts/lib/ /usr/lib/
|
||||
COPY --from=gst-wayland-cached-builder /artifacts/include/ /usr/include/
|
||||
RUN which nestri-server && ls -la /usr/lib/ | grep 'gstwaylanddisplay'
|
||||
### Artifacts from Builder ###
|
||||
COPY --from=builder /artifacts/bin/nestri-server /usr/bin/
|
||||
COPY --from=builder /artifacts/bin/bwrap /usr/bin/
|
||||
COPY --from=builder /artifacts/lib/ /usr/lib/
|
||||
COPY --from=builder /artifacts/lib32/ /usr/lib32/
|
||||
COPY --from=builder /artifacts/lib64/ /usr/lib64/
|
||||
COPY --from=builder /artifacts/bin/vimputti-manager /usr/bin/
|
||||
|
||||
### Scripts and Final Configuration ###
|
||||
COPY packages/scripts/ /etc/nestri/
|
||||
RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \
|
||||
chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" && \
|
||||
sed -i 's/^#\(en_US\.UTF-8\)/\1/' /etc/locale.gen && \
|
||||
setcap cap_net_admin+ep /usr/bin/vimputti-manager && \
|
||||
dbus-uuidgen > /etc/machine-id && \
|
||||
LANG=en_US.UTF-8 locale-gen
|
||||
|
||||
# Root for most container engines, nestri-user compatible for apptainer without fakeroot
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{
|
||||
"0"
|
||||
{
|
||||
"name" "proton-cachyos"
|
||||
"name" "proton_experimental"
|
||||
"config" ""
|
||||
"priority" "75"
|
||||
}
|
||||
|
||||
@@ -7,21 +7,23 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/buf": "^1.50.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.2.3"
|
||||
"@bufbuild/buf": "^1.57.2",
|
||||
"@bufbuild/protoc-gen-es": "^2.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.3",
|
||||
"@chainsafe/libp2p-noise": "^16.1.3",
|
||||
"@chainsafe/libp2p-yamux": "^7.0.1",
|
||||
"@libp2p/identify": "^3.0.32",
|
||||
"@libp2p/interface": "^2.10.2",
|
||||
"@libp2p/ping": "^2.0.32",
|
||||
"@libp2p/websockets": "^9.2.13",
|
||||
"@multiformats/multiaddr": "^12.4.0",
|
||||
"@bufbuild/protobuf": "^2.9.0",
|
||||
"@chainsafe/libp2p-noise": "^16.1.4",
|
||||
"@chainsafe/libp2p-quic": "^1.1.3",
|
||||
"@chainsafe/libp2p-yamux": "^7.0.4",
|
||||
"@libp2p/identify": "^3.0.39",
|
||||
"@libp2p/interface": "^2.11.0",
|
||||
"@libp2p/ping": "^2.0.37",
|
||||
"@libp2p/websockets": "^9.2.19",
|
||||
"@libp2p/webtransport": "^5.0.51",
|
||||
"@multiformats/multiaddr": "^12.5.1",
|
||||
"it-length-prefixed": "^10.0.1",
|
||||
"it-pipe": "^3.0.1",
|
||||
"libp2p": "^2.8.8",
|
||||
"libp2p": "^2.10.0",
|
||||
"uint8arraylist": "^2.4.8",
|
||||
"uint8arrays": "^5.1.0"
|
||||
}
|
||||
|
||||
@@ -1,113 +1,133 @@
|
||||
export const keyCodeToLinuxEventCode: { [key: string]: number } = {
|
||||
'KeyA': 30,
|
||||
'KeyB': 48,
|
||||
'KeyC': 46,
|
||||
'KeyD': 32,
|
||||
'KeyE': 18,
|
||||
'KeyF': 33,
|
||||
'KeyG': 34,
|
||||
'KeyH': 35,
|
||||
'KeyI': 23,
|
||||
'KeyJ': 36,
|
||||
'KeyK': 37,
|
||||
'KeyL': 38,
|
||||
'KeyM': 50,
|
||||
'KeyN': 49,
|
||||
'KeyO': 24,
|
||||
'KeyP': 25,
|
||||
'KeyQ': 16,
|
||||
'KeyR': 19,
|
||||
'KeyS': 31,
|
||||
'KeyT': 20,
|
||||
'KeyU': 22,
|
||||
'KeyV': 47,
|
||||
'KeyW': 17,
|
||||
'KeyX': 45,
|
||||
'KeyY': 21,
|
||||
'KeyZ': 44,
|
||||
'Digit1': 2,
|
||||
'Digit2': 3,
|
||||
'Digit3': 4,
|
||||
'Digit4': 5,
|
||||
'Digit5': 6,
|
||||
'Digit6': 7,
|
||||
'Digit7': 8,
|
||||
'Digit8': 9,
|
||||
'Digit9': 10,
|
||||
'Digit0': 11,
|
||||
'Enter': 28,
|
||||
'Escape': 1,
|
||||
'Backspace': 14,
|
||||
'Tab': 15,
|
||||
'Space': 57,
|
||||
'Minus': 12,
|
||||
'Equal': 13,
|
||||
'BracketLeft': 26,
|
||||
'BracketRight': 27,
|
||||
'Backslash': 43,
|
||||
'Semicolon': 39,
|
||||
'Quote': 40,
|
||||
'Backquote': 41,
|
||||
'Comma': 51,
|
||||
'Period': 52,
|
||||
'Slash': 53,
|
||||
'CapsLock': 58,
|
||||
'F1': 59,
|
||||
'F2': 60,
|
||||
'F3': 61,
|
||||
'F4': 62,
|
||||
'F5': 63,
|
||||
'F6': 64,
|
||||
'F7': 65,
|
||||
'F8': 66,
|
||||
'F9': 67,
|
||||
'F10': 68,
|
||||
'F11': 87,
|
||||
'F12': 88,
|
||||
'Insert': 110,
|
||||
'Delete': 111,
|
||||
'ArrowUp': 103,
|
||||
'ArrowDown': 108,
|
||||
'ArrowLeft': 105,
|
||||
'ArrowRight': 106,
|
||||
'Home': 102,
|
||||
'End': 107,
|
||||
'PageUp': 104,
|
||||
'PageDown': 109,
|
||||
'NumLock': 69,
|
||||
'ScrollLock': 70,
|
||||
'Pause': 119,
|
||||
'Numpad0': 82,
|
||||
'Numpad1': 79,
|
||||
'Numpad2': 80,
|
||||
'Numpad3': 81,
|
||||
'Numpad4': 75,
|
||||
'Numpad5': 76,
|
||||
'Numpad6': 77,
|
||||
'Numpad7': 71,
|
||||
'Numpad8': 72,
|
||||
'Numpad9': 73,
|
||||
'NumpadDivide': 98,
|
||||
'NumpadMultiply': 55,
|
||||
'NumpadSubtract': 74,
|
||||
'NumpadAdd': 78,
|
||||
'NumpadEnter': 96,
|
||||
'NumpadDecimal': 83,
|
||||
'ControlLeft': 29,
|
||||
'ControlRight': 97,
|
||||
'ShiftLeft': 42,
|
||||
'ShiftRight': 54,
|
||||
'AltLeft': 56,
|
||||
'AltRight': 100,
|
||||
//'MetaLeft': 125, // Disabled as will break input
|
||||
//'MetaRight': 126, // Disabled as will break input
|
||||
'ContextMenu': 127,
|
||||
KeyA: 30,
|
||||
KeyB: 48,
|
||||
KeyC: 46,
|
||||
KeyD: 32,
|
||||
KeyE: 18,
|
||||
KeyF: 33,
|
||||
KeyG: 34,
|
||||
KeyH: 35,
|
||||
KeyI: 23,
|
||||
KeyJ: 36,
|
||||
KeyK: 37,
|
||||
KeyL: 38,
|
||||
KeyM: 50,
|
||||
KeyN: 49,
|
||||
KeyO: 24,
|
||||
KeyP: 25,
|
||||
KeyQ: 16,
|
||||
KeyR: 19,
|
||||
KeyS: 31,
|
||||
KeyT: 20,
|
||||
KeyU: 22,
|
||||
KeyV: 47,
|
||||
KeyW: 17,
|
||||
KeyX: 45,
|
||||
KeyY: 21,
|
||||
KeyZ: 44,
|
||||
Digit1: 2,
|
||||
Digit2: 3,
|
||||
Digit3: 4,
|
||||
Digit4: 5,
|
||||
Digit5: 6,
|
||||
Digit6: 7,
|
||||
Digit7: 8,
|
||||
Digit8: 9,
|
||||
Digit9: 10,
|
||||
Digit0: 11,
|
||||
Enter: 28,
|
||||
Escape: 1,
|
||||
Backspace: 14,
|
||||
Tab: 15,
|
||||
Space: 57,
|
||||
Minus: 12,
|
||||
Equal: 13,
|
||||
BracketLeft: 26,
|
||||
BracketRight: 27,
|
||||
Backslash: 43,
|
||||
Semicolon: 39,
|
||||
Quote: 40,
|
||||
Backquote: 41,
|
||||
Comma: 51,
|
||||
Period: 52,
|
||||
Slash: 53,
|
||||
CapsLock: 58,
|
||||
F1: 59,
|
||||
F2: 60,
|
||||
F3: 61,
|
||||
F4: 62,
|
||||
F5: 63,
|
||||
F6: 64,
|
||||
F7: 65,
|
||||
F8: 66,
|
||||
F9: 67,
|
||||
F10: 68,
|
||||
F11: 87,
|
||||
F12: 88,
|
||||
Insert: 110,
|
||||
Delete: 111,
|
||||
ArrowUp: 103,
|
||||
ArrowDown: 108,
|
||||
ArrowLeft: 105,
|
||||
ArrowRight: 106,
|
||||
Home: 102,
|
||||
End: 107,
|
||||
PageUp: 104,
|
||||
PageDown: 109,
|
||||
NumLock: 69,
|
||||
ScrollLock: 70,
|
||||
Pause: 119,
|
||||
Numpad0: 82,
|
||||
Numpad1: 79,
|
||||
Numpad2: 80,
|
||||
Numpad3: 81,
|
||||
Numpad4: 75,
|
||||
Numpad5: 76,
|
||||
Numpad6: 77,
|
||||
Numpad7: 71,
|
||||
Numpad8: 72,
|
||||
Numpad9: 73,
|
||||
NumpadDivide: 98,
|
||||
NumpadMultiply: 55,
|
||||
NumpadSubtract: 74,
|
||||
NumpadAdd: 78,
|
||||
NumpadEnter: 96,
|
||||
NumpadDecimal: 83,
|
||||
ControlLeft: 29,
|
||||
ControlRight: 97,
|
||||
ShiftLeft: 42,
|
||||
ShiftRight: 54,
|
||||
AltLeft: 56,
|
||||
AltRight: 100,
|
||||
//'MetaLeft': 125, // Disabled as will break input
|
||||
//'MetaRight': 126, // Disabled as will break input
|
||||
ContextMenu: 127,
|
||||
};
|
||||
|
||||
export const mouseButtonToLinuxEventCode: { [button: number]: number } = {
|
||||
0: 272,
|
||||
2: 273,
|
||||
1: 274,
|
||||
3: 275,
|
||||
4: 276
|
||||
0: 272,
|
||||
2: 273,
|
||||
1: 274,
|
||||
3: 275,
|
||||
4: 276,
|
||||
};
|
||||
|
||||
export const controllerButtonToLinuxEventCode: { [button: number]: number } = {
|
||||
0: 0x130,
|
||||
1: 0x131,
|
||||
2: 0x134,
|
||||
3: 0x133,
|
||||
4: 0x136,
|
||||
5: 0x137,
|
||||
6: 0x138,
|
||||
7: 0x139,
|
||||
8: 0x13a,
|
||||
9: 0x13b,
|
||||
10: 0x13d,
|
||||
11: 0x13e,
|
||||
12: 0x220,
|
||||
13: 0x221,
|
||||
14: 0x222,
|
||||
15: 0x223,
|
||||
16: 0x13c,
|
||||
};
|
||||
|
||||
509
packages/input/src/controller.ts
Normal file
509
packages/input/src/controller.ts
Normal file
@@ -0,0 +1,509 @@
|
||||
import { controllerButtonToLinuxEventCode } from "./codes";
|
||||
import { WebRTCStream } from "./webrtc-stream";
|
||||
import {
|
||||
ProtoMessageBase,
|
||||
ProtoMessageInput,
|
||||
ProtoMessageInputSchema,
|
||||
} from "./proto/messages_pb";
|
||||
import {
|
||||
ProtoInputSchema,
|
||||
ProtoControllerAttachSchema,
|
||||
ProtoControllerDetachSchema,
|
||||
ProtoControllerButtonSchema,
|
||||
ProtoControllerTriggerSchema,
|
||||
ProtoControllerAxisSchema,
|
||||
ProtoControllerStickSchema,
|
||||
ProtoControllerRumble,
|
||||
} from "./proto/types_pb";
|
||||
import { create, toBinary, fromBinary } from "@bufbuild/protobuf";
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCStream;
|
||||
e: GamepadEvent;
|
||||
}
|
||||
|
||||
interface GamepadState {
|
||||
buttonState: Map<number, boolean>;
|
||||
leftTrigger: number;
|
||||
rightTrigger: number;
|
||||
leftX: number;
|
||||
leftY: number;
|
||||
rightX: number;
|
||||
rightY: number;
|
||||
dpadX: number;
|
||||
dpadY: number;
|
||||
}
|
||||
|
||||
export class Controller {
|
||||
protected wrtc: WebRTCStream;
|
||||
protected slot: number;
|
||||
protected connected: boolean = false;
|
||||
protected gamepad: Gamepad | null = null;
|
||||
protected lastState: GamepadState = {
|
||||
buttonState: new Map<number, boolean>(),
|
||||
leftTrigger: 0,
|
||||
rightTrigger: 0,
|
||||
leftX: 0,
|
||||
leftY: 0,
|
||||
rightX: 0,
|
||||
rightY: 0,
|
||||
dpadX: 0,
|
||||
dpadY: 0,
|
||||
};
|
||||
// TODO: As user configurable, set quite low now for decent controllers (not Nintendo ones :P)
|
||||
protected stickDeadzone: number = 2048; // 2048 / 32768 = ~0.06 (6% of stick range)
|
||||
|
||||
private updateInterval = 10.0; // 100 updates per second
|
||||
private _dcRumbleHandler: ((data: ArrayBuffer) => void) | null = null;
|
||||
|
||||
constructor({ webrtc, e }: Props) {
|
||||
this.wrtc = webrtc;
|
||||
this.slot = e.gamepad.index;
|
||||
|
||||
this.updateInterval = 1000 / webrtc.currentFrameRate;
|
||||
|
||||
// Gamepad connected
|
||||
this.gamepad = e.gamepad;
|
||||
|
||||
// Get vendor of gamepad from id string (i.e. "... Vendor: 054c Product: 09cc")
|
||||
const vendorMatch = e.gamepad.id.match(/Vendor:\s?([0-9a-fA-F]{4})/);
|
||||
const vendorId = vendorMatch ? vendorMatch[1].toLowerCase() : "unknown";
|
||||
// Get product id of gamepad from id string
|
||||
const productMatch = e.gamepad.id.match(/Product:\s?([0-9a-fA-F]{4})/);
|
||||
const productId = productMatch ? productMatch[1].toLowerCase() : "unknown";
|
||||
|
||||
const attachMsg = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerAttach",
|
||||
value: create(ProtoControllerAttachSchema, {
|
||||
type: "ControllerAttach",
|
||||
id: this.vendor_id_to_controller(vendorId, productId),
|
||||
slot: this.slot,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: attachMsg,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
|
||||
// Listen to feedback rumble events from server
|
||||
this._dcRumbleHandler = (data: any) => this.rumbleCallback(data as ArrayBuffer);
|
||||
this.wrtc.addDataChannelCallback(this._dcRumbleHandler);
|
||||
|
||||
this.run();
|
||||
}
|
||||
|
||||
// Maps vendor id and product id to supported controller type
|
||||
// Currently supported: Sony (ps4, ps5), Microsoft (xbox360, xboxone), Nintendo (switchpro)
|
||||
// Default fallback to xbox360
|
||||
private vendor_id_to_controller(vendorId: string, productId: string): string {
|
||||
switch (vendorId) {
|
||||
case "054c": // Sony
|
||||
switch (productId) {
|
||||
case "0ce6":
|
||||
return "ps5";
|
||||
case "05c4":
|
||||
case "09cc":
|
||||
return "ps4";
|
||||
default:
|
||||
return "ps4"; // default to ps4
|
||||
}
|
||||
case "045e": // Microsoft
|
||||
switch (productId) {
|
||||
case "02d1":
|
||||
case "02dd":
|
||||
return "xboxone";
|
||||
case "028e":
|
||||
return "xbox360";
|
||||
default:
|
||||
return "xbox360"; // default to xbox360
|
||||
}
|
||||
case "057e": // Nintendo
|
||||
switch (productId) {
|
||||
case "2009":
|
||||
case "200e":
|
||||
return "switchpro";
|
||||
default:
|
||||
return "switchpro"; // default to switchpro
|
||||
}
|
||||
default: {
|
||||
return "xbox360";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private remapFromTo(
|
||||
value: number,
|
||||
fromMin: number,
|
||||
fromMax: number,
|
||||
toMin: number,
|
||||
toMax: number,
|
||||
) {
|
||||
return ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin) + toMin;
|
||||
}
|
||||
|
||||
private pollGamepad() {
|
||||
const gamepads = navigator.getGamepads();
|
||||
if (this.slot < gamepads.length) {
|
||||
const gamepad = gamepads[this.slot];
|
||||
if (gamepad) {
|
||||
/* Button handling */
|
||||
gamepad.buttons.forEach((button, index) => {
|
||||
// Ignore d-pad buttons (12-15) as we handle those as axis
|
||||
if (index >= 12 && index <= 15) return;
|
||||
// ignore trigger buttons (6-7) as we handle those as axis
|
||||
if (index === 6 || index === 7) return;
|
||||
// If state differs, send
|
||||
if (button.pressed !== this.lastState.buttonState.get(index)) {
|
||||
const linuxCode = this.controllerButtonToVirtualKeyCode(index);
|
||||
if (linuxCode === undefined) {
|
||||
// Skip unmapped button index
|
||||
this.lastState.buttonState.set(index, button.pressed);
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerButton",
|
||||
value: create(ProtoControllerButtonSchema, {
|
||||
type: "ControllerButton",
|
||||
slot: this.slot,
|
||||
button: linuxCode,
|
||||
pressed: button.pressed,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const buttonMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: buttonProto,
|
||||
};
|
||||
this.wrtc.sendBinary(
|
||||
toBinary(ProtoMessageInputSchema, buttonMessage),
|
||||
);
|
||||
// Store button state
|
||||
this.lastState.buttonState.set(index, button.pressed);
|
||||
}
|
||||
});
|
||||
|
||||
/* Trigger handling */
|
||||
// map trigger value from 0.0 to 1.0 to -32768 to 32767
|
||||
const leftTrigger = Math.round(
|
||||
this.remapFromTo(gamepad.buttons[6]?.value ?? 0, 0, 1, -32768, 32767),
|
||||
);
|
||||
// If state differs, send
|
||||
if (leftTrigger !== this.lastState.leftTrigger) {
|
||||
const triggerProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerTrigger",
|
||||
value: create(ProtoControllerTriggerSchema, {
|
||||
type: "ControllerTrigger",
|
||||
slot: this.slot,
|
||||
trigger: 0, // 0 = left, 1 = right
|
||||
value: leftTrigger,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const triggerMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: triggerProto,
|
||||
};
|
||||
this.lastState.leftTrigger = leftTrigger;
|
||||
this.wrtc.sendBinary(
|
||||
toBinary(ProtoMessageInputSchema, triggerMessage),
|
||||
);
|
||||
}
|
||||
const rightTrigger = Math.round(
|
||||
this.remapFromTo(gamepad.buttons[7]?.value ?? 0, 0, 1, -32768, 32767),
|
||||
);
|
||||
// If state differs, send
|
||||
if (rightTrigger !== this.lastState.rightTrigger) {
|
||||
const triggerProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerTrigger",
|
||||
value: create(ProtoControllerTriggerSchema, {
|
||||
type: "ControllerTrigger",
|
||||
slot: this.slot,
|
||||
trigger: 1, // 0 = left, 1 = right
|
||||
value: rightTrigger,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const triggerMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: triggerProto,
|
||||
};
|
||||
this.lastState.rightTrigger = rightTrigger;
|
||||
this.wrtc.sendBinary(
|
||||
toBinary(ProtoMessageInputSchema, triggerMessage),
|
||||
);
|
||||
}
|
||||
|
||||
/* DPad handling */
|
||||
// We send dpad buttons as axis values -1 to 1 for left/up, right/down
|
||||
const dpadLeft = gamepad.buttons[14]?.pressed ? 1 : 0;
|
||||
const dpadRight = gamepad.buttons[15]?.pressed ? 1 : 0;
|
||||
const dpadX = dpadLeft ? -1 : dpadRight ? 1 : 0;
|
||||
if (dpadX !== this.lastState.dpadX) {
|
||||
const dpadProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerAxis",
|
||||
value: create(ProtoControllerAxisSchema, {
|
||||
type: "ControllerAxis",
|
||||
slot: this.slot,
|
||||
axis: 0, // 0 = dpadX, 1 = dpadY
|
||||
value: dpadX,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const dpadMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: dpadProto,
|
||||
};
|
||||
this.lastState.dpadX = dpadX;
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, dpadMessage));
|
||||
}
|
||||
|
||||
const dpadUp = gamepad.buttons[12]?.pressed ? 1 : 0;
|
||||
const dpadDown = gamepad.buttons[13]?.pressed ? 1 : 0;
|
||||
const dpadY = dpadUp ? -1 : dpadDown ? 1 : 0;
|
||||
if (dpadY !== this.lastState.dpadY) {
|
||||
const dpadProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerAxis",
|
||||
value: create(ProtoControllerAxisSchema, {
|
||||
type: "ControllerAxis",
|
||||
slot: this.slot,
|
||||
axis: 1, // 0 = dpadX, 1 = dpadY
|
||||
value: dpadY,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const dpadMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: dpadProto,
|
||||
};
|
||||
this.lastState.dpadY = dpadY;
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, dpadMessage));
|
||||
}
|
||||
|
||||
/* Stick handling */
|
||||
// stick values need to be mapped from -1.0 to 1.0 to -32768 to 32767
|
||||
const leftX = this.remapFromTo(gamepad.axes[0] ?? 0, -1, 1, -32768, 32767);
|
||||
const leftY = this.remapFromTo(gamepad.axes[1] ?? 0, -1, 1, -32768, 32767);
|
||||
// Apply deadzone
|
||||
const sendLeftX =
|
||||
Math.abs(leftX) > this.stickDeadzone ? Math.round(leftX) : 0;
|
||||
const sendLeftY =
|
||||
Math.abs(leftY) > this.stickDeadzone ? Math.round(leftY) : 0;
|
||||
// if outside deadzone, send normally if changed
|
||||
// if moves inside deadzone, zero it if not inside deadzone last time
|
||||
if (
|
||||
sendLeftX !== this.lastState.leftX ||
|
||||
sendLeftY !== this.lastState.leftY
|
||||
) {
|
||||
// console.log("Sticks: ", sendLeftX, sendLeftY, sendRightX, sendRightY);
|
||||
const stickProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerStick",
|
||||
value: create(ProtoControllerStickSchema, {
|
||||
type: "ControllerStick",
|
||||
slot: this.slot,
|
||||
stick: 0, // 0 = left, 1 = right
|
||||
x: sendLeftX,
|
||||
y: sendLeftY,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const stickMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: stickProto,
|
||||
};
|
||||
this.lastState.leftX = sendLeftX;
|
||||
this.lastState.leftY = sendLeftY;
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, stickMessage));
|
||||
}
|
||||
|
||||
const rightX = this.remapFromTo(gamepad.axes[2] ?? 0, -1, 1, -32768, 32767);
|
||||
const rightY = this.remapFromTo(gamepad.axes[3] ?? 0, -1, 1, -32768, 32767);
|
||||
// Apply deadzone
|
||||
const sendRightX =
|
||||
Math.abs(rightX) > this.stickDeadzone ? Math.round(rightX) : 0;
|
||||
const sendRightY =
|
||||
Math.abs(rightY) > this.stickDeadzone ? Math.round(rightY) : 0;
|
||||
if (
|
||||
sendRightX !== this.lastState.rightX ||
|
||||
sendRightY !== this.lastState.rightY
|
||||
) {
|
||||
const stickProto = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerStick",
|
||||
value: create(ProtoControllerStickSchema, {
|
||||
type: "ControllerStick",
|
||||
slot: this.slot,
|
||||
stick: 1, // 0 = left, 1 = right
|
||||
x: sendRightX,
|
||||
y: sendRightY,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const stickMessage: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: stickProto,
|
||||
};
|
||||
this.lastState.rightX = sendRightX;
|
||||
this.lastState.rightY = sendRightY;
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, stickMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loopInterval: any = null;
|
||||
|
||||
public run() {
|
||||
if (this.connected)
|
||||
this.stop();
|
||||
|
||||
this.connected = true;
|
||||
// Poll gamepads in setInterval loop
|
||||
this.loopInterval = setInterval(() => {
|
||||
if (this.connected) this.pollGamepad();
|
||||
}, this.updateInterval);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.loopInterval) {
|
||||
clearInterval(this.loopInterval);
|
||||
this.loopInterval = null;
|
||||
}
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
public getSlot() {
|
||||
return this.slot;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.stop();
|
||||
// Remove callback
|
||||
if (this._dcRumbleHandler !== null) {
|
||||
this.wrtc.removeDataChannelCallback(this._dcRumbleHandler);
|
||||
this._dcRumbleHandler = null;
|
||||
}
|
||||
// Gamepad disconnected
|
||||
const detachMsg = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerDetach",
|
||||
value: create(ProtoControllerDetachSchema, {
|
||||
type: "ControllerDetach",
|
||||
slot: this.slot,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "controllerInput",
|
||||
} as ProtoMessageBase,
|
||||
data: detachMsg,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
}
|
||||
|
||||
private controllerButtonToVirtualKeyCode(code: number) {
|
||||
return controllerButtonToLinuxEventCode[code] || undefined;
|
||||
}
|
||||
|
||||
private rumbleCallback(data: ArrayBuffer) {
|
||||
// If not connected, ignore
|
||||
if (!this.connected) return;
|
||||
try {
|
||||
// First decode the wrapper message
|
||||
const uint8Data = new Uint8Array(data);
|
||||
const messageWrapper = fromBinary(ProtoMessageInputSchema, uint8Data);
|
||||
|
||||
// Check if it contains controller rumble data
|
||||
if (messageWrapper.data?.inputType?.case === "controllerRumble") {
|
||||
const rumbleMsg = messageWrapper.data.inputType.value as ProtoControllerRumble;
|
||||
|
||||
// Check if aimed at this controller slot
|
||||
if (rumbleMsg.slot !== this.slot) return;
|
||||
|
||||
// Trigger actual rumble
|
||||
// Need to remap from 0-65535 to 0.0-1.0 ranges
|
||||
const clampedLowFreq = Math.max(0, Math.min(65535, rumbleMsg.lowFrequency));
|
||||
const rumbleLowFreq = this.remapFromTo(
|
||||
clampedLowFreq,
|
||||
0,
|
||||
65535,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
const clampedHighFreq = Math.max(0, Math.min(65535, rumbleMsg.highFrequency));
|
||||
const rumbleHighFreq = this.remapFromTo(
|
||||
clampedHighFreq,
|
||||
0,
|
||||
65535,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
// Cap to valid range (max 5000)
|
||||
const rumbleDuration = Math.max(0, Math.min(5000, rumbleMsg.duration));
|
||||
if (this.gamepad.vibrationActuator) {
|
||||
this.gamepad.vibrationActuator.playEffect("dual-rumble", {
|
||||
startDelay: 0,
|
||||
duration: rumbleDuration,
|
||||
weakMagnitude: rumbleLowFreq,
|
||||
strongMagnitude: rumbleHighFreq,
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to decode rumble message:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./keyboard"
|
||||
export * from "./mouse"
|
||||
export * from "./controller"
|
||||
export * from "./webrtc-stream"
|
||||
@@ -9,27 +9,23 @@ import {
|
||||
ProtoInputSchema,
|
||||
ProtoKeyDownSchema,
|
||||
ProtoKeyUpSchema,
|
||||
ProtoMouseMoveSchema
|
||||
} from "./proto/types_pb";
|
||||
import {create, toBinary} from "@bufbuild/protobuf";
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCStream;
|
||||
canvas: HTMLCanvasElement;
|
||||
}
|
||||
|
||||
export class Keyboard {
|
||||
protected wrtc: WebRTCStream;
|
||||
protected canvas: HTMLCanvasElement;
|
||||
protected connected!: boolean;
|
||||
|
||||
// Store references to event listeners
|
||||
private readonly keydownListener: (e: KeyboardEvent) => void;
|
||||
private readonly keyupListener: (e: KeyboardEvent) => void;
|
||||
|
||||
constructor({webrtc, canvas}: Props) {
|
||||
constructor({webrtc}: Props) {
|
||||
this.wrtc = webrtc;
|
||||
this.canvas = canvas;
|
||||
this.keydownListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
@@ -54,23 +50,12 @@ export class Keyboard {
|
||||
}
|
||||
|
||||
private run() {
|
||||
//calls all the other functions
|
||||
if (!document.pointerLockElement) {
|
||||
if (this.connected) {
|
||||
this.stop()
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.connected)
|
||||
this.stop()
|
||||
|
||||
if (document.pointerLockElement == this.canvas) {
|
||||
this.connected = true
|
||||
document.addEventListener("keydown", this.keydownListener, {passive: false});
|
||||
document.addEventListener("keyup", this.keyupListener, {passive: false});
|
||||
} else {
|
||||
if (this.connected) {
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
this.connected = true
|
||||
document.addEventListener("keydown", this.keydownListener, {passive: false});
|
||||
document.addEventListener("keyup", this.keyupListener, {passive: false});
|
||||
}
|
||||
|
||||
private stop() {
|
||||
@@ -120,7 +105,6 @@ export class Keyboard {
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
document.exitPointerLock();
|
||||
this.stop();
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
@@ -24,13 +24,12 @@ export class Mouse {
|
||||
protected canvas: HTMLCanvasElement;
|
||||
protected connected!: boolean;
|
||||
|
||||
// Store references to event listeners
|
||||
private sendInterval = 16 //60fps
|
||||
private sendInterval = 10 // 100 updates per second
|
||||
|
||||
// Store references to event listeners
|
||||
private readonly mousemoveListener: (e: MouseEvent) => void;
|
||||
private movementX: number = 0;
|
||||
private movementY: number = 0;
|
||||
private isProcessing: boolean = false;
|
||||
|
||||
private readonly mousedownListener: (e: MouseEvent) => void;
|
||||
private readonly mouseupListener: (e: MouseEvent) => void;
|
||||
@@ -40,7 +39,7 @@ export class Mouse {
|
||||
this.wrtc = webrtc;
|
||||
this.canvas = canvas;
|
||||
|
||||
this.sendInterval = 1000 / webrtc.currentFrameRate
|
||||
this.sendInterval = 1000 / webrtc.currentFrameRate;
|
||||
|
||||
this.mousemoveListener = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -75,8 +74,8 @@ export class Mouse {
|
||||
case: "mouseWheel",
|
||||
value: create(ProtoMouseWheelSchema, {
|
||||
type: "MouseWheel",
|
||||
x: e.deltaX,
|
||||
y: e.deltaY
|
||||
x: Math.round(e.deltaX),
|
||||
y: Math.round(e.deltaY),
|
||||
}),
|
||||
}
|
||||
}));
|
||||
@@ -135,8 +134,8 @@ export class Mouse {
|
||||
case: "mouseMove",
|
||||
value: create(ProtoMouseMoveSchema, {
|
||||
type: "MouseMove",
|
||||
x: this.movementX,
|
||||
y: this.movementY,
|
||||
x: Math.round(this.movementX),
|
||||
y: Math.round(this.movementY),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated from file latency_tracker.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Timestamp } from "@bufbuild/protobuf/wkt";
|
||||
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated from file messages.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { ProtoInput } from "./types_pb";
|
||||
import { file_types } from "./types_pb";
|
||||
import type { ProtoLatencyTracker } from "./latency_tracker_pb";
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated from file types.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file types.proto.
|
||||
*/
|
||||
export const file_types: GenFile = /*@__PURE__*/
|
||||
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSLcAgoKUHJvdG9JbnB1dBIrCgptb3VzZV9tb3ZlGAEgASgLMhUucHJvdG8uUHJvdG9Nb3VzZU1vdmVIABIyCg5tb3VzZV9tb3ZlX2FicxgCIAEoCzIYLnByb3RvLlByb3RvTW91c2VNb3ZlQWJzSAASLQoLbW91c2Vfd2hlZWwYAyABKAsyFi5wcm90by5Qcm90b01vdXNlV2hlZWxIABIyCg5tb3VzZV9rZXlfZG93bhgEIAEoCzIYLnByb3RvLlByb3RvTW91c2VLZXlEb3duSAASLgoMbW91c2Vfa2V5X3VwGAUgASgLMhYucHJvdG8uUHJvdG9Nb3VzZUtleVVwSAASJwoIa2V5X2Rvd24YBiABKAsyEy5wcm90by5Qcm90b0tleURvd25IABIjCgZrZXlfdXAYByABKAsyES5wcm90by5Qcm90b0tleVVwSABCDAoKaW5wdXRfdHlwZUIWWhRyZWxheS9pbnRlcm5hbC9wcm90b2IGcHJvdG8z");
|
||||
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSI/ChVQcm90b0NvbnRyb2xsZXJBdHRhY2gSDAoEdHlwZRgBIAEoCRIKCgJpZBgCIAEoCRIMCgRzbG90GAMgASgFIjMKFVByb3RvQ29udHJvbGxlckRldGFjaBIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUiVAoVUHJvdG9Db250cm9sbGVyQnV0dG9uEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIOCgZidXR0b24YAyABKAUSDwoHcHJlc3NlZBgEIAEoCCJUChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIPCgd0cmlnZ2VyGAMgASgFEg0KBXZhbHVlGAQgASgFIlcKFFByb3RvQ29udHJvbGxlclN0aWNrEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRINCgVzdGljaxgDIAEoBRIJCgF4GAQgASgFEgkKAXkYBSABKAUiTgoTUHJvdG9Db250cm9sbGVyQXhpcxIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUSDAoEYXhpcxgDIAEoBRINCgV2YWx1ZRgEIAEoBSJ0ChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEdHlwZRgBIAEoCRIMCgRzbG90GAIgASgFEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi9QUKClByb3RvSW5wdXQSKwoKbW91c2VfbW92ZRgBIAEoCzIVLnByb3RvLlByb3RvTW91c2VNb3ZlSAASMgoObW91c2VfbW92ZV9hYnMYAiABKAsyGC5wcm90by5Qcm90b01vdXNlTW92ZUFic0gAEi0KC21vdXNlX3doZWVsGAMgASgLMhYucHJvdG8uUHJvdG9Nb3VzZVdoZWVsSAASMgoObW91c2Vfa2V5X2Rvd24YBCABKAsyGC5wcm90by5Qcm90b01vdXNlS2V5RG93bkgAEi4KDG1vdXNlX2tleV91cBgFIAEoCzIWLnByb3RvLlByb3RvTW91c2VLZXlVcEgAEicKCGtleV9kb3duGAYgASgLMhMucHJvdG8uUHJvdG9LZXlEb3duSAASIwoGa2V5X3VwGAcgASgLMhEucHJvdG8uUHJvdG9LZXlVcEgAEjkKEWNvbnRyb2xsZXJfYXR0YWNoGAggASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyQXR0YWNoSAASOQoRY29udHJvbGxlcl9kZXRhY2gYCSABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJEZXRhY2hIABI5ChFjb250cm9sbGVyX2J1dHRvbhgKIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckJ1dHRvbkgAEjsKEmNvbnRyb2xsZXJfdHJpZ2dlchgLIAEoCzIdLnByb3RvLlByb3RvQ29udHJvbGxlclRyaWdnZXJIABI3ChBjb250cm9sbGVyX3N0aWNrGAwgASgLMhsucHJvdG8uUHJvdG9Db250cm9sbGVyU3RpY2tIABI1Cg9jb250cm9sbGVyX2F4aXMYDSABKAsyGi5wcm90by5Qcm90b0NvbnRyb2xsZXJBeGlzSAASOQoRY29udHJvbGxlcl9ydW1ibGUYDiABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIAEIMCgppbnB1dF90eXBlQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM");
|
||||
|
||||
/**
|
||||
* MouseMove message
|
||||
@@ -209,6 +209,293 @@ export type ProtoKeyUp = Message<"proto.ProtoKeyUp"> & {
|
||||
export const ProtoKeyUpSchema: GenMessage<ProtoKeyUp> = /*@__PURE__*/
|
||||
messageDesc(file_types, 6);
|
||||
|
||||
/**
|
||||
* ControllerAttach message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerAttach
|
||||
*/
|
||||
export type ProtoControllerAttach = Message<"proto.ProtoControllerAttach"> & {
|
||||
/**
|
||||
* Fixed value "ControllerAttach"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* One of the following enums: "ps", "xbox" or "switch"
|
||||
*
|
||||
* @generated from field: string id = 2;
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 3;
|
||||
*/
|
||||
slot: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerAttach.
|
||||
* Use `create(ProtoControllerAttachSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerAttachSchema: GenMessage<ProtoControllerAttach> = /*@__PURE__*/
|
||||
messageDesc(file_types, 7);
|
||||
|
||||
/**
|
||||
* ControllerDetach message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerDetach
|
||||
*/
|
||||
export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & {
|
||||
/**
|
||||
* Fixed value "ControllerDetach"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerDetach.
|
||||
* Use `create(ProtoControllerDetachSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerDetachSchema: GenMessage<ProtoControllerDetach> = /*@__PURE__*/
|
||||
messageDesc(file_types, 8);
|
||||
|
||||
/**
|
||||
* ControllerButton message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerButton
|
||||
*/
|
||||
export type ProtoControllerButton = Message<"proto.ProtoControllerButton"> & {
|
||||
/**
|
||||
* Fixed value "ControllerButtons"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Button code (linux input event code)
|
||||
*
|
||||
* @generated from field: int32 button = 3;
|
||||
*/
|
||||
button: number;
|
||||
|
||||
/**
|
||||
* true if pressed, false if released
|
||||
*
|
||||
* @generated from field: bool pressed = 4;
|
||||
*/
|
||||
pressed: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerButton.
|
||||
* Use `create(ProtoControllerButtonSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerButtonSchema: GenMessage<ProtoControllerButton> = /*@__PURE__*/
|
||||
messageDesc(file_types, 9);
|
||||
|
||||
/**
|
||||
* ControllerTriggers message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerTrigger
|
||||
*/
|
||||
export type ProtoControllerTrigger = Message<"proto.ProtoControllerTrigger"> & {
|
||||
/**
|
||||
* Fixed value "ControllerTriggers"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Trigger number (0 for left, 1 for right)
|
||||
*
|
||||
* @generated from field: int32 trigger = 3;
|
||||
*/
|
||||
trigger: number;
|
||||
|
||||
/**
|
||||
* trigger value (-32768 to 32767)
|
||||
*
|
||||
* @generated from field: int32 value = 4;
|
||||
*/
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerTrigger.
|
||||
* Use `create(ProtoControllerTriggerSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerTriggerSchema: GenMessage<ProtoControllerTrigger> = /*@__PURE__*/
|
||||
messageDesc(file_types, 10);
|
||||
|
||||
/**
|
||||
* ControllerSticks message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerStick
|
||||
*/
|
||||
export type ProtoControllerStick = Message<"proto.ProtoControllerStick"> & {
|
||||
/**
|
||||
* Fixed value "ControllerStick"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Stick number (0 for left, 1 for right)
|
||||
*
|
||||
* @generated from field: int32 stick = 3;
|
||||
*/
|
||||
stick: number;
|
||||
|
||||
/**
|
||||
* X axis value (-32768 to 32767)
|
||||
*
|
||||
* @generated from field: int32 x = 4;
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* Y axis value (-32768 to 32767)
|
||||
*
|
||||
* @generated from field: int32 y = 5;
|
||||
*/
|
||||
y: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerStick.
|
||||
* Use `create(ProtoControllerStickSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerStickSchema: GenMessage<ProtoControllerStick> = /*@__PURE__*/
|
||||
messageDesc(file_types, 11);
|
||||
|
||||
/**
|
||||
* ControllerAxis message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerAxis
|
||||
*/
|
||||
export type ProtoControllerAxis = Message<"proto.ProtoControllerAxis"> & {
|
||||
/**
|
||||
* Fixed value "ControllerAxis"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
|
||||
*
|
||||
* @generated from field: int32 axis = 3;
|
||||
*/
|
||||
axis: number;
|
||||
|
||||
/**
|
||||
* axis value (-1 to 1)
|
||||
*
|
||||
* @generated from field: int32 value = 4;
|
||||
*/
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerAxis.
|
||||
* Use `create(ProtoControllerAxisSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerAxisSchema: GenMessage<ProtoControllerAxis> = /*@__PURE__*/
|
||||
messageDesc(file_types, 12);
|
||||
|
||||
/**
|
||||
* ControllerRumble message
|
||||
*
|
||||
* @generated from message proto.ProtoControllerRumble
|
||||
*/
|
||||
export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & {
|
||||
/**
|
||||
* Fixed value "ControllerRumble"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Low frequency rumble (0-65535)
|
||||
*
|
||||
* @generated from field: int32 low_frequency = 3;
|
||||
*/
|
||||
lowFrequency: number;
|
||||
|
||||
/**
|
||||
* High frequency rumble (0-65535)
|
||||
*
|
||||
* @generated from field: int32 high_frequency = 4;
|
||||
*/
|
||||
highFrequency: number;
|
||||
|
||||
/**
|
||||
* Duration in milliseconds
|
||||
*
|
||||
* @generated from field: int32 duration = 5;
|
||||
*/
|
||||
duration: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerRumble.
|
||||
* Use `create(ProtoControllerRumbleSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerRumbleSchema: GenMessage<ProtoControllerRumble> = /*@__PURE__*/
|
||||
messageDesc(file_types, 13);
|
||||
|
||||
/**
|
||||
* Union of all Input types
|
||||
*
|
||||
@@ -260,6 +547,48 @@ export type ProtoInput = Message<"proto.ProtoInput"> & {
|
||||
*/
|
||||
value: ProtoKeyUp;
|
||||
case: "keyUp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerAttach controller_attach = 8;
|
||||
*/
|
||||
value: ProtoControllerAttach;
|
||||
case: "controllerAttach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerDetach controller_detach = 9;
|
||||
*/
|
||||
value: ProtoControllerDetach;
|
||||
case: "controllerDetach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerButton controller_button = 10;
|
||||
*/
|
||||
value: ProtoControllerButton;
|
||||
case: "controllerButton";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerTrigger controller_trigger = 11;
|
||||
*/
|
||||
value: ProtoControllerTrigger;
|
||||
case: "controllerTrigger";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerStick controller_stick = 12;
|
||||
*/
|
||||
value: ProtoControllerStick;
|
||||
case: "controllerStick";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerAxis controller_axis = 13;
|
||||
*/
|
||||
value: ProtoControllerAxis;
|
||||
case: "controllerAxis";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerRumble controller_rumble = 14;
|
||||
*/
|
||||
value: ProtoControllerRumble;
|
||||
case: "controllerRumble";
|
||||
} | { case: undefined; value?: undefined };
|
||||
};
|
||||
|
||||
@@ -268,5 +597,5 @@ export type ProtoInput = Message<"proto.ProtoInput"> & {
|
||||
* Use `create(ProtoInputSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoInputSchema: GenMessage<ProtoInput> = /*@__PURE__*/
|
||||
messageDesc(file_types, 7);
|
||||
messageDesc(file_types, 14);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SafeStream,
|
||||
} from "./messages";
|
||||
import { webSockets } from "@libp2p/websockets";
|
||||
import { webTransport } from "@libp2p/webtransport";
|
||||
import { createLibp2p, Libp2p } from "libp2p";
|
||||
import { noise } from "@chainsafe/libp2p-noise";
|
||||
import { yamux } from "@chainsafe/libp2p-yamux";
|
||||
@@ -13,9 +14,6 @@ import { multiaddr } from "@multiformats/multiaddr";
|
||||
import { Connection } from "@libp2p/interface";
|
||||
import { ping } from "@libp2p/ping";
|
||||
|
||||
//FIXME: Sometimes the room will wait to say offline, then appear to be online after retrying :D
|
||||
// This works for me, with my trashy internet, does it work for you as well?
|
||||
|
||||
const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0";
|
||||
|
||||
export class WebRTCStream {
|
||||
@@ -30,8 +28,9 @@ export class WebRTCStream {
|
||||
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined;
|
||||
private _serverURL: string | undefined = undefined;
|
||||
private _roomName: string | undefined = undefined;
|
||||
private _isConnected: boolean = false; // Add flag to track connection state
|
||||
currentFrameRate: number = 60;
|
||||
private _isConnected: boolean = false;
|
||||
private _dataChannelCallbacks: Array<(data: any) => void> = [];
|
||||
currentFrameRate: number = 100;
|
||||
|
||||
constructor(
|
||||
serverURL: string,
|
||||
@@ -59,7 +58,7 @@ export class WebRTCStream {
|
||||
console.log("Setting up libp2p");
|
||||
|
||||
this._p2p = await createLibp2p({
|
||||
transports: [webSockets()],
|
||||
transports: [webSockets(), webTransport()],
|
||||
connectionEncrypters: [noise()],
|
||||
streamMuxers: [yamux()],
|
||||
connectionGater: {
|
||||
@@ -219,7 +218,8 @@ export class WebRTCStream {
|
||||
}
|
||||
|
||||
private _checkConnectionState() {
|
||||
if (!this._pc) return;
|
||||
if (!this._pc || !this._p2p || !this._p2pConn)
|
||||
return;
|
||||
|
||||
console.debug("Checking connection state:", {
|
||||
connectionState: this._pc.connectionState,
|
||||
@@ -267,9 +267,9 @@ export class WebRTCStream {
|
||||
this._pc.connectionState === "closed" ||
|
||||
this._pc.iceConnectionState === "failed"
|
||||
) {
|
||||
console.log("Connection failed or closed, attempting reconnect");
|
||||
this._isConnected = false; // Reset connected state
|
||||
this._handleConnectionFailure();
|
||||
console.log("PeerConnection failed or closed");
|
||||
//this._isConnected = false; // Reset connected state
|
||||
//this._handleConnectionFailure();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,7 @@ export class WebRTCStream {
|
||||
console.error("Error closing data channel:", err);
|
||||
}
|
||||
this._dataChannel = undefined;
|
||||
this._dataChannelCallbacks = [];
|
||||
}
|
||||
this._isConnected = false; // Reset connected state during cleanup
|
||||
}
|
||||
@@ -329,15 +330,31 @@ export class WebRTCStream {
|
||||
}
|
||||
}
|
||||
|
||||
public addDataChannelCallback(callback: (data: any) => void) {
|
||||
this._dataChannelCallbacks.push(callback);
|
||||
}
|
||||
|
||||
public removeDataChannelCallback(callback: (data: any) => void) {
|
||||
this._dataChannelCallbacks = this._dataChannelCallbacks.filter(cb => cb !== callback);
|
||||
}
|
||||
|
||||
private _setupDataChannelEvents() {
|
||||
if (!this._dataChannel) return;
|
||||
|
||||
this._dataChannel.onclose = () => console.log("sendChannel has closed");
|
||||
this._dataChannel.onopen = () => console.log("sendChannel has opened");
|
||||
this._dataChannel.onmessage = (e) =>
|
||||
console.log(
|
||||
`Message from DataChannel '${this._dataChannel?.label}' payload '${e.data}'`,
|
||||
);
|
||||
this._dataChannel.onmessage = (event => {
|
||||
// Parse as ProtoBuf message
|
||||
const data = event.data;
|
||||
// Call registered callback if exists
|
||||
this._dataChannelCallbacks.forEach((callback) => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (err) {
|
||||
console.error("Error in data channel callback:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _gatherFrameRate() {
|
||||
|
||||
18
packages/patches/bubblewrap/bubbleunheck.patch
Normal file
18
packages/patches/bubblewrap/bubbleunheck.patch
Normal file
@@ -0,0 +1,18 @@
|
||||
diff --git a/bubblewrap.c b/bubblewrap.c
|
||||
index f8728c7..42cfe2e 100644
|
||||
--- a/bubblewrap.c
|
||||
+++ b/bubblewrap.c
|
||||
@@ -876,13 +876,6 @@ acquire_privs (void)
|
||||
/* Keep only the required capabilities for setup */
|
||||
set_required_caps ();
|
||||
}
|
||||
- else if (real_uid != 0 && has_caps ())
|
||||
- {
|
||||
- /* We have some capabilities in the non-setuid case, which should not happen.
|
||||
- Probably caused by the binary being setcap instead of setuid which we
|
||||
- don't support anymore */
|
||||
- die ("Unexpected capabilities but not setuid, old file caps config?");
|
||||
- }
|
||||
else if (real_uid == 0)
|
||||
{
|
||||
/* If our uid is 0, default to inheriting all caps; the caller
|
||||
@@ -11,6 +11,6 @@
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.4.2",
|
||||
"@nestri/input": "*",
|
||||
"astro": "5.13.2"
|
||||
"astro": "5.14.5"
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ if (envs_map.size > 0) {
|
||||
</DefaultLayout>
|
||||
|
||||
<script>
|
||||
import { Mouse, Keyboard, WebRTCStream } from "@nestri/input";
|
||||
import { Mouse, Keyboard, Controller, WebRTCStream } from "@nestri/input";
|
||||
const ENVS = document.getElementById("ENVS")!.dataset.envs as string;
|
||||
let ENVS_MAP: Map<string, string | undefined> | null = null;
|
||||
if (ENVS && ENVS.length > 0) {
|
||||
@@ -32,6 +32,11 @@ if (envs_map.size > 0) {
|
||||
console.debug("ENVS_MAP:", ENVS_MAP);
|
||||
}
|
||||
|
||||
// Method which returns true if mobile device
|
||||
const isMobile = () => {
|
||||
return /Mobi|Android/i.test(navigator.userAgent);
|
||||
};
|
||||
|
||||
// Elements
|
||||
const canvas = document.getElementById("playCanvas")! as HTMLCanvasElement;
|
||||
const offlineText = document.getElementById("offlineText")! as HTMLHeadingElement;
|
||||
@@ -82,51 +87,88 @@ if (envs_map.size > 0) {
|
||||
// Input
|
||||
let nestriMouse: Mouse | null = null;
|
||||
let nestriKeyboard: Keyboard | null = null;
|
||||
let nestriControllers: Controller[] = [];
|
||||
|
||||
window.addEventListener("gamepadconnected", (e) => {
|
||||
// Ignore gamepads with id including "nestri"
|
||||
console.log("Gamepad connected:", e.gamepad);
|
||||
if (e.gamepad.id.toLowerCase().includes("nestri"))
|
||||
return;
|
||||
|
||||
const controller = new Controller({
|
||||
webrtc: stream,
|
||||
e: e,
|
||||
});
|
||||
nestriControllers.push(controller);
|
||||
});
|
||||
window.addEventListener("gamepaddisconnected", (e) => {
|
||||
console.log("Gamepad disconnected:", e.gamepad);
|
||||
if (e.gamepad.id.toLowerCase().includes("nestri"))
|
||||
return;
|
||||
|
||||
let disconnected = nestriControllers.find((c) => c.getSlot() === e.gamepad.index);
|
||||
if (disconnected) {
|
||||
disconnected.dispose();
|
||||
nestriControllers = nestriControllers.filter((c) => c !== disconnected);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("pointerlockchange", () => {
|
||||
if (document.pointerLockElement === canvas) {
|
||||
if (nestriMouse || nestriKeyboard)
|
||||
if (nestriMouse)
|
||||
return;
|
||||
|
||||
nestriMouse = new Mouse({
|
||||
canvas: canvas,
|
||||
webrtc: stream,
|
||||
});
|
||||
nestriKeyboard = new Keyboard({
|
||||
canvas: canvas,
|
||||
webrtc: stream,
|
||||
});
|
||||
} else {
|
||||
if (nestriMouse) {
|
||||
nestriMouse.dispose();
|
||||
nestriMouse = null;
|
||||
}
|
||||
if (nestriKeyboard) {
|
||||
nestriKeyboard.dispose();
|
||||
nestriKeyboard = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("fullscreenchange", () => {
|
||||
if (document.fullscreenElement) {
|
||||
if (nestriKeyboard)
|
||||
return;
|
||||
|
||||
nestriKeyboard = new Keyboard({
|
||||
webrtc: stream,
|
||||
});
|
||||
|
||||
nestriControllers.forEach((c) => c.run());
|
||||
|
||||
if ("keyboard" in navigator && "lock" in (navigator.keyboard as any)) {
|
||||
const keys = [
|
||||
"AltLeft", "AltRight", "Tab", "Escape",
|
||||
"ContextMenu", "MetaLeft", "MetaRight"
|
||||
];
|
||||
|
||||
(navigator.keyboard as any).lock(keys).then(() => {
|
||||
console.log("Keyboard lock acquired");
|
||||
}).catch((err: any) => {
|
||||
console.error("Failed to acquire keyboard lock:", err);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (nestriKeyboard) {
|
||||
nestriKeyboard.dispose();
|
||||
nestriKeyboard = null;
|
||||
}
|
||||
nestriControllers.forEach((c) => c.stop());
|
||||
}
|
||||
})
|
||||
|
||||
const lockPlay = async function () {
|
||||
if (document.fullscreenElement)
|
||||
return;
|
||||
|
||||
await canvas.requestFullscreen();
|
||||
await canvas.requestPointerLock();
|
||||
|
||||
if (document.fullscreenElement !== null) {
|
||||
if ("keyboard" in navigator && "lock" in (navigator.keyboard as any)) {
|
||||
const keys = [
|
||||
"AltLeft", "AltRight", "Tab", "Escape",
|
||||
"ContextMenu", "MetaLeft", "MetaRight"
|
||||
];
|
||||
|
||||
try {
|
||||
await (navigator.keyboard as any).lock(keys);
|
||||
console.log("Keyboard lock acquired");
|
||||
} catch (e) {
|
||||
console.warn("Keyboard lock failed:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isMobile())
|
||||
await canvas.requestPointerLock();
|
||||
};
|
||||
|
||||
canvas.addEventListener("click", lockPlay);
|
||||
|
||||
1
packages/relay/.dockerignore
Normal file
1
packages/relay/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
persist-data/
|
||||
@@ -1,59 +1,50 @@
|
||||
module relay
|
||||
|
||||
go 1.24
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/libp2p/go-libp2p v0.41.1
|
||||
github.com/libp2p/go-libp2p-pubsub v0.13.1
|
||||
github.com/libp2p/go-libp2p v0.44.0
|
||||
github.com/libp2p/go-libp2p-pubsub v0.15.0
|
||||
github.com/libp2p/go-reuseport v0.4.0
|
||||
github.com/multiformats/go-multiaddr v0.15.0
|
||||
github.com/multiformats/go-multiaddr v0.16.1
|
||||
github.com/oklog/ulid/v2 v2.1.1
|
||||
github.com/pion/ice/v4 v4.0.10
|
||||
github.com/pion/interceptor v0.1.38
|
||||
github.com/pion/rtp v1.8.15
|
||||
github.com/pion/webrtc/v4 v4.1.1
|
||||
google.golang.org/protobuf v1.36.6
|
||||
github.com/pion/interceptor v0.1.41
|
||||
github.com/pion/rtp v1.8.24
|
||||
github.com/pion/webrtc/v4 v4.1.6
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/elastic/gosigar v0.14.3 // indirect
|
||||
github.com/filecoin-project/go-clock v0.1.0 // indirect
|
||||
github.com/flynn/noise v1.1.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/ipfs/go-cid v0.5.0 // indirect
|
||||
github.com/ipfs/go-log/v2 v2.6.0 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/koron/go-ssdp v0.0.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/koron/go-ssdp v0.1.0 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.3.0 // indirect
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
|
||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.2 // indirect
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.3.0 // indirect
|
||||
github.com/libp2p/go-yamux/v5 v5.1.0 // indirect
|
||||
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.66 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
@@ -63,54 +54,51 @@ require (
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.9.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.10.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-multistream v0.6.0 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/multiformats/go-multistream v0.6.1 // indirect
|
||||
github.com/multiformats/go-varint v0.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.7 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
github.com/pion/rtcp v1.2.16 // indirect
|
||||
github.com/pion/sctp v1.8.40 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.16 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.8 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.8 // indirect
|
||||
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.64.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.52.0 // indirect
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
|
||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
github.com/quic-go/webtransport-go v0.9.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/dig v1.19.0 // indirect
|
||||
go.uber.org/fx v1.24.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -19,17 +18,8 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -39,13 +29,7 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
||||
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
|
||||
github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
@@ -57,16 +41,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -77,17 +52,12 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
@@ -103,8 +73,6 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||
github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg=
|
||||
github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
@@ -112,15 +80,14 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU=
|
||||
github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y=
|
||||
github.com/koron/go-ssdp v0.1.0/go.mod h1:GltaDBjtK1kemZOusWYLGotV0kBeEf59Bp0wtSB0uyU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -130,39 +97,41 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||
github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784=
|
||||
github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo=
|
||||
github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE=
|
||||
github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI=
|
||||
github.com/libp2p/go-libp2p v0.44.0 h1:5Gtt8OrF8yiXmH+Mx4+/iBeFRMK1TY3a8OrEBDEqAvs=
|
||||
github.com/libp2p/go-libp2p v0.44.0/go.mod h1:NovCojezAt4dnDd4fH048K7PKEqH0UFYYqJRjIIu8zc=
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8=
|
||||
github.com/libp2p/go-libp2p-pubsub v0.13.1 h1:tV3ttzzZSCk0EtEXnxVmWIXgjVxXx+20Jwjbs/Ctzjo=
|
||||
github.com/libp2p/go-libp2p-pubsub v0.13.1/go.mod h1:MKPU5vMI8RRFyTP0HfdsF9cLmL1nHAeJm44AxJGJx44=
|
||||
github.com/libp2p/go-libp2p-pubsub v0.15.0 h1:cG7Cng2BT82WttmPFMi50gDNV+58K626m/wR00vGL1o=
|
||||
github.com/libp2p/go-libp2p-pubsub v0.15.0/go.mod h1:lr4oE8bFgQaifRcoc2uWhWWiK6tPdOEKpUuR408GFN4=
|
||||
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
|
||||
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
|
||||
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
|
||||
github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
|
||||
github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8=
|
||||
github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE=
|
||||
github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc=
|
||||
github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA=
|
||||
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
|
||||
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
|
||||
github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE=
|
||||
github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o=
|
||||
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
|
||||
github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marcopolo/simnet v0.0.1 h1:rSMslhPz6q9IvJeFWDoMGxMIrlsbXau3NkuIXHGJxfg=
|
||||
github.com/marcopolo/simnet v0.0.1/go.mod h1:WDaQkgLAjqDUEBAOXz22+1j6wXKfGlC5sD5XWt3ddOs=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
||||
@@ -183,36 +152,29 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
|
||||
github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo=
|
||||
github.com/multiformats/go-multiaddr v0.15.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
|
||||
github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw=
|
||||
github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M=
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
|
||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
||||
github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc=
|
||||
github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI=
|
||||
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
|
||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA=
|
||||
github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg=
|
||||
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||
github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ=
|
||||
github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw=
|
||||
github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI=
|
||||
github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
||||
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
@@ -222,29 +184,29 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
|
||||
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.38 h1:Mgt3XIIq47uR5vcLLahfRucE6tFPjxHak+z5ZZFEzLU=
|
||||
github.com/pion/interceptor v0.1.38/go.mod h1:HS9X+Ue5LDE6q2C2tuvOuO83XkBdJFgn6MBDtfoJX4Q=
|
||||
github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw=
|
||||
github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.15 h1:MuhuGn1cxpVCPLNY1lI7F1tQ8Spntpgf12ob+pOYT8s=
|
||||
github.com/pion/rtp v1.8.15/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.8.24 h1:+ICyZXUQDv95EsHN70RrA4XKJf5MGWyC6QQc1u6/ynI=
|
||||
github.com/pion/rtp v1.8.24/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8=
|
||||
github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo=
|
||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM=
|
||||
github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
@@ -253,43 +215,36 @@ github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1A
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v4 v4.1.1 h1:PMFPtLg1kpD2pVtun+LGUzA3k54JdFl87WO0Z1+HKug=
|
||||
github.com/pion/webrtc/v4 v4.1.1/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4=
|
||||
github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
|
||||
github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||
github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw=
|
||||
github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
|
||||
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
|
||||
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
|
||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70=
|
||||
github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
@@ -311,10 +266,8 @@ github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
@@ -323,15 +276,13 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
@@ -341,20 +292,20 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
||||
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -369,22 +320,20 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -406,8 +355,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -423,17 +372,14 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -444,13 +390,14 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef h1:5xFtU4tmJMJSxSeDlr1dgBff2tDXrq0laLdS1EA3LYw=
|
||||
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -467,24 +414,24 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -505,10 +452,9 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
|
||||
@@ -30,29 +30,12 @@ func InitWebRTCAPI() error {
|
||||
return fmt.Errorf("failed to register extensions: %w", err)
|
||||
}
|
||||
|
||||
// Default codecs cover most of our needs
|
||||
// Default codecs cover our needs
|
||||
err = mediaEngine.RegisterDefaultCodecs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add H.265 for special cases
|
||||
videoRTCPFeedback := []webrtc.RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH265, ClockRate: 90000, RTCPFeedback: videoRTCPFeedback},
|
||||
PayloadType: 48,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeRTX, ClockRate: 90000, SDPFmtpLine: "apt=48"},
|
||||
PayloadType: 49,
|
||||
},
|
||||
} {
|
||||
if err = mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Interceptor registry
|
||||
interceptorRegistry := &interceptor.Registry{}
|
||||
|
||||
@@ -98,7 +81,8 @@ func InitWebRTCAPI() error {
|
||||
slog.Info("Using WebRTC UDP Port Range", "start", flags.WebRTCUDPStart, "end", flags.WebRTCUDPEnd)
|
||||
}
|
||||
|
||||
settingEngine.SetIncludeLoopbackCandidate(true) // Just in case
|
||||
// Improves speed when sending offers to browsers (https://github.com/pion/webrtc/issues/3174)
|
||||
settingEngine.SetIncludeLoopbackCandidate(true)
|
||||
|
||||
// Create a new API object with our customized settings
|
||||
globalWebRTCAPI = webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithSettingEngine(settingEngine), webrtc.WithInterceptorRegistry(interceptorRegistry))
|
||||
|
||||
@@ -24,6 +24,8 @@ type Flags struct {
|
||||
AutoAddLocalIP bool // Automatically add local IP to NAT 1 to 1 IPs
|
||||
NAT11IP string // WebRTC NAT 1 to 1 IP - allows specifying IP of relay if behind NAT
|
||||
PersistDir string // Directory to save persistent data to
|
||||
Metrics bool // Enable metrics endpoint
|
||||
MetricsPort int // Port for metrics endpoint
|
||||
}
|
||||
|
||||
func (flags *Flags) DebugLog() {
|
||||
@@ -39,6 +41,8 @@ func (flags *Flags) DebugLog() {
|
||||
"autoAddLocalIP", flags.AutoAddLocalIP,
|
||||
"webrtcNAT11IPs", flags.NAT11IP,
|
||||
"persistDir", flags.PersistDir,
|
||||
"metrics", flags.Metrics,
|
||||
"metricsPort", flags.MetricsPort,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,12 +83,14 @@ func InitFlags() {
|
||||
flag.IntVar(&globalFlags.WebRTCUDPStart, "webrtcUDPStart", getEnvAsInt("WEBRTC_UDP_START", 0), "WebRTC UDP port range start")
|
||||
flag.IntVar(&globalFlags.WebRTCUDPEnd, "webrtcUDPEnd", getEnvAsInt("WEBRTC_UDP_END", 0), "WebRTC UDP port range end")
|
||||
flag.StringVar(&globalFlags.STUNServer, "stunServer", getEnvAsString("STUN_SERVER", "stun.l.google.com:19302"), "WebRTC STUN server")
|
||||
flag.IntVar(&globalFlags.UDPMuxPort, "webrtcUDPMux", getEnvAsInt("WEBRTC_UDP_MUX", 8088), "WebRTC UDP mux port")
|
||||
flag.BoolVar(&globalFlags.AutoAddLocalIP, "autoAddLocalIP", getEnvAsBool("AUTO_ADD_LOCAL_IP", true), "Automatically add local IP to NAT 1 to 1 IPs")
|
||||
flag.IntVar(&globalFlags.UDPMuxPort, "webrtcUDPMux", getEnvAsInt("WEBRTC_UDP_MUX", 9099), "WebRTC UDP mux port")
|
||||
flag.BoolVar(&globalFlags.AutoAddLocalIP, "autoAddLocalIP", getEnvAsBool("AUTO_ADD_LOCAL_IP", false), "Automatically add local IP to NAT 1 to 1 IPs")
|
||||
// String with comma separated IPs
|
||||
nat11IP := ""
|
||||
flag.StringVar(&nat11IP, "webrtcNAT11IP", getEnvAsString("WEBRTC_NAT_IP", ""), "WebRTC NAT 1 to 1 IP")
|
||||
flag.StringVar(&globalFlags.PersistDir, "persistDir", getEnvAsString("PERSIST_DIR", "./persist-data"), "Directory to save persistent data to")
|
||||
flag.BoolVar(&globalFlags.Metrics, "metrics", getEnvAsBool("METRICS", false), "Enable metrics endpoint")
|
||||
flag.IntVar(&globalFlags.MetricsPort, "metricsPort", getEnvAsInt("METRICS_PORT", 3030), "Port for metrics endpoint")
|
||||
// Parse flags
|
||||
flag.Parse()
|
||||
|
||||
|
||||
@@ -4,24 +4,31 @@ import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"relay/internal/common"
|
||||
"relay/internal/shared"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p"
|
||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
"github.com/libp2p/go-libp2p/core/network"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
|
||||
"github.com/libp2p/go-libp2p/p2p/protocol/ping"
|
||||
"github.com/libp2p/go-libp2p/p2p/security/noise"
|
||||
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
|
||||
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
|
||||
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
|
||||
webtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// -- Variables --
|
||||
@@ -30,17 +37,9 @@ var globalRelay *Relay
|
||||
|
||||
// -- Structs --
|
||||
|
||||
// RelayInfo contains light information of Relay, in mesh-friendly format
|
||||
type RelayInfo struct {
|
||||
ID peer.ID
|
||||
MeshAddrs []string // Addresses of this relay
|
||||
MeshRooms *common.SafeMap[string, shared.RoomInfo] // Rooms hosted by this relay
|
||||
MeshLatencies *common.SafeMap[string, time.Duration] // Latencies to other peers from this relay
|
||||
}
|
||||
|
||||
// Relay structure enhanced with metrics and state
|
||||
type Relay struct {
|
||||
RelayInfo
|
||||
*PeerInfo
|
||||
|
||||
Host host.Host // libp2p host for peer-to-peer networking
|
||||
PubSub *pubsub.PubSub // PubSub for state synchronization
|
||||
@@ -48,7 +47,6 @@ type Relay struct {
|
||||
|
||||
// Local
|
||||
LocalRooms *common.SafeMap[ulid.ULID, *shared.Room] // room ID -> local Room struct (hosted by this relay)
|
||||
LocalMeshPeers *common.SafeMap[peer.ID, *RelayInfo] // peer ID -> mesh peer relay info (connected to this relay)
|
||||
LocalMeshConnections *common.SafeMap[peer.ID, *webrtc.PeerConnection] // peer ID -> PeerConnection (connected to this relay)
|
||||
|
||||
// Protocols
|
||||
@@ -60,11 +58,43 @@ type Relay struct {
|
||||
}
|
||||
|
||||
func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay, error) {
|
||||
// If metrics are enabled, start the metrics server first
|
||||
metricsOpts := make([]libp2p.Option, 0)
|
||||
var rmgr network.ResourceManager
|
||||
if common.GetFlags().Metrics {
|
||||
go func() {
|
||||
slog.Info("Starting prometheus metrics server at '/debug/metrics/prometheus'", "port", common.GetFlags().MetricsPort)
|
||||
http.Handle("/debug/metrics/prometheus", promhttp.Handler())
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", common.GetFlags().MetricsPort), nil); err != nil {
|
||||
slog.Error("Failed to start metrics server", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
rcmgr.MustRegisterWith(prometheus.DefaultRegisterer)
|
||||
|
||||
str, err := rcmgr.NewStatsTraceReporter()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
rmgr, err = rcmgr.NewResourceManager(rcmgr.NewFixedLimiter(rcmgr.DefaultLimits.AutoScale()), rcmgr.WithTraceReporter(str))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
metricsOpts = append(metricsOpts, libp2p.ResourceManager(rmgr))
|
||||
metricsOpts = append(metricsOpts, libp2p.PrometheusRegisterer(prometheus.DefaultRegisterer))
|
||||
} else {
|
||||
rmgr = nil
|
||||
}
|
||||
|
||||
listenAddrs := []string{
|
||||
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4 - Raw TCP
|
||||
fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6 - Raw TCP
|
||||
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/ws", port), // IPv4 - TCP WebSocket
|
||||
fmt.Sprintf("/ip6/::/tcp/%d/ws", port), // IPv6 - TCP WebSocket
|
||||
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4 - Raw TCP
|
||||
fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6 - Raw TCP
|
||||
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/ws", port), // IPv4 - TCP WebSocket
|
||||
fmt.Sprintf("/ip6/::/tcp/%d/ws", port), // IPv6 - TCP WebSocket
|
||||
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1/webtransport", port), // IPv4 - UDP QUIC WebTransport
|
||||
fmt.Sprintf("/ip6/::/udp/%d/quic-v1/webtransport", port), // IPv6 - UDP QUIC WebTransport
|
||||
}
|
||||
|
||||
var muAddrs []multiaddr.Multiaddr
|
||||
@@ -78,11 +108,12 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
|
||||
|
||||
// Initialize libp2p host
|
||||
p2pHost, err := libp2p.New(
|
||||
// TODO: Currently static identity
|
||||
libp2p.ChainOptions(metricsOpts...),
|
||||
libp2p.Identity(identityKey),
|
||||
// Enable required transports
|
||||
libp2p.Transport(tcp.NewTCPTransport),
|
||||
libp2p.Transport(ws.New),
|
||||
libp2p.Transport(webtransport.New),
|
||||
// Other options
|
||||
libp2p.ListenAddrs(muAddrs...),
|
||||
libp2p.Security(noise.ID, noise.New),
|
||||
@@ -91,6 +122,7 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
|
||||
libp2p.EnableNATService(),
|
||||
libp2p.EnableAutoNATv2(),
|
||||
libp2p.ShareTCPListener(),
|
||||
libp2p.QUICReuse(quicreuse.NewConnManager),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create libp2p host for relay: %w", err)
|
||||
@@ -105,23 +137,13 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
|
||||
// Initialize Ping Service
|
||||
pingSvc := ping.NewPingService(p2pHost)
|
||||
|
||||
var addresses []string
|
||||
for _, addr := range p2pHost.Addrs() {
|
||||
addresses = append(addresses, addr.String())
|
||||
}
|
||||
|
||||
r := &Relay{
|
||||
RelayInfo: RelayInfo{
|
||||
ID: p2pHost.ID(),
|
||||
MeshAddrs: addresses,
|
||||
MeshRooms: common.NewSafeMap[string, shared.RoomInfo](),
|
||||
MeshLatencies: common.NewSafeMap[string, time.Duration](),
|
||||
},
|
||||
Host: p2pHost,
|
||||
PubSub: p2pPubsub,
|
||||
PingService: pingSvc,
|
||||
LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](),
|
||||
LocalMeshPeers: common.NewSafeMap[peer.ID, *RelayInfo](),
|
||||
PeerInfo: NewPeerInfo(p2pHost.ID(), p2pHost.Addrs()),
|
||||
Host: p2pHost,
|
||||
PubSub: p2pPubsub,
|
||||
PingService: pingSvc,
|
||||
LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](),
|
||||
LocalMeshConnections: common.NewSafeMap[peer.ID, *webrtc.PeerConnection](),
|
||||
}
|
||||
|
||||
// Add network notifier after relay is initialized
|
||||
@@ -152,7 +174,7 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
|
||||
func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) (*Relay, error) {
|
||||
var err error
|
||||
persistentDir := common.GetFlags().PersistDir
|
||||
|
||||
@@ -164,7 +186,7 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
|
||||
if hasIdentity {
|
||||
_, err = os.Stat(persistentDir + "/identity.key")
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to check identity key file: %w", err)
|
||||
return nil, fmt.Errorf("failed to check identity key file: %w", err)
|
||||
} else if os.IsNotExist(err) {
|
||||
hasIdentity = false
|
||||
}
|
||||
@@ -172,17 +194,17 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
|
||||
if !hasIdentity {
|
||||
// Make sure the persistent directory exists
|
||||
if err = os.MkdirAll(persistentDir, 0700); err != nil {
|
||||
return fmt.Errorf("failed to create persistent data directory: %w", err)
|
||||
return nil, fmt.Errorf("failed to create persistent data directory: %w", err)
|
||||
}
|
||||
// Generate
|
||||
slog.Info("Generating new identity for relay")
|
||||
privKey, err = common.GenerateED25519Key()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate new identity: %w", err)
|
||||
return nil, fmt.Errorf("failed to generate new identity: %w", err)
|
||||
}
|
||||
// Save the key
|
||||
if err = common.SaveED25519Key(privKey, persistentDir+"/identity.key"); err != nil {
|
||||
return fmt.Errorf("failed to save identity key: %w", err)
|
||||
return nil, fmt.Errorf("failed to save identity key: %w", err)
|
||||
}
|
||||
slog.Info("New identity generated and saved", "path", persistentDir+"/identity.key")
|
||||
} else {
|
||||
@@ -190,25 +212,45 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
|
||||
// Load the key
|
||||
privKey, err = common.LoadED25519Key(persistentDir + "/identity.key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load identity key: %w", err)
|
||||
return nil, fmt.Errorf("failed to load identity key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to libp2p crypto.PrivKey
|
||||
identityKey, err = crypto.UnmarshalEd25519PrivateKey(privKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal ED25519 private key: %w", err)
|
||||
return nil, fmt.Errorf("failed to unmarshal ED25519 private key: %w", err)
|
||||
}
|
||||
|
||||
globalRelay, err = NewRelay(ctx, common.GetFlags().EndpointPort, identityKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create relay: %w", err)
|
||||
return nil, fmt.Errorf("failed to create relay: %w", err)
|
||||
}
|
||||
|
||||
if err = common.InitWebRTCAPI(); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.Info("Relay initialized", "id", globalRelay.ID)
|
||||
return nil
|
||||
|
||||
// Load previous peers on startup
|
||||
defaultFile := common.GetFlags().PersistDir + "/peerstore.json"
|
||||
if err = globalRelay.LoadFromFile(defaultFile); err != nil {
|
||||
slog.Warn("Failed to load previous peer store", "error", err)
|
||||
} else {
|
||||
globalRelay.Peers.Range(func(id peer.ID, pi *PeerInfo) bool {
|
||||
if len(pi.Addrs) <= 0 {
|
||||
slog.Warn("Peer from peer store has no addresses", "peer", id)
|
||||
return true
|
||||
}
|
||||
|
||||
// Connect to first address only
|
||||
if err = globalRelay.ConnectToPeer(context.Background(), pi.Addrs[0]); err != nil {
|
||||
slog.Error("Failed to connect to peer from peer store", "peer", id, "error", err)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return globalRelay, nil
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type discoveryNotifee struct {
|
||||
|
||||
func (d *discoveryNotifee) HandlePeerFound(pi peer.AddrInfo) {
|
||||
if d.relay != nil {
|
||||
if err := d.relay.connectToRelay(context.Background(), &pi); err != nil {
|
||||
if err := d.relay.connectToPeer(context.Background(), &pi); err != nil {
|
||||
slog.Error("failed to connect to discovered relay", "peer", pi.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (r *Relay) publishRelayMetrics(ctx context.Context) error {
|
||||
// Check all peer latencies
|
||||
r.checkAllPeerLatencies(ctx)
|
||||
|
||||
data, err := json.Marshal(r.RelayInfo)
|
||||
data, err := json.Marshal(r.PeerInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal relay status: %w", err)
|
||||
}
|
||||
@@ -109,8 +109,8 @@ func (r *Relay) measureLatencyToPeer(ctx context.Context, peerID peer.ID) {
|
||||
if result.Error != nil {
|
||||
slog.Warn("Latency check failed, removing peer from local peers map", "peer", peerID, "err", result.Error)
|
||||
// Remove from MeshPeers if ping failed
|
||||
if r.LocalMeshPeers.Has(peerID) {
|
||||
r.LocalMeshPeers.Delete(peerID)
|
||||
if r.Peers.Has(peerID) {
|
||||
r.Peers.Delete(peerID)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -123,6 +123,6 @@ func (r *Relay) measureLatencyToPeer(ctx context.Context, peerID peer.ID) {
|
||||
latency = 1 * time.Microsecond
|
||||
}
|
||||
|
||||
r.RelayInfo.MeshLatencies.Set(peerID.String(), latency)
|
||||
r.PeerInfo.Latencies.Set(peerID, latency)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type networkNotifier struct {
|
||||
|
||||
// Connected is called when a connection is established
|
||||
func (n *networkNotifier) Connected(net network.Network, conn network.Conn) {
|
||||
if n.relay == nil {
|
||||
if n.relay != nil {
|
||||
n.relay.onPeerConnected(conn.RemotePeer())
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,8 @@ func (r *Relay) setupPubSub(ctx context.Context) error {
|
||||
|
||||
// --- Connection Management ---
|
||||
|
||||
// connectToRelay is internal method to connect to a relay peer using multiaddresses
|
||||
func (r *Relay) connectToRelay(ctx context.Context, peerInfo *peer.AddrInfo) error {
|
||||
// connectToPeer is internal method to connect to a peer using multiaddresses
|
||||
func (r *Relay) connectToPeer(ctx context.Context, peerInfo *peer.AddrInfo) error {
|
||||
if peerInfo.ID == r.ID {
|
||||
return errors.New("cannot connect to self")
|
||||
}
|
||||
@@ -94,19 +94,14 @@ func (r *Relay) connectToRelay(ctx context.Context, peerInfo *peer.AddrInfo) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectToRelay connects to another relay by its multiaddress.
|
||||
func (r *Relay) ConnectToRelay(ctx context.Context, addr string) error {
|
||||
ma, err := multiaddr.NewMultiaddr(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid multiaddress: %w", err)
|
||||
}
|
||||
|
||||
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
|
||||
// ConnectToPeer connects to another peer by its multiaddress.
|
||||
func (r *Relay) ConnectToPeer(ctx context.Context, addr multiaddr.Multiaddr) error {
|
||||
peerInfo, err := peer.AddrInfoFromP2pAddr(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract peer info: %w", err)
|
||||
}
|
||||
|
||||
return r.connectToRelay(ctx, peerInfo)
|
||||
return r.connectToPeer(ctx, peerInfo)
|
||||
}
|
||||
|
||||
// printConnectInstructions logs the multiaddresses for connecting to this relay.
|
||||
|
||||
77
packages/relay/internal/core/peer.go
Normal file
77
packages/relay/internal/core/peer.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"relay/internal/common"
|
||||
"relay/internal/shared"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
// PeerInfo contains information of a peer, in light transmit-friendly format
|
||||
type PeerInfo struct {
|
||||
ID peer.ID
|
||||
Addrs []multiaddr.Multiaddr // Addresses of this peer
|
||||
Peers *common.SafeMap[peer.ID, *PeerInfo] // Peers connected to this peer
|
||||
Latencies *common.SafeMap[peer.ID, time.Duration] // Latencies to other peers from this peer
|
||||
Rooms *common.SafeMap[string, shared.RoomInfo] // Rooms this peer is part of or owner of
|
||||
}
|
||||
|
||||
func NewPeerInfo(id peer.ID, addrs []multiaddr.Multiaddr) *PeerInfo {
|
||||
return &PeerInfo{
|
||||
ID: id,
|
||||
Addrs: addrs,
|
||||
Peers: common.NewSafeMap[peer.ID, *PeerInfo](),
|
||||
Latencies: common.NewSafeMap[peer.ID, time.Duration](),
|
||||
Rooms: common.NewSafeMap[string, shared.RoomInfo](),
|
||||
}
|
||||
}
|
||||
|
||||
// SaveToFile saves the peer store to a JSON file in persistent path
|
||||
func (pi *PeerInfo) SaveToFile(filePath string) error {
|
||||
if len(filePath) <= 0 {
|
||||
return errors.New("filepath is not set")
|
||||
}
|
||||
|
||||
// Marshal the peer store to JSON array (we don't need to store IDs..)
|
||||
data, err := pi.Peers.MarshalJSON()
|
||||
if err != nil {
|
||||
return errors.New("failed to marshal peer store data: " + err.Error())
|
||||
}
|
||||
|
||||
// Save the data to a file
|
||||
if err = os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return errors.New("failed to save peer store to file: " + err.Error())
|
||||
}
|
||||
|
||||
slog.Info("PeerStore saved to file", "path", filePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromFile loads the peer store from a JSON file in persistent path
|
||||
func (pi *PeerInfo) LoadFromFile(filePath string) error {
|
||||
if len(filePath) <= 0 {
|
||||
return errors.New("filepath is not set")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
slog.Info("PeerStore file does not exist, starting with empty store")
|
||||
return nil // No peers to load
|
||||
}
|
||||
return errors.New("failed to read peer store file: " + err.Error())
|
||||
}
|
||||
|
||||
// Unmarshal the JSON data into the peer store
|
||||
if err = pi.Peers.UnmarshalJSON(data); err != nil {
|
||||
return errors.New("failed to unmarshal peer store data: " + err.Error())
|
||||
}
|
||||
|
||||
slog.Info("PeerStore loaded from file", "path", filePath)
|
||||
return nil
|
||||
}
|
||||
@@ -40,15 +40,15 @@ type StreamConnection struct {
|
||||
// StreamProtocol deals with meshed stream forwarding
|
||||
type StreamProtocol struct {
|
||||
relay *Relay
|
||||
servedConns *common.SafeMap[peer.ID, *StreamConnection] // peer ID -> StreamConnection (for served streams)
|
||||
incomingConns *common.SafeMap[string, *StreamConnection] // room name -> StreamConnection (for incoming pushed streams)
|
||||
requestedConns *common.SafeMap[string, *StreamConnection] // room name -> StreamConnection (for requested streams from other relays)
|
||||
servedConns *common.SafeMap[string, *common.SafeMap[peer.ID, *StreamConnection]] // room name -> (peer ID -> StreamConnection) (for served streams)
|
||||
incomingConns *common.SafeMap[string, *StreamConnection] // room name -> StreamConnection (for incoming pushed streams)
|
||||
requestedConns *common.SafeMap[string, *StreamConnection] // room name -> StreamConnection (for requested streams from other relays)
|
||||
}
|
||||
|
||||
func NewStreamProtocol(relay *Relay) *StreamProtocol {
|
||||
protocol := &StreamProtocol{
|
||||
relay: relay,
|
||||
servedConns: common.NewSafeMap[peer.ID, *StreamConnection](),
|
||||
servedConns: common.NewSafeMap[string, *common.SafeMap[peer.ID, *StreamConnection]](),
|
||||
incomingConns: common.NewSafeMap[string, *StreamConnection](),
|
||||
requestedConns: common.NewSafeMap[string, *StreamConnection](),
|
||||
}
|
||||
@@ -66,6 +66,7 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(stream), bufio.NewWriter(stream))
|
||||
safeBRW := common.NewSafeBufioRW(brw)
|
||||
|
||||
var currentRoomName string // Track the current room for this stream
|
||||
iceHolder := make([]webrtc.ICECandidateInit, 0)
|
||||
for {
|
||||
data, err := safeBRW.Receive()
|
||||
@@ -101,7 +102,9 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
|
||||
continue
|
||||
}
|
||||
|
||||
currentRoomName = roomName // Store the room name
|
||||
slog.Info("Received stream request for room", "room", roomName)
|
||||
|
||||
room := sp.relay.GetRoomByName(roomName)
|
||||
if room == nil || !room.IsOnline() || room.OwnerID != sp.relay.ID {
|
||||
// TODO: Allow forward requests to other relays from here?
|
||||
@@ -126,8 +129,12 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
|
||||
pc, err := common.CreatePeerConnection(func() {
|
||||
slog.Info("PeerConnection closed for requested stream", "room", roomName)
|
||||
// Cleanup the stream connection
|
||||
if ok := sp.servedConns.Has(stream.Conn().RemotePeer()); ok {
|
||||
sp.servedConns.Delete(stream.Conn().RemotePeer())
|
||||
if roomMap, ok := sp.servedConns.Get(roomName); ok {
|
||||
roomMap.Delete(stream.Conn().RemotePeer())
|
||||
// If the room map is empty, delete it
|
||||
if roomMap.Len() == 0 {
|
||||
sp.servedConns.Delete(roomName)
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
@@ -204,7 +211,12 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
|
||||
}
|
||||
|
||||
// Store the connection
|
||||
sp.servedConns.Set(stream.Conn().RemotePeer(), &StreamConnection{
|
||||
roomMap, ok := sp.servedConns.Get(roomName)
|
||||
if !ok {
|
||||
roomMap = common.NewSafeMap[peer.ID, *StreamConnection]()
|
||||
sp.servedConns.Set(roomName, roomMap)
|
||||
}
|
||||
roomMap.Set(stream.Conn().RemotePeer(), &StreamConnection{
|
||||
pc: pc,
|
||||
ndc: ndc,
|
||||
})
|
||||
@@ -216,17 +228,25 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
|
||||
slog.Error("Failed to unmarshal ICE message", "err", err)
|
||||
continue
|
||||
}
|
||||
if conn, ok := sp.servedConns.Get(stream.Conn().RemotePeer()); ok && conn.pc.RemoteDescription() != nil {
|
||||
if err := conn.pc.AddICECandidate(iceMsg.Candidate); err != nil {
|
||||
slog.Error("Failed to add ICE candidate", "err", err)
|
||||
}
|
||||
for _, heldIce := range iceHolder {
|
||||
if err := conn.pc.AddICECandidate(heldIce); err != nil {
|
||||
slog.Error("Failed to add held ICE candidate", "err", err)
|
||||
// Use currentRoomName to get the connection from nested map
|
||||
if len(currentRoomName) > 0 {
|
||||
if roomMap, ok := sp.servedConns.Get(currentRoomName); ok {
|
||||
if conn, ok := roomMap.Get(stream.Conn().RemotePeer()); ok && conn.pc.RemoteDescription() != nil {
|
||||
if err := conn.pc.AddICECandidate(iceMsg.Candidate); err != nil {
|
||||
slog.Error("Failed to add ICE candidate", "err", err)
|
||||
}
|
||||
for _, heldIce := range iceHolder {
|
||||
if err := conn.pc.AddICECandidate(heldIce); err != nil {
|
||||
slog.Error("Failed to add held ICE candidate", "err", err)
|
||||
}
|
||||
}
|
||||
// Clear the held candidates
|
||||
iceHolder = make([]webrtc.ICECandidateInit, 0)
|
||||
} else {
|
||||
// Hold the candidate until remote description is set
|
||||
iceHolder = append(iceHolder, iceMsg.Candidate)
|
||||
}
|
||||
}
|
||||
// Clear the held candidates
|
||||
iceHolder = make([]webrtc.ICECandidateInit, 0)
|
||||
} else {
|
||||
// Hold the candidate until remote description is set
|
||||
iceHolder = append(iceHolder, iceMsg.Candidate)
|
||||
@@ -237,12 +257,19 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
|
||||
slog.Error("Failed to unmarshal answer from signaling message", "err", err)
|
||||
continue
|
||||
}
|
||||
if conn, ok := sp.servedConns.Get(stream.Conn().RemotePeer()); ok {
|
||||
if err := conn.pc.SetRemoteDescription(answerMsg.SDP); err != nil {
|
||||
slog.Error("Failed to set remote description for answer", "err", err)
|
||||
continue
|
||||
// Use currentRoomName to get the connection from nested map
|
||||
if len(currentRoomName) > 0 {
|
||||
if roomMap, ok := sp.servedConns.Get(currentRoomName); ok {
|
||||
if conn, ok := roomMap.Get(stream.Conn().RemotePeer()); ok {
|
||||
if err := conn.pc.SetRemoteDescription(answerMsg.SDP); err != nil {
|
||||
slog.Error("Failed to set remote description for answer", "err", err)
|
||||
continue
|
||||
}
|
||||
slog.Debug("Set remote description for answer")
|
||||
} else {
|
||||
slog.Warn("Received answer without active PeerConnection")
|
||||
}
|
||||
}
|
||||
slog.Debug("Set remote description for answer")
|
||||
} else {
|
||||
slog.Warn("Received answer without active PeerConnection")
|
||||
}
|
||||
@@ -452,7 +479,7 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
|
||||
data, err := safeBRW.Receive()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, network.ErrReset) {
|
||||
slog.Debug("Stream push connection closed by peer", "peer", stream.Conn().RemotePeer())
|
||||
slog.Debug("Stream push connection closed by peer", "peer", stream.Conn().RemotePeer(), "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -568,6 +595,21 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
|
||||
room.DataChannel.RegisterOnClose(func() {
|
||||
slog.Debug("DataChannel closed for pushed stream", "room", room.Name)
|
||||
})
|
||||
room.DataChannel.RegisterMessageCallback("input", func(data []byte) {
|
||||
if room.DataChannel != nil {
|
||||
// Pass to servedConns DataChannels for this specific room
|
||||
if roomMap, ok := sp.servedConns.Get(room.Name); ok {
|
||||
roomMap.Range(func(peerID peer.ID, conn *StreamConnection) bool {
|
||||
if conn.ndc != nil {
|
||||
if err = conn.ndc.SendBinary(data); err != nil {
|
||||
slog.Error("Failed to forward input message from pushed stream to viewer", "room", room.Name, "peer", peerID, "err", err)
|
||||
}
|
||||
}
|
||||
return true // Continue iteration
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Set the DataChannel in the incomingConns map
|
||||
if conn, ok := sp.incomingConns.Get(room.Name); ok {
|
||||
@@ -687,7 +729,7 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
|
||||
func (sp *StreamProtocol) RequestStream(ctx context.Context, room *shared.Room, peerID peer.ID) error {
|
||||
stream, err := sp.relay.Host.NewStream(ctx, peerID, protocolStreamRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stream request: %w", err)
|
||||
return fmt.Errorf("failed to create stream: %w", err)
|
||||
}
|
||||
|
||||
return sp.requestStream(stream, room)
|
||||
|
||||
@@ -57,15 +57,15 @@ func (r *Relay) DeleteRoomIfEmpty(room *shared.Room) {
|
||||
|
||||
// GetRemoteRoomByName returns room from mesh by name
|
||||
func (r *Relay) GetRemoteRoomByName(roomName string) *shared.RoomInfo {
|
||||
for _, room := range r.MeshRooms.Copy() {
|
||||
for _, room := range r.Rooms.Copy() {
|
||||
if room.Name == roomName && room.OwnerID != r.ID {
|
||||
// Make sure connection is alive
|
||||
if r.Host.Network().Connectedness(room.OwnerID) == network.Connected {
|
||||
return &room
|
||||
} else {
|
||||
slog.Debug("Removing stale peer, owns a room without connection", "room", roomName, "peer", room.OwnerID)
|
||||
r.onPeerDisconnected(room.OwnerID)
|
||||
}
|
||||
|
||||
slog.Debug("Removing stale peer, owns a room without connection", "room", roomName, "peer", room.OwnerID)
|
||||
r.onPeerDisconnected(room.OwnerID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -72,8 +72,8 @@ func (r *Relay) handleRelayMetricsMessages(ctx context.Context, sub *pubsub.Subs
|
||||
continue
|
||||
}
|
||||
|
||||
var info RelayInfo
|
||||
if err := json.Unmarshal(msg.Data, &info); err != nil {
|
||||
var info PeerInfo
|
||||
if err = json.Unmarshal(msg.Data, &info); err != nil {
|
||||
slog.Error("Failed to unmarshal relay status", "from", msg.GetFrom(), "data_len", len(msg.Data), "err", err)
|
||||
continue
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func (r *Relay) handleRelayMetricsMessages(ctx context.Context, sub *pubsub.Subs
|
||||
// --- State Check Functions ---
|
||||
// hasConnectedPeer checks if peer is in map and has a valid connection
|
||||
func (r *Relay) hasConnectedPeer(peerID peer.ID) bool {
|
||||
if _, ok := r.LocalMeshPeers.Get(peerID); !ok {
|
||||
if _, ok := r.Peers.Get(peerID); !ok {
|
||||
return false
|
||||
}
|
||||
if r.Host.Network().Connectedness(peerID) != network.Connected {
|
||||
@@ -102,14 +102,14 @@ func (r *Relay) hasConnectedPeer(peerID peer.ID) bool {
|
||||
// --- State Change Functions ---
|
||||
|
||||
// onPeerStatus updates the status of a peer based on received metrics, adding local perspective
|
||||
func (r *Relay) onPeerStatus(recvInfo RelayInfo) {
|
||||
r.LocalMeshPeers.Set(recvInfo.ID, &recvInfo)
|
||||
func (r *Relay) onPeerStatus(recvInfo PeerInfo) {
|
||||
r.Peers.Set(recvInfo.ID, &recvInfo)
|
||||
}
|
||||
|
||||
// onPeerConnected is called when a new peer connects to the relay
|
||||
func (r *Relay) onPeerConnected(peerID peer.ID) {
|
||||
// Add to local peer map
|
||||
r.LocalMeshPeers.Set(peerID, &RelayInfo{
|
||||
r.Peers.Set(peerID, &PeerInfo{
|
||||
ID: peerID,
|
||||
})
|
||||
|
||||
@@ -131,16 +131,12 @@ func (r *Relay) onPeerConnected(peerID peer.ID) {
|
||||
func (r *Relay) onPeerDisconnected(peerID peer.ID) {
|
||||
slog.Info("Mesh peer disconnected, deleting from local peer map", "peer", peerID)
|
||||
// Remove peer from local mesh peers
|
||||
if r.LocalMeshPeers.Has(peerID) {
|
||||
r.LocalMeshPeers.Delete(peerID)
|
||||
if r.Peers.Has(peerID) {
|
||||
r.Peers.Delete(peerID)
|
||||
}
|
||||
// Remove any rooms associated with this peer
|
||||
if r.MeshRooms.Has(peerID.String()) {
|
||||
r.MeshRooms.Delete(peerID.String())
|
||||
}
|
||||
// Remove any latencies associated with this peer
|
||||
if r.LocalMeshPeers.Has(peerID) {
|
||||
r.LocalMeshPeers.Delete(peerID)
|
||||
if r.Rooms.Has(peerID.String()) {
|
||||
r.Rooms.Delete(peerID.String())
|
||||
}
|
||||
|
||||
// TODO: If any rooms were routed through this peer, handle that case
|
||||
@@ -155,7 +151,7 @@ func (r *Relay) updateMeshRoomStates(peerID peer.ID, states []shared.RoomInfo) {
|
||||
}
|
||||
|
||||
// If previously did not exist, but does now, request a connection if participants exist for our room
|
||||
existed := r.MeshRooms.Has(state.ID.String())
|
||||
existed := r.Rooms.Has(state.ID.String())
|
||||
if !existed {
|
||||
// Request connection to this peer if we have participants in our local room
|
||||
if room, ok := r.LocalRooms.Get(state.ID); ok {
|
||||
@@ -168,6 +164,6 @@ func (r *Relay) updateMeshRoomStates(peerID peer.ID, states []shared.RoomInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
r.MeshRooms.Set(state.ID.String(), state)
|
||||
r.Rooms.Set(state.ID.String(), state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc (unknown)
|
||||
// source: latency_tracker.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc (unknown)
|
||||
// source: messages.proto
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc (unknown)
|
||||
// source: types.proto
|
||||
|
||||
@@ -416,6 +416,481 @@ func (x *ProtoKeyUp) GetKey() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// ControllerAttach message
|
||||
type ProtoControllerAttach struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerAttach"
|
||||
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` // One of the following enums: "ps", "xbox" or "switch"
|
||||
Slot int32 `protobuf:"varint,3,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAttach) Reset() {
|
||||
*x = ProtoControllerAttach{}
|
||||
mi := &file_types_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAttach) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProtoControllerAttach) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoControllerAttach) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ProtoControllerAttach.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoControllerAttach) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAttach) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAttach) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAttach) GetSlot() int32 {
|
||||
if x != nil {
|
||||
return x.Slot
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ControllerDetach message
|
||||
type ProtoControllerDetach struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerDetach"
|
||||
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProtoControllerDetach) Reset() {
|
||||
*x = ProtoControllerDetach{}
|
||||
mi := &file_types_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProtoControllerDetach) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProtoControllerDetach) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoControllerDetach) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ProtoControllerDetach.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoControllerDetach) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *ProtoControllerDetach) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProtoControllerDetach) GetSlot() int32 {
|
||||
if x != nil {
|
||||
return x.Slot
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ControllerButton message
|
||||
type ProtoControllerButton struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerButtons"
|
||||
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProtoControllerButton) Reset() {
|
||||
*x = ProtoControllerButton{}
|
||||
mi := &file_types_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProtoControllerButton) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProtoControllerButton) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoControllerButton) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ProtoControllerButton.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoControllerButton) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *ProtoControllerButton) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProtoControllerButton) GetSlot() int32 {
|
||||
if x != nil {
|
||||
return x.Slot
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerButton) GetButton() int32 {
|
||||
if x != nil {
|
||||
return x.Button
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerButton) GetPressed() bool {
|
||||
if x != nil {
|
||||
return x.Pressed
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ControllerTriggers message
|
||||
type ProtoControllerTrigger struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerTriggers"
|
||||
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProtoControllerTrigger) Reset() {
|
||||
*x = ProtoControllerTrigger{}
|
||||
mi := &file_types_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProtoControllerTrigger) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProtoControllerTrigger) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoControllerTrigger) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ProtoControllerTrigger.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoControllerTrigger) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *ProtoControllerTrigger) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProtoControllerTrigger) GetSlot() int32 {
|
||||
if x != nil {
|
||||
return x.Slot
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerTrigger) GetTrigger() int32 {
|
||||
if x != nil {
|
||||
return x.Trigger
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerTrigger) GetValue() int32 {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ControllerSticks message
|
||||
type ProtoControllerStick struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerStick"
|
||||
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
|
||||
Stick int32 `protobuf:"varint,3,opt,name=stick,proto3" json:"stick,omitempty"` // Stick number (0 for left, 1 for right)
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProtoControllerStick) Reset() {
|
||||
*x = ProtoControllerStick{}
|
||||
mi := &file_types_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProtoControllerStick) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProtoControllerStick) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoControllerStick) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ProtoControllerStick.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoControllerStick) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *ProtoControllerStick) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProtoControllerStick) GetSlot() int32 {
|
||||
if x != nil {
|
||||
return x.Slot
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerStick) GetStick() int32 {
|
||||
if x != nil {
|
||||
return x.Stick
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerStick) GetX() int32 {
|
||||
if x != nil {
|
||||
return x.X
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerStick) GetY() int32 {
|
||||
if x != nil {
|
||||
return x.Y
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ControllerAxis message
|
||||
type ProtoControllerAxis struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerAxis"
|
||||
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAxis) Reset() {
|
||||
*x = ProtoControllerAxis{}
|
||||
mi := &file_types_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAxis) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProtoControllerAxis) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoControllerAxis) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ProtoControllerAxis.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoControllerAxis) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAxis) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAxis) GetSlot() int32 {
|
||||
if x != nil {
|
||||
return x.Slot
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAxis) GetAxis() int32 {
|
||||
if x != nil {
|
||||
return x.Axis
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerAxis) GetValue() int32 {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ControllerRumble message
|
||||
type ProtoControllerRumble struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerRumble"
|
||||
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
|
||||
LowFrequency int32 `protobuf:"varint,3,opt,name=low_frequency,json=lowFrequency,proto3" json:"low_frequency,omitempty"` // Low frequency rumble (0-65535)
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProtoControllerRumble) Reset() {
|
||||
*x = ProtoControllerRumble{}
|
||||
mi := &file_types_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ProtoControllerRumble) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ProtoControllerRumble) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoControllerRumble) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ProtoControllerRumble.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoControllerRumble) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *ProtoControllerRumble) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProtoControllerRumble) GetSlot() int32 {
|
||||
if x != nil {
|
||||
return x.Slot
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerRumble) GetLowFrequency() int32 {
|
||||
if x != nil {
|
||||
return x.LowFrequency
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerRumble) GetHighFrequency() int32 {
|
||||
if x != nil {
|
||||
return x.HighFrequency
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProtoControllerRumble) GetDuration() int32 {
|
||||
if x != nil {
|
||||
return x.Duration
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Union of all Input types
|
||||
type ProtoInput struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@@ -428,6 +903,13 @@ type ProtoInput struct {
|
||||
// *ProtoInput_MouseKeyUp
|
||||
// *ProtoInput_KeyDown
|
||||
// *ProtoInput_KeyUp
|
||||
// *ProtoInput_ControllerAttach
|
||||
// *ProtoInput_ControllerDetach
|
||||
// *ProtoInput_ControllerButton
|
||||
// *ProtoInput_ControllerTrigger
|
||||
// *ProtoInput_ControllerStick
|
||||
// *ProtoInput_ControllerAxis
|
||||
// *ProtoInput_ControllerRumble
|
||||
InputType isProtoInput_InputType `protobuf_oneof:"input_type"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -435,7 +917,7 @@ type ProtoInput struct {
|
||||
|
||||
func (x *ProtoInput) Reset() {
|
||||
*x = ProtoInput{}
|
||||
mi := &file_types_proto_msgTypes[7]
|
||||
mi := &file_types_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -447,7 +929,7 @@ func (x *ProtoInput) String() string {
|
||||
func (*ProtoInput) ProtoMessage() {}
|
||||
|
||||
func (x *ProtoInput) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[7]
|
||||
mi := &file_types_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -460,7 +942,7 @@ func (x *ProtoInput) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ProtoInput.ProtoReflect.Descriptor instead.
|
||||
func (*ProtoInput) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{7}
|
||||
return file_types_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *ProtoInput) GetInputType() isProtoInput_InputType {
|
||||
@@ -533,6 +1015,69 @@ func (x *ProtoInput) GetKeyUp() *ProtoKeyUp {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoInput) GetControllerAttach() *ProtoControllerAttach {
|
||||
if x != nil {
|
||||
if x, ok := x.InputType.(*ProtoInput_ControllerAttach); ok {
|
||||
return x.ControllerAttach
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoInput) GetControllerDetach() *ProtoControllerDetach {
|
||||
if x != nil {
|
||||
if x, ok := x.InputType.(*ProtoInput_ControllerDetach); ok {
|
||||
return x.ControllerDetach
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoInput) GetControllerButton() *ProtoControllerButton {
|
||||
if x != nil {
|
||||
if x, ok := x.InputType.(*ProtoInput_ControllerButton); ok {
|
||||
return x.ControllerButton
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoInput) GetControllerTrigger() *ProtoControllerTrigger {
|
||||
if x != nil {
|
||||
if x, ok := x.InputType.(*ProtoInput_ControllerTrigger); ok {
|
||||
return x.ControllerTrigger
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoInput) GetControllerStick() *ProtoControllerStick {
|
||||
if x != nil {
|
||||
if x, ok := x.InputType.(*ProtoInput_ControllerStick); ok {
|
||||
return x.ControllerStick
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoInput) GetControllerAxis() *ProtoControllerAxis {
|
||||
if x != nil {
|
||||
if x, ok := x.InputType.(*ProtoInput_ControllerAxis); ok {
|
||||
return x.ControllerAxis
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ProtoInput) GetControllerRumble() *ProtoControllerRumble {
|
||||
if x != nil {
|
||||
if x, ok := x.InputType.(*ProtoInput_ControllerRumble); ok {
|
||||
return x.ControllerRumble
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isProtoInput_InputType interface {
|
||||
isProtoInput_InputType()
|
||||
}
|
||||
@@ -565,6 +1110,34 @@ type ProtoInput_KeyUp struct {
|
||||
KeyUp *ProtoKeyUp `protobuf:"bytes,7,opt,name=key_up,json=keyUp,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoInput_ControllerAttach struct {
|
||||
ControllerAttach *ProtoControllerAttach `protobuf:"bytes,8,opt,name=controller_attach,json=controllerAttach,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoInput_ControllerDetach struct {
|
||||
ControllerDetach *ProtoControllerDetach `protobuf:"bytes,9,opt,name=controller_detach,json=controllerDetach,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoInput_ControllerButton struct {
|
||||
ControllerButton *ProtoControllerButton `protobuf:"bytes,10,opt,name=controller_button,json=controllerButton,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoInput_ControllerTrigger struct {
|
||||
ControllerTrigger *ProtoControllerTrigger `protobuf:"bytes,11,opt,name=controller_trigger,json=controllerTrigger,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoInput_ControllerStick struct {
|
||||
ControllerStick *ProtoControllerStick `protobuf:"bytes,12,opt,name=controller_stick,json=controllerStick,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoInput_ControllerAxis struct {
|
||||
ControllerAxis *ProtoControllerAxis `protobuf:"bytes,13,opt,name=controller_axis,json=controllerAxis,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ProtoInput_ControllerRumble struct {
|
||||
ControllerRumble *ProtoControllerRumble `protobuf:"bytes,14,opt,name=controller_rumble,json=controllerRumble,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*ProtoInput_MouseMove) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_MouseMoveAbs) isProtoInput_InputType() {}
|
||||
@@ -579,6 +1152,20 @@ func (*ProtoInput_KeyDown) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_KeyUp) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_ControllerAttach) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_ControllerDetach) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_ControllerButton) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_ControllerTrigger) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_ControllerStick) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_ControllerAxis) isProtoInput_InputType() {}
|
||||
|
||||
func (*ProtoInput_ControllerRumble) isProtoInput_InputType() {}
|
||||
|
||||
var File_types_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_types_proto_rawDesc = "" +
|
||||
@@ -608,7 +1195,41 @@ const file_types_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"ProtoKeyUp\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" +
|
||||
"\x03key\x18\x02 \x01(\x05R\x03key\"\xab\x03\n" +
|
||||
"\x03key\x18\x02 \x01(\x05R\x03key\"O\n" +
|
||||
"\x15ProtoControllerAttach\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04slot\x18\x03 \x01(\x05R\x04slot\"?\n" +
|
||||
"\x15ProtoControllerDetach\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04slot\x18\x02 \x01(\x05R\x04slot\"q\n" +
|
||||
"\x15ProtoControllerButton\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x16\n" +
|
||||
"\x06button\x18\x03 \x01(\x05R\x06button\x12\x18\n" +
|
||||
"\apressed\x18\x04 \x01(\bR\apressed\"p\n" +
|
||||
"\x16ProtoControllerTrigger\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x18\n" +
|
||||
"\atrigger\x18\x03 \x01(\x05R\atrigger\x12\x14\n" +
|
||||
"\x05value\x18\x04 \x01(\x05R\x05value\"p\n" +
|
||||
"\x14ProtoControllerStick\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x14\n" +
|
||||
"\x05stick\x18\x03 \x01(\x05R\x05stick\x12\f\n" +
|
||||
"\x01x\x18\x04 \x01(\x05R\x01x\x12\f\n" +
|
||||
"\x01y\x18\x05 \x01(\x05R\x01y\"g\n" +
|
||||
"\x13ProtoControllerAxis\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x12\n" +
|
||||
"\x04axis\x18\x03 \x01(\x05R\x04axis\x12\x14\n" +
|
||||
"\x05value\x18\x04 \x01(\x05R\x05value\"\xa7\x01\n" +
|
||||
"\x15ProtoControllerRumble\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04slot\x18\x02 \x01(\x05R\x04slot\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\"\xc0\a\n" +
|
||||
"\n" +
|
||||
"ProtoInput\x126\n" +
|
||||
"\n" +
|
||||
@@ -620,7 +1241,15 @@ const file_types_proto_rawDesc = "" +
|
||||
"\fmouse_key_up\x18\x05 \x01(\v2\x16.proto.ProtoMouseKeyUpH\x00R\n" +
|
||||
"mouseKeyUp\x120\n" +
|
||||
"\bkey_down\x18\x06 \x01(\v2\x13.proto.ProtoKeyDownH\x00R\akeyDown\x12*\n" +
|
||||
"\x06key_up\x18\a \x01(\v2\x11.proto.ProtoKeyUpH\x00R\x05keyUpB\f\n" +
|
||||
"\x06key_up\x18\a \x01(\v2\x11.proto.ProtoKeyUpH\x00R\x05keyUp\x12K\n" +
|
||||
"\x11controller_attach\x18\b \x01(\v2\x1c.proto.ProtoControllerAttachH\x00R\x10controllerAttach\x12K\n" +
|
||||
"\x11controller_detach\x18\t \x01(\v2\x1c.proto.ProtoControllerDetachH\x00R\x10controllerDetach\x12K\n" +
|
||||
"\x11controller_button\x18\n" +
|
||||
" \x01(\v2\x1c.proto.ProtoControllerButtonH\x00R\x10controllerButton\x12N\n" +
|
||||
"\x12controller_trigger\x18\v \x01(\v2\x1d.proto.ProtoControllerTriggerH\x00R\x11controllerTrigger\x12H\n" +
|
||||
"\x10controller_stick\x18\f \x01(\v2\x1b.proto.ProtoControllerStickH\x00R\x0fcontrollerStick\x12E\n" +
|
||||
"\x0fcontroller_axis\x18\r \x01(\v2\x1a.proto.ProtoControllerAxisH\x00R\x0econtrollerAxis\x12K\n" +
|
||||
"\x11controller_rumble\x18\x0e \x01(\v2\x1c.proto.ProtoControllerRumbleH\x00R\x10controllerRumbleB\f\n" +
|
||||
"\n" +
|
||||
"input_typeB\x16Z\x14relay/internal/protob\x06proto3"
|
||||
|
||||
@@ -636,30 +1265,44 @@ func file_types_proto_rawDescGZIP() []byte {
|
||||
return file_types_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
|
||||
var file_types_proto_goTypes = []any{
|
||||
(*ProtoMouseMove)(nil), // 0: proto.ProtoMouseMove
|
||||
(*ProtoMouseMoveAbs)(nil), // 1: proto.ProtoMouseMoveAbs
|
||||
(*ProtoMouseWheel)(nil), // 2: proto.ProtoMouseWheel
|
||||
(*ProtoMouseKeyDown)(nil), // 3: proto.ProtoMouseKeyDown
|
||||
(*ProtoMouseKeyUp)(nil), // 4: proto.ProtoMouseKeyUp
|
||||
(*ProtoKeyDown)(nil), // 5: proto.ProtoKeyDown
|
||||
(*ProtoKeyUp)(nil), // 6: proto.ProtoKeyUp
|
||||
(*ProtoInput)(nil), // 7: proto.ProtoInput
|
||||
(*ProtoMouseMove)(nil), // 0: proto.ProtoMouseMove
|
||||
(*ProtoMouseMoveAbs)(nil), // 1: proto.ProtoMouseMoveAbs
|
||||
(*ProtoMouseWheel)(nil), // 2: proto.ProtoMouseWheel
|
||||
(*ProtoMouseKeyDown)(nil), // 3: proto.ProtoMouseKeyDown
|
||||
(*ProtoMouseKeyUp)(nil), // 4: proto.ProtoMouseKeyUp
|
||||
(*ProtoKeyDown)(nil), // 5: proto.ProtoKeyDown
|
||||
(*ProtoKeyUp)(nil), // 6: proto.ProtoKeyUp
|
||||
(*ProtoControllerAttach)(nil), // 7: proto.ProtoControllerAttach
|
||||
(*ProtoControllerDetach)(nil), // 8: proto.ProtoControllerDetach
|
||||
(*ProtoControllerButton)(nil), // 9: proto.ProtoControllerButton
|
||||
(*ProtoControllerTrigger)(nil), // 10: proto.ProtoControllerTrigger
|
||||
(*ProtoControllerStick)(nil), // 11: proto.ProtoControllerStick
|
||||
(*ProtoControllerAxis)(nil), // 12: proto.ProtoControllerAxis
|
||||
(*ProtoControllerRumble)(nil), // 13: proto.ProtoControllerRumble
|
||||
(*ProtoInput)(nil), // 14: proto.ProtoInput
|
||||
}
|
||||
var file_types_proto_depIdxs = []int32{
|
||||
0, // 0: proto.ProtoInput.mouse_move:type_name -> proto.ProtoMouseMove
|
||||
1, // 1: proto.ProtoInput.mouse_move_abs:type_name -> proto.ProtoMouseMoveAbs
|
||||
2, // 2: proto.ProtoInput.mouse_wheel:type_name -> proto.ProtoMouseWheel
|
||||
3, // 3: proto.ProtoInput.mouse_key_down:type_name -> proto.ProtoMouseKeyDown
|
||||
4, // 4: proto.ProtoInput.mouse_key_up:type_name -> proto.ProtoMouseKeyUp
|
||||
5, // 5: proto.ProtoInput.key_down:type_name -> proto.ProtoKeyDown
|
||||
6, // 6: proto.ProtoInput.key_up:type_name -> proto.ProtoKeyUp
|
||||
7, // [7:7] is the sub-list for method output_type
|
||||
7, // [7:7] is the sub-list for method input_type
|
||||
7, // [7:7] is the sub-list for extension type_name
|
||||
7, // [7:7] is the sub-list for extension extendee
|
||||
0, // [0:7] is the sub-list for field type_name
|
||||
0, // 0: proto.ProtoInput.mouse_move:type_name -> proto.ProtoMouseMove
|
||||
1, // 1: proto.ProtoInput.mouse_move_abs:type_name -> proto.ProtoMouseMoveAbs
|
||||
2, // 2: proto.ProtoInput.mouse_wheel:type_name -> proto.ProtoMouseWheel
|
||||
3, // 3: proto.ProtoInput.mouse_key_down:type_name -> proto.ProtoMouseKeyDown
|
||||
4, // 4: proto.ProtoInput.mouse_key_up:type_name -> proto.ProtoMouseKeyUp
|
||||
5, // 5: proto.ProtoInput.key_down:type_name -> proto.ProtoKeyDown
|
||||
6, // 6: proto.ProtoInput.key_up:type_name -> proto.ProtoKeyUp
|
||||
7, // 7: proto.ProtoInput.controller_attach:type_name -> proto.ProtoControllerAttach
|
||||
8, // 8: proto.ProtoInput.controller_detach:type_name -> proto.ProtoControllerDetach
|
||||
9, // 9: proto.ProtoInput.controller_button:type_name -> proto.ProtoControllerButton
|
||||
10, // 10: proto.ProtoInput.controller_trigger:type_name -> proto.ProtoControllerTrigger
|
||||
11, // 11: proto.ProtoInput.controller_stick:type_name -> proto.ProtoControllerStick
|
||||
12, // 12: proto.ProtoInput.controller_axis:type_name -> proto.ProtoControllerAxis
|
||||
13, // 13: proto.ProtoInput.controller_rumble:type_name -> proto.ProtoControllerRumble
|
||||
14, // [14:14] is the sub-list for method output_type
|
||||
14, // [14:14] is the sub-list for method input_type
|
||||
14, // [14:14] is the sub-list for extension type_name
|
||||
14, // [14:14] is the sub-list for extension extendee
|
||||
0, // [0:14] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_types_proto_init() }
|
||||
@@ -667,7 +1310,7 @@ func file_types_proto_init() {
|
||||
if File_types_proto != nil {
|
||||
return
|
||||
}
|
||||
file_types_proto_msgTypes[7].OneofWrappers = []any{
|
||||
file_types_proto_msgTypes[14].OneofWrappers = []any{
|
||||
(*ProtoInput_MouseMove)(nil),
|
||||
(*ProtoInput_MouseMoveAbs)(nil),
|
||||
(*ProtoInput_MouseWheel)(nil),
|
||||
@@ -675,6 +1318,13 @@ func file_types_proto_init() {
|
||||
(*ProtoInput_MouseKeyUp)(nil),
|
||||
(*ProtoInput_KeyDown)(nil),
|
||||
(*ProtoInput_KeyUp)(nil),
|
||||
(*ProtoInput_ControllerAttach)(nil),
|
||||
(*ProtoInput_ControllerDetach)(nil),
|
||||
(*ProtoInput_ControllerButton)(nil),
|
||||
(*ProtoInput_ControllerTrigger)(nil),
|
||||
(*ProtoInput_ControllerStick)(nil),
|
||||
(*ProtoInput_ControllerAxis)(nil),
|
||||
(*ProtoInput_ControllerRumble)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -682,7 +1332,7 @@ func file_types_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 8,
|
||||
NumMessages: 15,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -49,25 +49,12 @@ func (r *Room) removeParticipantByID(pID ulid.ULID) {
|
||||
}
|
||||
}
|
||||
|
||||
// Removes all participants from a Room
|
||||
/*func (r *Room) removeAllParticipants() {
|
||||
for id, participant := range r.Participants.Copy() {
|
||||
if err := r.signalParticipantOffline(participant); err != nil {
|
||||
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
|
||||
}
|
||||
r.Participants.Delete(id)
|
||||
slog.Debug("Removed participant from room", "participant", id, "room", r.Name)
|
||||
}
|
||||
}*/
|
||||
|
||||
// IsOnline checks if the room is online (has both audio and video tracks)
|
||||
func (r *Room) IsOnline() bool {
|
||||
return r.AudioTrack != nil && r.VideoTrack != nil
|
||||
}
|
||||
|
||||
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
|
||||
//oldOnline := r.IsOnline()
|
||||
|
||||
switch trackType {
|
||||
case webrtc.RTPCodecTypeAudio:
|
||||
r.AudioTrack = track
|
||||
@@ -76,69 +63,4 @@ func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalS
|
||||
default:
|
||||
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
|
||||
}
|
||||
|
||||
/*newOnline := r.IsOnline()
|
||||
if oldOnline != newOnline {
|
||||
if newOnline {
|
||||
slog.Debug("Room online, participants will be signaled", "room", r.Name)
|
||||
r.signalParticipantsWithTracks()
|
||||
} else {
|
||||
slog.Debug("Room offline, signaling participants", "room", r.Name)
|
||||
r.signalParticipantsOffline()
|
||||
}
|
||||
|
||||
// TODO: Publish updated state to mesh
|
||||
go func() {
|
||||
if err := r.Relay.publishRoomStates(context.Background()); err != nil {
|
||||
slog.Error("Failed to publish room states on change", "room", r.Name, "err", err)
|
||||
}
|
||||
}()
|
||||
}*/
|
||||
}
|
||||
|
||||
/* TODO: libp2p'ify
|
||||
func (r *Room) signalParticipantsWithTracks() {
|
||||
for _, participant := range r.Participants.Copy() {
|
||||
if err := r.signalParticipantWithTracks(participant); err != nil {
|
||||
slog.Error("Failed to signal participant with tracks", "participant", participant.ID, "room", r.Name, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) signalParticipantWithTracks(participant *Participant) error {
|
||||
if r.AudioTrack != nil {
|
||||
if err := participant.addTrack(r.AudioTrack); err != nil {
|
||||
return fmt.Errorf("failed to add audio track: %w", err)
|
||||
}
|
||||
}
|
||||
if r.VideoTrack != nil {
|
||||
if err := participant.addTrack(r.VideoTrack); err != nil {
|
||||
return fmt.Errorf("failed to add video track: %w", err)
|
||||
}
|
||||
}
|
||||
if err := participant.signalOffer(); err != nil {
|
||||
return fmt.Errorf("failed to signal offer: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Room) signalParticipantsOffline() {
|
||||
for _, participant := range r.Participants.Copy() {
|
||||
if err := r.signalParticipantOffline(participant); err != nil {
|
||||
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// signalParticipantOffline signals a single participant offline
|
||||
func (r *Room) signalParticipantOffline(participant *Participant) error {
|
||||
// Skip if websocket is nil or closed
|
||||
if participant.WebSocket == nil || participant.WebSocket.IsClosed() {
|
||||
return nil
|
||||
}
|
||||
if err := participant.WebSocket.SendAnswerMessageWS(connections.AnswerOffline); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -32,7 +32,7 @@ func main() {
|
||||
slog.SetDefault(logger)
|
||||
|
||||
// Start relay
|
||||
err := core.InitRelay(mainCtx, mainStopper)
|
||||
relay, err := core.InitRelay(mainCtx, mainStopper)
|
||||
if err != nil {
|
||||
slog.Error("Failed to initialize relay", "err", err)
|
||||
mainStopper()
|
||||
@@ -41,5 +41,10 @@ func main() {
|
||||
|
||||
// Wait for exit signal
|
||||
<-mainCtx.Done()
|
||||
slog.Info("Shutting down gracefully by signal...")
|
||||
slog.Info("Shutting down gracefully by signal..")
|
||||
|
||||
defaultFile := common.GetFlags().PersistDir + "/peerstore.json"
|
||||
if err = relay.SaveToFile(defaultFile); err != nil {
|
||||
slog.Error("Failed to save peer store", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--*) shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
exec "$@"
|
||||
@@ -21,6 +21,13 @@ chown_user_directory() {
|
||||
echo "Error: Failed to change ownership of ${NESTRI_HOME} to ${NESTRI_USER}:${NESTRI_USER}" >&2
|
||||
return 1
|
||||
fi
|
||||
# Also apply to .cache separately
|
||||
if [[ -d "${NESTRI_HOME}/.cache" ]]; then
|
||||
if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}/.cache" 2>/dev/null; then
|
||||
echo "Error: Failed to change ownership of ${NESTRI_HOME}/.cache to ${NESTRI_USER}:${NESTRI_USER}" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -48,13 +55,13 @@ setup_namespaceless() {
|
||||
|
||||
# Ensures cache directory exists
|
||||
setup_cache() {
|
||||
log "Setting up NVIDIA driver cache directory at $CACHE_DIR..."
|
||||
log "Setting up cache directory at $CACHE_DIR..."
|
||||
mkdir -p "$CACHE_DIR" || {
|
||||
log "Warning: Failed to create cache directory, continuing without cache."
|
||||
log "Warning: Failed to create cache directory, continuing.."
|
||||
return 1
|
||||
}
|
||||
$ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "$CACHE_DIR" 2>/dev/null || {
|
||||
log "Warning: Failed to set cache directory ownership, continuing..."
|
||||
log "Warning: Failed to set cache directory ownership, continuing.."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +230,7 @@ main() {
|
||||
|
||||
# Start by getting the container we are running under
|
||||
get_container_info || {
|
||||
log "Warning: Failed to detect container information."
|
||||
log "Warning: Failed to detect container information"
|
||||
}
|
||||
log_container_info
|
||||
|
||||
@@ -231,6 +238,9 @@ main() {
|
||||
ENTCMD_PREFIX="sudo -E"
|
||||
fi
|
||||
|
||||
# Setup cache now
|
||||
setup_cache
|
||||
|
||||
# Configure SSH
|
||||
if [ -n "${SSH_ENABLE_PORT+x}" ] && [ "${SSH_ENABLE_PORT:-0}" -ne 0 ] && \
|
||||
[ -n "${SSH_ALLOWED_KEY+x}" ] && [ -n "${SSH_ALLOWED_KEY}" ]; then
|
||||
@@ -244,14 +254,14 @@ main() {
|
||||
|
||||
# Get and detect GPU(s)
|
||||
get_gpu_info || {
|
||||
log "Error: Failed to detect GPU information."
|
||||
log "Error: Failed to detect GPU information"
|
||||
exit 1
|
||||
}
|
||||
log_gpu_info
|
||||
|
||||
# Handle NVIDIA GPU
|
||||
if [[ -n "${vendor_devices[nvidia]:-}" ]]; then
|
||||
log "NVIDIA GPU(s) detected, applying driver fix..."
|
||||
log "NVIDIA GPU(s) detected, applying driver fix.."
|
||||
|
||||
# Determine NVIDIA driver version
|
||||
local nvidia_driver_version=""
|
||||
@@ -265,16 +275,15 @@ main() {
|
||||
log "Error: Failed to determine NVIDIA driver version."
|
||||
# Check for other GPU vendors before exiting
|
||||
if [[ -n "${vendor_devices[amd]:-}" || -n "${vendor_devices[intel]:-}" ]]; then
|
||||
log "Other GPUs (AMD or Intel) detected, continuing without NVIDIA driver."
|
||||
log "Other GPUs (AMD or Intel) detected, continuing without NVIDIA driver"
|
||||
else
|
||||
log "No other GPUs detected, exiting due to NVIDIA driver version failure."
|
||||
log "No other GPUs detected, exiting due to NVIDIA driver version failure"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log "Detected NVIDIA driver version: $nvidia_driver_version"
|
||||
|
||||
# Set up cache and get installer
|
||||
setup_cache
|
||||
# Get installer
|
||||
local arch=$(uname -m)
|
||||
local filename="NVIDIA-Linux-${arch}-${nvidia_driver_version}.run"
|
||||
cd "$NVIDIA_INSTALLER_DIR" || {
|
||||
@@ -284,9 +293,9 @@ main() {
|
||||
get_nvidia_installer "$nvidia_driver_version" "$arch" || {
|
||||
# Check for other GPU vendors before exiting
|
||||
if [[ -n "${vendor_devices[amd]:-}" || -n "${vendor_devices[intel]:-}" ]]; then
|
||||
log "Other GPUs (AMD or Intel) detected, continuing without NVIDIA driver."
|
||||
log "Other GPUs (AMD or Intel) detected, continuing without NVIDIA driver"
|
||||
else
|
||||
log "No other GPUs detected, exiting due to NVIDIA installer failure."
|
||||
log "No other GPUs detected, exiting due to NVIDIA installer failure"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -295,9 +304,9 @@ main() {
|
||||
install_nvidia_driver "$filename" || {
|
||||
# Check for other GPU vendors before exiting
|
||||
if [[ -n "${vendor_devices[amd]:-}" || -n "${vendor_devices[intel]:-}" ]]; then
|
||||
log "Other GPUs (AMD or Intel) detected, continuing without NVIDIA driver."
|
||||
log "Other GPUs (AMD or Intel) detected, continuing without NVIDIA driver"
|
||||
else
|
||||
log "No other GPUs detected, exiting due to NVIDIA driver installation failure."
|
||||
log "No other GPUs detected, exiting due to NVIDIA driver installation failure"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -305,14 +314,14 @@ main() {
|
||||
fi
|
||||
|
||||
# Make sure gamescope has CAP_SYS_NICE capabilities if available
|
||||
log "Checking for CAP_SYS_NICE availability..."
|
||||
log "Checking for CAP_SYS_NICE availability.."
|
||||
if capsh --print | grep -q "Current:.*cap_sys_nice"; then
|
||||
log "Giving gamescope compositor CAP_SYS_NICE permissions..."
|
||||
log "Giving gamescope compositor CAP_SYS_NICE permissions.."
|
||||
setcap 'CAP_SYS_NICE+eip' /usr/bin/gamescope 2>/dev/null || {
|
||||
log "Warning: Failed to set CAP_SYS_NICE on gamescope, continuing without it..."
|
||||
log "Warning: Failed to set CAP_SYS_NICE on gamescope, continuing without it.."
|
||||
}
|
||||
else
|
||||
log "Skipping CAP_SYS_NICE for gamescope, capability not available..."
|
||||
log "Skipping CAP_SYS_NICE for gamescope, capability not available"
|
||||
fi
|
||||
|
||||
# Handle user directory permissions
|
||||
@@ -325,6 +334,19 @@ main() {
|
||||
setup_namespaceless
|
||||
fi
|
||||
|
||||
# Make sure /run/udev/ directory exists with /run/udev/control, needed for virtual controller support
|
||||
if [[ ! -d "/run/udev" || ! -e "/run/udev/control" ]]; then
|
||||
log "Creating /run/udev directory and control file..."
|
||||
$ENTCMD_PREFIX mkdir -p /run/udev || {
|
||||
log "Error: Failed to create /run/udev directory"
|
||||
exit 1
|
||||
}
|
||||
$ENTCMD_PREFIX touch /run/udev/control || {
|
||||
log "Error: Failed to create /run/udev/control file"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Switch to nestri runner entrypoint
|
||||
log "Switching to application startup entrypoint..."
|
||||
if [[ ! -f /etc/nestri/entrypoint_nestri.sh ]]; then
|
||||
@@ -339,6 +361,6 @@ main() {
|
||||
}
|
||||
|
||||
# Trap signals for clean exit
|
||||
trap 'log "Received termination signal, exiting..."; exit 1' SIGINT SIGTERM
|
||||
trap 'log "Received termination signal, exiting.."; exit 0' SIGINT SIGTERM
|
||||
|
||||
main
|
||||
|
||||
@@ -31,6 +31,9 @@ parse_resolution() {
|
||||
MAX_RETRIES=3
|
||||
RETRY_COUNT=0
|
||||
WAYLAND_READY_DELAY=3
|
||||
ENTCMD_PREFIX=""
|
||||
PRELOAD_SHIM_64=/usr/lib64/libvimputti_shim.so
|
||||
PRELOAD_SHIM_32=/usr/lib32/libvimputti_shim.so
|
||||
|
||||
# Kills process if running
|
||||
kill_if_running() {
|
||||
@@ -43,83 +46,49 @@ kill_if_running() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Starts up Steam namespace-less live-patcher
|
||||
start_steam_namespaceless_patcher() {
|
||||
kill_if_running "${PATCHER_PID:-}" "steam-patcher"
|
||||
|
||||
local entrypoints=(
|
||||
"${HOME}/.local/share/Steam/steamrt64/steam-runtime-steamrt/_v2-entry-point"
|
||||
"${HOME}/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/_v2-entry-point"
|
||||
"${HOME}/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point"
|
||||
# < Add more entrypoints here if needed >
|
||||
)
|
||||
local custom_entrypoint="/etc/nestri/_v2-entry-point"
|
||||
local temp_entrypoint="/tmp/_v2-entry-point.padded"
|
||||
|
||||
if [[ ! -f "$custom_entrypoint" ]]; then
|
||||
log "Error: Custom _v2-entry-point not found at $custom_entrypoint"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Starting Steam _v2-entry-point patcher..."
|
||||
(
|
||||
while true; do
|
||||
for i in "${!entrypoints[@]}"; do
|
||||
local steam_entrypoint="${entrypoints[$i]}"
|
||||
|
||||
if [[ -f "$steam_entrypoint" ]]; then
|
||||
# Get original file size
|
||||
local original_size
|
||||
original_size=$(stat -c %s "$steam_entrypoint" 2>/dev/null)
|
||||
if [[ -z "$original_size" ]] || [[ "$original_size" -eq 0 ]]; then
|
||||
log "Warning: Could not determine size of $steam_entrypoint, retrying..."
|
||||
continue
|
||||
fi
|
||||
|
||||
# Copy custom entrypoint to temp location
|
||||
cp "$custom_entrypoint" "$temp_entrypoint" 2>/dev/null || {
|
||||
log "Warning: Failed to copy custom entrypoint to $temp_entrypoint"
|
||||
continue
|
||||
}
|
||||
|
||||
# Pad the temporary file to match original size
|
||||
if (( $(stat -c %s "$temp_entrypoint") < original_size )); then
|
||||
truncate -s "$original_size" "$temp_entrypoint" 2>/dev/null || {
|
||||
log "Warning: Failed to pad $temp_entrypoint to $original_size bytes"
|
||||
continue
|
||||
}
|
||||
fi
|
||||
|
||||
# Copy padded file to Steam's entrypoint, if contents differ
|
||||
if ! cmp -s "$temp_entrypoint" "$steam_entrypoint"; then
|
||||
cp "$temp_entrypoint" "$steam_entrypoint" 2>/dev/null || {
|
||||
log "Warning: Failed to patch $steam_entrypoint"
|
||||
}
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Sleep for 1s
|
||||
sleep 1
|
||||
done
|
||||
) &
|
||||
PATCHER_PID=$!
|
||||
log "Steam _v2-entry-point patcher started (PID: $PATCHER_PID)"
|
||||
}
|
||||
|
||||
# Starts nestri-server
|
||||
start_nestri_server() {
|
||||
kill_if_running "${NESTRI_PID:-}" "nestri-server"
|
||||
|
||||
log "Starting nestri-server..."
|
||||
nestri-server $NESTRI_PARAMS &
|
||||
NESTRI_PID=$!
|
||||
|
||||
log "Waiting for Wayland display 'wayland-1'..."
|
||||
WAYLAND_SOCKET="${XDG_RUNTIME_DIR}/wayland-1"
|
||||
|
||||
# Make sure to remove old socket if exists (along with .lock file)
|
||||
if [[ -e "$WAYLAND_SOCKET" ]]; then
|
||||
log "Removing stale Wayland socket $WAYLAND_SOCKET"
|
||||
rm -f "$WAYLAND_SOCKET" 2>/dev/null || {
|
||||
log "Error: Failed to remove stale Wayland socket $WAYLAND_SOCKET"
|
||||
exit 1
|
||||
}
|
||||
# Ignore error if .lock file doesn't exist
|
||||
rm -f "${WAYLAND_SOCKET}.lock" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Also if gstreamer cache exists, remove it to avoid previous errors from persisting
|
||||
local gst_cache="${NESTRI_HOME}/.cache/gstreamer-1.0/"
|
||||
if [[ -d "$gst_cache" ]]; then
|
||||
log "Removing gstreamer cache at $gst_cache"
|
||||
rm -rf "${gst_cache}" 2>/dev/null || {
|
||||
log "Warning: Failed to remove gstreamer cache at $gst_cache"
|
||||
}
|
||||
fi
|
||||
|
||||
# Start nestri-server
|
||||
log "Starting nestri-server.."
|
||||
# Try with realtime scheduling first (chrt -f 80), if fails, launch normally
|
||||
if $ENTCMD_PREFIX chrt -f 80 true 2>/dev/null; then
|
||||
$ENTCMD_PREFIX chrt -f 80 nestri-server $NESTRI_PARAMS &
|
||||
NESTRI_PID=$!
|
||||
log "Started nestri-server with realtime scheduling"
|
||||
else
|
||||
$ENTCMD_PREFIX nestri-server $NESTRI_PARAMS &
|
||||
NESTRI_PID=$!
|
||||
log "Started nestri-server"
|
||||
fi
|
||||
|
||||
log "Waiting for Wayland display $WAYLAND_SOCKET.."
|
||||
for ((i=1; i<=15; i++)); do
|
||||
if [[ -e "$WAYLAND_SOCKET" ]]; then
|
||||
log "Wayland display 'wayland-1' ready"
|
||||
log "Wayland display $WAYLAND_SOCKET ready"
|
||||
sleep "${WAYLAND_READY_DELAY:-3}"
|
||||
start_compositor
|
||||
return
|
||||
@@ -127,12 +96,7 @@ start_nestri_server() {
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log "Error: Wayland display 'wayland-1' not available"
|
||||
|
||||
# Workaround for gstreamer being bit slow at times
|
||||
log "Clearing gstreamer cache.."
|
||||
rm -rf "${HOME}/.cache/gstreamer-1.0" 2>/dev/null || true
|
||||
|
||||
log "Error: Wayland display $WAYLAND_SOCKET not available"
|
||||
increment_retry "nestri-server"
|
||||
restart_chain
|
||||
}
|
||||
@@ -144,15 +108,17 @@ start_compositor() {
|
||||
|
||||
# Set default values only if variables are unset (not empty)
|
||||
if [[ -z "${NESTRI_LAUNCH_CMD+x}" ]]; then
|
||||
NESTRI_LAUNCH_CMD="steam-native -tenfoot -cef-force-gpu"
|
||||
NESTRI_LAUNCH_CMD="dbus-launch steam -tenfoot -cef-force-gpu"
|
||||
fi
|
||||
if [[ -z "${NESTRI_LAUNCH_COMPOSITOR+x}" ]]; then
|
||||
NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland --force-grab-cursor -g -f --rt --mangoapp -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}"
|
||||
fi
|
||||
|
||||
# Start Steam patcher only if Steam command is present and if needed for container runtime
|
||||
if [[ -n "${NESTRI_LAUNCH_CMD}" ]] && [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]] && [[ "${container_runtime:-}" != "podman" ]]; then
|
||||
start_steam_namespaceless_patcher
|
||||
# If PRELOAD_SHIM_arch's are set and exist, set LD_PRELOAD for 32/64-bit apps
|
||||
local do_ld_preload=false
|
||||
if [[ -f "$PRELOAD_SHIM_64" ]] || [[ -f "$PRELOAD_SHIM_32" ]]; then
|
||||
do_ld_preload=true
|
||||
log "Using LD_PRELOAD shim(s)"
|
||||
fi
|
||||
|
||||
# Launch compositor if configured
|
||||
@@ -163,32 +129,51 @@ start_compositor() {
|
||||
# Check if this is a gamescope command
|
||||
if [[ "$compositor_cmd" == *"gamescope"* ]]; then
|
||||
is_gamescope=true
|
||||
# Append application command for gamescope if needed
|
||||
if [[ -n "$NESTRI_LAUNCH_CMD" ]] && [[ "$compositor_cmd" != *" -- "* ]]; then
|
||||
# If steam in launch command, enable gamescope integration via -e
|
||||
if [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then
|
||||
compositor_cmd+=" -e"
|
||||
fi
|
||||
compositor_cmd+=" -- $NESTRI_LAUNCH_CMD"
|
||||
# If ld_preload is true, add env with LD_PRELOAD
|
||||
if $do_ld_preload; then
|
||||
compositor_cmd+=" -- env LD_PRELOAD='/usr/\$LIB/libvimputti_shim.so' bash -c $(printf %q "$NESTRI_LAUNCH_CMD")"
|
||||
else
|
||||
compositor_cmd+=" -- bash -c $(printf %q "$NESTRI_LAUNCH_CMD")"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get appropriate socket based on compositor type
|
||||
if $is_gamescope; then
|
||||
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/gamescope-0"
|
||||
else
|
||||
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/wayland-0"
|
||||
fi
|
||||
|
||||
# Clean up old socket if exists
|
||||
if [[ -e "$COMPOSITOR_SOCKET" ]]; then
|
||||
log "Removing stale compositor socket $COMPOSITOR_SOCKET"
|
||||
rm -f "$COMPOSITOR_SOCKET" 2>/dev/null || {
|
||||
log "Error: Failed to remove stale compositor socket $COMPOSITOR_SOCKET"
|
||||
exit 1
|
||||
}
|
||||
# Ignore error if .lock file doesn't exist
|
||||
rm -f "${COMPOSITOR_SOCKET}.lock" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log "Starting compositor: $compositor_cmd"
|
||||
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$compositor_cmd" &
|
||||
COMPOSITOR_PID=$!
|
||||
|
||||
# Wait for appropriate socket based on compositor type
|
||||
if $is_gamescope; then
|
||||
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/gamescope-0"
|
||||
log "Waiting for gamescope socket..."
|
||||
else
|
||||
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/wayland-0"
|
||||
log "Waiting for wayland-0 socket..."
|
||||
# If ld_preload is true, export LD_PRELOAD
|
||||
if $do_ld_preload; then
|
||||
export LD_PRELOAD='/usr/$LIB/libvimputti_shim.so'
|
||||
fi
|
||||
|
||||
log "Waiting for compositor socket $COMPOSITOR_SOCKET.."
|
||||
for ((i=1; i<=15; i++)); do
|
||||
if [[ -e "$COMPOSITOR_SOCKET" ]]; then
|
||||
log "Compositor socket ready ($COMPOSITOR_SOCKET)."
|
||||
log "Compositor socket ready $COMPOSITOR_SOCKET"
|
||||
# Patch resolution with wlr-randr for non-gamescope compositors
|
||||
if ! $is_gamescope; then
|
||||
local OUTPUT_NAME
|
||||
@@ -205,6 +190,8 @@ start_compositor() {
|
||||
WAYLAND_DISPLAY=wayland-0 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
|
||||
APP_PID=$!
|
||||
fi
|
||||
else
|
||||
log "Gamescope detected, skipping wlr-randr resolution patch"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
@@ -246,8 +233,6 @@ cleanup() {
|
||||
kill_if_running "${NESTRI_PID:-}" "nestri-server"
|
||||
kill_if_running "${COMPOSITOR_PID:-}" "compositor"
|
||||
kill_if_running "${APP_PID:-}" "application"
|
||||
kill_if_running "${PATCHER_PID:-}" "steam-patcher"
|
||||
rm -f "/tmp/_v2-entry-point.padded" 2>/dev/null
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
@@ -272,11 +257,6 @@ main_loop() {
|
||||
log "application died"
|
||||
increment_retry "application"
|
||||
start_compositor
|
||||
# Check patcher
|
||||
elif [[ -n "${PATCHER_PID:-}" ]] && ! kill -0 "${PATCHER_PID}" 2>/dev/null; then
|
||||
log "steam-patcher died"
|
||||
increment_retry "steam-patcher"
|
||||
start_steam_namespaceless_patcher
|
||||
fi
|
||||
done
|
||||
}
|
||||
@@ -287,9 +267,8 @@ main() {
|
||||
log "Warning: Failed to detect container information."
|
||||
}
|
||||
|
||||
# Ensure DBus session env exists
|
||||
if command -v dbus-launch >/dev/null 2>&1 && [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
|
||||
eval "$(dbus-launch)"
|
||||
if [[ "$container_runtime" != "apptainer" ]]; then
|
||||
ENTCMD_PREFIX="sudo -E -u ${NESTRI_USER}"
|
||||
fi
|
||||
|
||||
restart_chain
|
||||
|
||||
@@ -41,6 +41,15 @@ priority=5
|
||||
nice=-10
|
||||
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||
|
||||
[program:vimputti-manager]
|
||||
user=nestri
|
||||
command=vimputti-manager
|
||||
autorestart=true
|
||||
autostart=true
|
||||
startretries=3
|
||||
priority=6
|
||||
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s,VIMPUTTI_PATH=%(ENV_NESTRI_VIMPUTTI_PATH)s
|
||||
|
||||
[program:entrypoint]
|
||||
command=/etc/nestri/entrypoint.sh
|
||||
autorestart=false
|
||||
|
||||
1712
packages/server/Cargo.lock
generated
1712
packages/server/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,22 +11,22 @@ path = "src/main.rs"
|
||||
gstreamer = { version = "0.24", features = ["v1_26"] }
|
||||
gstreamer-webrtc = { version = "0.24", features = ["v1_26"] }
|
||||
gst-plugin-webrtc = { version = "0.14" }
|
||||
serde = {version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.45", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.48", features = ["full"] }
|
||||
tokio-stream = { version = "0.1", features = ["full"] }
|
||||
clap = { version = "4.5", features = ["env", "derive"] }
|
||||
serde_json = "1.0"
|
||||
webrtc = "0.13"
|
||||
webrtc = "0.14"
|
||||
regex = "1.11"
|
||||
rand = "0.9"
|
||||
rustls = { version = "0.23", features = ["ring"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
vimputti = "0.1.3"
|
||||
chrono = "0.4"
|
||||
prost = "0.14"
|
||||
prost-types = "0.14"
|
||||
parking_lot = "0.12"
|
||||
atomic_refcell = "0.1"
|
||||
byteorder = "1.5"
|
||||
libp2p = { version = "0.56", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "websocket", "autonat"] }
|
||||
libp2p-identify = "0.47"
|
||||
@@ -39,3 +39,4 @@ libp2p-dns = { version = "0.44", features = ["tokio"] }
|
||||
libp2p-tcp = { version = "0.44", features = ["tokio"] }
|
||||
libp2p-websocket = "0.45"
|
||||
dashmap = "6.1"
|
||||
anyhow = "1.0"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.89"
|
||||
channel = "1.90"
|
||||
@@ -58,6 +58,14 @@ impl Args {
|
||||
.env("NESTRI_ROOM")
|
||||
.help("Nestri room name/identifier"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("vimputti-path")
|
||||
.long("vimputti-path")
|
||||
.env("VIMPUTTI_PATH")
|
||||
.help("Path to vimputti socket")
|
||||
.value_parser(NonEmptyStringValueParser::new())
|
||||
.default_value("/tmp/vimputti-0"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("gpu-vendor")
|
||||
.short('g')
|
||||
@@ -204,10 +212,10 @@ impl Args {
|
||||
.default_value("192"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("dma-buf")
|
||||
.long("dma-buf")
|
||||
.env("DMA_BUF")
|
||||
.help("Use DMA-BUF for pipeline")
|
||||
Arg::new("zero-copy")
|
||||
.long("zero-copy")
|
||||
.env("ZERO_COPY")
|
||||
.help("Use zero-copy pipeline")
|
||||
.value_parser(BoolishValueParser::new())
|
||||
.default_value("false"),
|
||||
)
|
||||
|
||||
@@ -12,9 +12,12 @@ pub struct AppArgs {
|
||||
/// Nestri room name/identifier
|
||||
pub room: String,
|
||||
|
||||
/// Experimental DMA-BUF support
|
||||
/// vimputti socket path
|
||||
pub vimputti_path: Option<String>,
|
||||
|
||||
/// Experimental zero-copy pipeline support
|
||||
/// TODO: Move to video encoding flags
|
||||
pub dma_buf: bool,
|
||||
pub zero_copy: bool,
|
||||
}
|
||||
impl AppArgs {
|
||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||
@@ -45,7 +48,13 @@ impl AppArgs {
|
||||
.get_one::<String>("room")
|
||||
.unwrap_or(&rand::random::<u32>().to_string())
|
||||
.clone(),
|
||||
dma_buf: matches.get_one::<bool>("dma-buf").unwrap_or(&false).clone(),
|
||||
vimputti_path: matches
|
||||
.get_one::<String>("vimputti-path")
|
||||
.map(|s| s.clone()),
|
||||
zero_copy: matches
|
||||
.get_one::<bool>("zero-copy")
|
||||
.unwrap_or(&false)
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +69,10 @@ impl AppArgs {
|
||||
tracing::info!("> framerate: {}", self.framerate);
|
||||
tracing::info!("> relay_url: '{}'", self.relay_url);
|
||||
tracing::info!("> room: '{}'", self.room);
|
||||
tracing::info!("> dma_buf: {}", self.dma_buf);
|
||||
tracing::info!(
|
||||
"> vimputti_path: '{}'",
|
||||
self.vimputti_path.as_ref().map_or("None", |s| s.as_str())
|
||||
);
|
||||
tracing::info!("> zero_copy: {}", self.zero_copy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,10 @@ pub struct DeviceArgs {
|
||||
impl DeviceArgs {
|
||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||
Self {
|
||||
gpu_vendor: matches
|
||||
.get_one::<String>("gpu-vendor")
|
||||
.cloned(),
|
||||
gpu_name: matches
|
||||
.get_one::<String>("gpu-name")
|
||||
.cloned(),
|
||||
gpu_index: matches
|
||||
.get_one::<u32>("gpu-index")
|
||||
.cloned(),
|
||||
gpu_card_path: matches
|
||||
.get_one::<String>("gpu-card-path")
|
||||
.cloned(),
|
||||
gpu_vendor: matches.get_one::<String>("gpu-vendor").cloned(),
|
||||
gpu_name: matches.get_one::<String>("gpu-name").cloned(),
|
||||
gpu_index: matches.get_one::<u32>("gpu-index").cloned(),
|
||||
gpu_card_path: matches.get_one::<String>("gpu-card-path").cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -276,8 +276,8 @@ pub fn encoder_low_latency_params(
|
||||
_rate_control: &RateControl,
|
||||
framerate: u32,
|
||||
) -> VideoEncoderInfo {
|
||||
// 2 second GOP size, maybe lower to 1 second for fast recovery, if needed?
|
||||
let mut encoder_optz = encoder_gop_params(encoder, framerate * 2);
|
||||
// 1 second keyframe interval for fast recovery, is this too taxing?
|
||||
let mut encoder_optz = encoder_gop_params(encoder, framerate);
|
||||
|
||||
match encoder_optz.encoder_api {
|
||||
EncoderAPI::QSV => {
|
||||
@@ -291,6 +291,7 @@ pub fn encoder_low_latency_params(
|
||||
encoder_optz.set_parameter("multi-pass", "disabled");
|
||||
encoder_optz.set_parameter("preset", "p1");
|
||||
encoder_optz.set_parameter("tune", "ultra-low-latency");
|
||||
encoder_optz.set_parameter("zerolatency", "true");
|
||||
}
|
||||
EncoderAPI::AMF => {
|
||||
encoder_optz.set_parameter("preset", "speed");
|
||||
@@ -400,11 +401,21 @@ pub fn get_compatible_encoders(gpus: &Vec<GPUInfo>) -> Vec<VideoEncoderInfo> {
|
||||
}
|
||||
None
|
||||
} else if element.has_property("cuda-device-id") {
|
||||
let device_id =
|
||||
match element.property_value("cuda-device-id").get::<i32>() {
|
||||
Ok(v) if v >= 0 => Some(v as usize),
|
||||
_ => None,
|
||||
};
|
||||
let device_id = match element
|
||||
.property_value("cuda-device-id")
|
||||
.get::<i32>()
|
||||
{
|
||||
Ok(v) if v >= 0 => Some(v as usize),
|
||||
_ => {
|
||||
// If only one NVIDIA GPU, default to 0
|
||||
// fixes "Type: 'Hardware', Device: 'CPU'" issue
|
||||
if get_gpus_by_vendor(&gpus, GPUVendor::NVIDIA).len() == 1 {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// We'll just treat cuda-device-id as an index
|
||||
device_id.and_then(|id| {
|
||||
@@ -574,7 +585,7 @@ pub fn get_best_working_encoder(
|
||||
encoders: &Vec<VideoEncoderInfo>,
|
||||
codec: &Codec,
|
||||
encoder_type: &EncoderType,
|
||||
dma_buf: bool,
|
||||
zero_copy: bool,
|
||||
) -> Result<VideoEncoderInfo, Box<dyn Error>> {
|
||||
let mut candidates = get_encoders_by_videocodec(
|
||||
encoders,
|
||||
@@ -590,7 +601,7 @@ pub fn get_best_working_encoder(
|
||||
while !candidates.is_empty() {
|
||||
let best = get_best_compatible_encoder(&candidates, codec, encoder_type)?;
|
||||
tracing::info!("Testing encoder: {}", best.name,);
|
||||
if test_encoder(&best, dma_buf).is_ok() {
|
||||
if test_encoder(&best, zero_copy).is_ok() {
|
||||
return Ok(best);
|
||||
} else {
|
||||
// Remove this encoder and try next best
|
||||
@@ -602,7 +613,7 @@ pub fn get_best_working_encoder(
|
||||
}
|
||||
|
||||
/// Test if a pipeline with the given encoder can be created and set to Playing
|
||||
pub fn test_encoder(encoder: &VideoEncoderInfo, dma_buf: bool) -> Result<(), Box<dyn Error>> {
|
||||
pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), Box<dyn Error>> {
|
||||
let src = gstreamer::ElementFactory::make("waylanddisplaysrc").build()?;
|
||||
if let Some(gpu_info) = &encoder.gpu_info {
|
||||
src.set_property_from_str("render-node", gpu_info.render_path());
|
||||
@@ -610,12 +621,16 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, dma_buf: bool) -> Result<(), Box
|
||||
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let caps = gstreamer::Caps::from_str(&format!(
|
||||
"{},width=1280,height=720,framerate=30/1{}",
|
||||
if dma_buf {
|
||||
"video/x-raw(memory:DMABuf)"
|
||||
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 dma_buf { "" } else { ",format=RGBx" }
|
||||
if zero_copy { "" } else { ",format=RGBx" }
|
||||
))?;
|
||||
caps_filter.set_property("caps", &caps);
|
||||
|
||||
@@ -627,66 +642,47 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, dma_buf: bool) -> Result<(), Box
|
||||
// Create pipeline and link elements
|
||||
let pipeline = gstreamer::Pipeline::new();
|
||||
|
||||
if dma_buf && encoder.encoder_api == EncoderAPI::NVENC {
|
||||
// GL upload element
|
||||
let glupload = gstreamer::ElementFactory::make("glupload").build()?;
|
||||
// GL color convert element
|
||||
let glconvert = gstreamer::ElementFactory::make("glcolorconvert").build()?;
|
||||
// GL color convert caps
|
||||
let gl_caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let gl_caps = gstreamer::Caps::from_str("video/x-raw(memory:GLMemory),format=NV12")?;
|
||||
gl_caps_filter.set_property("caps", &gl_caps);
|
||||
// CUDA upload element
|
||||
let cudaupload = gstreamer::ElementFactory::make("cudaupload").build()?;
|
||||
if zero_copy {
|
||||
if encoder.encoder_api == EncoderAPI::NVENC {
|
||||
// NVENC zero-copy path
|
||||
pipeline.add_many(&[&src, &caps_filter, &enc, &sink])?;
|
||||
gstreamer::Element::link_many(&[&src, &caps_filter, &enc, &sink])?;
|
||||
} else {
|
||||
// VA-API/QSV zero-copy path
|
||||
let vapostproc = gstreamer::ElementFactory::make("vapostproc").build()?;
|
||||
let va_caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let va_caps = gstreamer::Caps::from_str("video/x-raw(memory:VAMemory),format=NV12")?;
|
||||
va_caps_filter.set_property("caps", &va_caps);
|
||||
|
||||
pipeline.add_many(&[
|
||||
&src,
|
||||
&caps_filter,
|
||||
&glupload,
|
||||
&glconvert,
|
||||
&gl_caps_filter,
|
||||
&cudaupload,
|
||||
&enc,
|
||||
&sink,
|
||||
])?;
|
||||
gstreamer::Element::link_many(&[
|
||||
&src,
|
||||
&caps_filter,
|
||||
&glupload,
|
||||
&glconvert,
|
||||
&gl_caps_filter,
|
||||
&cudaupload,
|
||||
&enc,
|
||||
&sink,
|
||||
])?;
|
||||
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 {
|
||||
let vapostproc = gstreamer::ElementFactory::make("vapostproc").build()?;
|
||||
// VA caps filter
|
||||
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,
|
||||
])?;
|
||||
// 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 _ = pipeline.set_state(gstreamer::State::Playing);
|
||||
for msg in bus.iter_timed(gstreamer::ClockTime::from_seconds(2)) {
|
||||
pipeline.set_state(gstreamer::State::Playing)?;
|
||||
|
||||
// Wait for either error or async-done (state change complete)
|
||||
for msg in bus.iter_timed(gstreamer::ClockTime::from_seconds(10)) {
|
||||
match msg.view() {
|
||||
gstreamer::MessageView::Error(err) => {
|
||||
let err_msg = format!("Pipeline error: {}", err.error());
|
||||
@@ -694,14 +690,17 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, dma_buf: bool) -> Result<(), Box
|
||||
let _ = pipeline.set_state(gstreamer::State::Null);
|
||||
return Err(err_msg.into());
|
||||
}
|
||||
gstreamer::MessageView::Eos(_) => {
|
||||
tracing::info!("Pipeline EOS received");
|
||||
gstreamer::MessageView::AsyncDone(_) => {
|
||||
// Pipeline successfully reached PLAYING state
|
||||
tracing::debug!("Pipeline reached PLAYING state successfully");
|
||||
let _ = pipeline.set_state(gstreamer::State::Null);
|
||||
return Err("Pipeline EOS received, encoder test failed".into());
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, timeout occurred without reaching PLAYING or error
|
||||
let _ = pipeline.set_state(gstreamer::State::Null);
|
||||
Ok(())
|
||||
Err("Encoder test timed out".into())
|
||||
}
|
||||
|
||||
@@ -112,11 +112,25 @@ pub fn get_gpus() -> Result<Vec<GPUInfo>, Box<dyn Error>> {
|
||||
let minor = &caps[1];
|
||||
|
||||
// Read vendor and device ID
|
||||
let vendor_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/vendor", minor))?;
|
||||
let vendor_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/vendor", minor));
|
||||
let vendor_str = match vendor_str {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to read vendor for card{}: {}", minor, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let vendor_str = vendor_str.trim_start_matches("0x").trim_end_matches('\n');
|
||||
let vendor = u16::from_str_radix(vendor_str, 16)?;
|
||||
|
||||
let device_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/device", minor))?;
|
||||
let device_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/device", minor));
|
||||
let device_str = match device_str {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to read device for card{}: {}", minor, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let device_str = device_str.trim_start_matches("0x").trim_end_matches('\n');
|
||||
|
||||
// Look up in hwdata PCI database
|
||||
@@ -129,7 +143,15 @@ pub fn get_gpus() -> Result<Vec<GPUInfo>, Box<dyn Error>> {
|
||||
};
|
||||
|
||||
// Read PCI bus ID
|
||||
let pci_bus_id = fs::read_to_string(format!("/sys/class/drm/card{}/device/uevent", minor))?;
|
||||
let pci_bus_id = fs::read_to_string(format!("/sys/class/drm/card{}/device/uevent", minor));
|
||||
let pci_bus_id = match pci_bus_id {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to read PCI bus ID for card{}: {}", minor, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Extract PCI_SLOT_NAME from uevent content
|
||||
let pci_bus_id = pci_bus_id
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
@@ -191,7 +213,6 @@ fn parse_pci_ids(pci_data: &str, vendor_id: &str, device_id: &str) -> Option<Str
|
||||
|
||||
fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> {
|
||||
let entries = fs::read_dir("/sys/bus/pci/devices").ok()?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
if !entry.path().to_string_lossy().contains(&pci_addr) {
|
||||
continue;
|
||||
|
||||
1
packages/server/src/input.rs
Normal file
1
packages/server/src/input.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod controller;
|
||||
205
packages/server/src/input/controller.rs
Normal file
205
packages/server/src/input/controller.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use crate::proto::proto::proto_input::InputType::{
|
||||
ControllerAttach, ControllerAxis, ControllerButton, ControllerDetach, ControllerRumble,
|
||||
ControllerStick, ControllerTrigger,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
fn controller_string_to_type(controller_type: &str) -> Result<vimputti::DeviceConfig> {
|
||||
match controller_type.to_lowercase().as_str() {
|
||||
"ps4" => Ok(vimputti::ControllerTemplates::ps4()),
|
||||
"ps5" => Ok(vimputti::ControllerTemplates::ps5()),
|
||||
"xbox360" => Ok(vimputti::ControllerTemplates::xbox360()),
|
||||
"xboxone" => Ok(vimputti::ControllerTemplates::xbox_one()),
|
||||
"switchpro" => Ok(vimputti::ControllerTemplates::switch_pro()),
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Unsupported controller type: {}",
|
||||
controller_type
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ControllerInput {
|
||||
config: vimputti::DeviceConfig,
|
||||
device: vimputti::client::VirtualController,
|
||||
}
|
||||
impl ControllerInput {
|
||||
pub async fn new(
|
||||
controller_type: String,
|
||||
client: &vimputti::client::VimputtiClient,
|
||||
) -> Result<Self> {
|
||||
let config = controller_string_to_type(&controller_type)?;
|
||||
Ok(Self {
|
||||
config: config.clone(),
|
||||
device: client.create_device(config).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn device_mut(&mut self) -> &mut vimputti::client::VirtualController {
|
||||
&mut self.device
|
||||
}
|
||||
|
||||
pub fn device(&self) -> &vimputti::client::VirtualController {
|
||||
&self.device
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ControllerManager {
|
||||
vimputti_client: Arc<vimputti::client::VimputtiClient>,
|
||||
cmd_tx: mpsc::Sender<crate::proto::proto::ProtoInput>,
|
||||
rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>, // (slot, strong, weak, duration_ms)
|
||||
}
|
||||
impl ControllerManager {
|
||||
pub fn new(
|
||||
vimputti_client: Arc<vimputti::client::VimputtiClient>,
|
||||
) -> Result<(Self, mpsc::Receiver<(u32, u16, u16, u16)>)> {
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel(100);
|
||||
let (rumble_tx, rumble_rx) = mpsc::channel(100);
|
||||
tokio::spawn(command_loop(
|
||||
cmd_rx,
|
||||
vimputti_client.clone(),
|
||||
rumble_tx.clone(),
|
||||
));
|
||||
Ok((
|
||||
Self {
|
||||
vimputti_client,
|
||||
cmd_tx,
|
||||
rumble_tx,
|
||||
},
|
||||
rumble_rx,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn send_command(&self, input: crate::proto::proto::ProtoInput) -> Result<()> {
|
||||
self.cmd_tx.send(input).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn command_loop(
|
||||
mut cmd_rx: mpsc::Receiver<crate::proto::proto::ProtoInput>,
|
||||
vimputti_client: Arc<vimputti::client::VimputtiClient>,
|
||||
rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>,
|
||||
) {
|
||||
let mut controllers: HashMap<u32, ControllerInput> = HashMap::new();
|
||||
while let Some(input) = cmd_rx.recv().await {
|
||||
if let Some(input_type) = input.input_type {
|
||||
match input_type {
|
||||
ControllerAttach(data) => {
|
||||
// Check if controller already exists in the slot, if so, ignore
|
||||
if controllers.contains_key(&(data.slot as u32)) {
|
||||
tracing::warn!(
|
||||
"Controller slot {} already occupied, ignoring attach",
|
||||
data.slot
|
||||
);
|
||||
} else {
|
||||
if let Ok(mut controller) =
|
||||
ControllerInput::new(data.id.clone(), &vimputti_client).await
|
||||
{
|
||||
let slot = data.slot as u32;
|
||||
let rumble_tx = rumble_tx.clone();
|
||||
|
||||
controller
|
||||
.device_mut()
|
||||
.on_rumble(move |strong, weak, duration_ms| {
|
||||
let _ = rumble_tx.try_send((slot, strong, weak, duration_ms));
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Failed to register rumble callback for slot {}: {}",
|
||||
slot,
|
||||
e
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
controllers.insert(data.slot as u32, controller);
|
||||
tracing::info!("Controller {} attached to slot {}", data.id, data.slot);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to create controller of type {} for slot {}",
|
||||
data.id,
|
||||
data.slot
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ControllerDetach(data) => {
|
||||
if controllers.remove(&(data.slot as u32)).is_some() {
|
||||
tracing::info!("Controller detached from slot {}", data.slot);
|
||||
} else {
|
||||
tracing::warn!("No controller found in slot {} to detach", data.slot);
|
||||
}
|
||||
}
|
||||
ControllerButton(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.slot as u32)) {
|
||||
if let Some(button) = vimputti::Button::from_ev_code(data.button as u16) {
|
||||
let device = controller.device();
|
||||
device.button(button, data.pressed);
|
||||
device.sync();
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Controller slot {} not found for button event", data.slot);
|
||||
}
|
||||
}
|
||||
ControllerStick(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.slot as u32)) {
|
||||
let device = controller.device();
|
||||
if data.stick == 0 {
|
||||
// Left stick
|
||||
device.axis(vimputti::Axis::LeftStickX, data.x);
|
||||
device.sync();
|
||||
device.axis(vimputti::Axis::LeftStickY, data.y);
|
||||
} else if data.stick == 1 {
|
||||
// Right stick
|
||||
device.axis(vimputti::Axis::RightStickX, data.x);
|
||||
device.sync();
|
||||
device.axis(vimputti::Axis::RightStickY, data.y);
|
||||
}
|
||||
device.sync();
|
||||
} else {
|
||||
tracing::warn!("Controller slot {} not found for stick event", data.slot);
|
||||
}
|
||||
}
|
||||
ControllerTrigger(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.slot as u32)) {
|
||||
let device = controller.device();
|
||||
if data.trigger == 0 {
|
||||
// Left trigger
|
||||
device.axis(vimputti::Axis::LowerLeftTrigger, data.value);
|
||||
} else if data.trigger == 1 {
|
||||
// Right trigger
|
||||
device.axis(vimputti::Axis::LowerRightTrigger, data.value);
|
||||
}
|
||||
device.sync();
|
||||
} else {
|
||||
tracing::warn!("Controller slot {} not found for trigger event", data.slot);
|
||||
}
|
||||
}
|
||||
ControllerAxis(data) => {
|
||||
if let Some(controller) = controllers.get(&(data.slot as u32)) {
|
||||
let device = controller.device();
|
||||
if data.axis == 0 {
|
||||
// dpad x
|
||||
device.axis(vimputti::Axis::DPadX, data.value);
|
||||
} else if data.axis == 1 {
|
||||
// dpad y
|
||||
device.axis(vimputti::Axis::DPadY, data.value);
|
||||
}
|
||||
device.sync();
|
||||
}
|
||||
}
|
||||
// Rumble will be outgoing event..
|
||||
ControllerRumble(_) => {
|
||||
//no-op
|
||||
}
|
||||
_ => {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod args;
|
||||
mod enc_helper;
|
||||
mod gpu;
|
||||
mod input;
|
||||
mod latency;
|
||||
mod messages;
|
||||
mod nestrisink;
|
||||
@@ -10,6 +11,7 @@ mod proto;
|
||||
use crate::args::encoding_args;
|
||||
use crate::enc_helper::{EncoderAPI, EncoderType};
|
||||
use crate::gpu::{GPUInfo, GPUVendor};
|
||||
use crate::input::controller::ControllerManager;
|
||||
use crate::nestrisink::NestriSignaller;
|
||||
use crate::p2p::p2p::NestriP2P;
|
||||
use gstreamer::prelude::*;
|
||||
@@ -118,7 +120,7 @@ fn handle_encoder_video(
|
||||
&video_encoders,
|
||||
&args.encoding.video.codec,
|
||||
&args.encoding.video.encoder_type,
|
||||
args.app.dma_buf,
|
||||
args.app.zero_copy,
|
||||
)?;
|
||||
}
|
||||
tracing::info!("Selected video encoder: '{}'", video_encoder.name);
|
||||
@@ -174,9 +176,6 @@ fn handle_encoder_audio(args: &args::Args) -> String {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Parse command line arguments
|
||||
let mut args = args::Args::new();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::builder()
|
||||
@@ -185,6 +184,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
)
|
||||
.init();
|
||||
|
||||
// Parse command line arguments
|
||||
let mut args = args::Args::new();
|
||||
|
||||
if args.app.verbose {
|
||||
args.debug_print();
|
||||
}
|
||||
@@ -199,13 +201,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
gstreamer::init()?;
|
||||
let _ = gstrswebrtc::plugin_register_static(); // Might be already registered, so we'll pass..
|
||||
|
||||
if args.app.dma_buf {
|
||||
if args.app.zero_copy {
|
||||
if args.encoding.video.encoder_type != EncoderType::HARDWARE {
|
||||
tracing::warn!("DMA-BUF is only supported with hardware encoders, disabling DMA-BUF..");
|
||||
args.app.dma_buf = false;
|
||||
tracing::warn!(
|
||||
"zero-copy is only supported with hardware encoders, disabling zero-copy.."
|
||||
);
|
||||
args.app.zero_copy = false;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"DMA-BUF is experimental, it may or may not improve performance, or even work at all."
|
||||
"zero-copy is experimental, it may or may not improve performance, or even work at all."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -238,6 +242,28 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let nestri_p2p = Arc::new(NestriP2P::new().await?);
|
||||
let p2p_conn = nestri_p2p.connect(relay_url).await?;
|
||||
|
||||
// Get vimputti manager connection if available
|
||||
let vpath = match args.app.vimputti_path {
|
||||
Some(ref path) => path.clone(),
|
||||
None => "/tmp/vimputti-0".to_string(),
|
||||
};
|
||||
let vimputti_client = match vimputti::VimputtiClient::connect(vpath).await {
|
||||
Ok(client) => {
|
||||
tracing::info!("Connected to vimputti manager");
|
||||
Some(Arc::new(client))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to connect to vimputti manager: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
let (controller_manager, rumble_rx) = if let Some(vclient) = vimputti_client {
|
||||
let (controller_manager, rumble_rx) = ControllerManager::new(vclient)?;
|
||||
(Some(Arc::new(controller_manager)), Some(rumble_rx))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
/*** PIPELINE CREATION ***/
|
||||
// Create the pipeline
|
||||
let pipeline = Arc::new(gstreamer::Pipeline::new());
|
||||
@@ -266,7 +292,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
// Required to fix gstreamer opus issue, where quality sounds off (due to wrong sample rate)
|
||||
let audio_capsfilter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let audio_caps = gstreamer::Caps::from_str("audio/x-raw,rate=48000,channels=2").unwrap();
|
||||
let audio_caps = gstreamer::Caps::from_str("audio/x-raw,rate=48000,channels=2")?;
|
||||
audio_capsfilter.set_property("caps", &audio_caps);
|
||||
|
||||
// Audio Encoder Element
|
||||
@@ -302,22 +328,30 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let caps = gstreamer::Caps::from_str(&format!(
|
||||
"{},width={},height={},framerate={}/1{}",
|
||||
if args.app.dma_buf {
|
||||
"video/x-raw(memory:DMABuf)"
|
||||
if args.app.zero_copy {
|
||||
if video_encoder_info.encoder_api == EncoderAPI::NVENC {
|
||||
"video/x-raw(memory:CUDAMemory)"
|
||||
} else {
|
||||
"video/x-raw(memory:DMABuf)"
|
||||
}
|
||||
} else {
|
||||
"video/x-raw"
|
||||
},
|
||||
args.app.resolution.0,
|
||||
args.app.resolution.1,
|
||||
args.app.framerate,
|
||||
if args.app.dma_buf { "" } else { ",format=RGBx" }
|
||||
if args.app.zero_copy {
|
||||
""
|
||||
} else {
|
||||
",format=RGBx"
|
||||
}
|
||||
))?;
|
||||
caps_filter.set_property("caps", &caps);
|
||||
|
||||
// Get bit-depth and choose appropriate format (NV12 or P010_10LE)
|
||||
// H.264 does not support above 8-bit. Also we require DMA-BUF.
|
||||
let video_format = if args.encoding.video.bit_depth == 10
|
||||
&& args.app.dma_buf
|
||||
&& args.app.zero_copy
|
||||
&& video_encoder_info.codec != enc_helper::VideoCodec::H264
|
||||
{
|
||||
"P010_10LE"
|
||||
@@ -325,27 +359,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"NV12"
|
||||
};
|
||||
|
||||
// GL and CUDA elements (NVIDIA only..)
|
||||
let mut glupload = None;
|
||||
let mut glconvert = None;
|
||||
let mut gl_caps_filter = None;
|
||||
let mut cudaupload = None;
|
||||
if args.app.dma_buf && video_encoder_info.encoder_api == EncoderAPI::NVENC {
|
||||
// GL upload element
|
||||
glupload = Some(gstreamer::ElementFactory::make("glupload").build()?);
|
||||
// GL color convert element
|
||||
glconvert = Some(gstreamer::ElementFactory::make("glcolorconvert").build()?);
|
||||
// GL color convert caps
|
||||
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let gl_caps = gstreamer::Caps::from_str(
|
||||
format!("video/x-raw(memory:GLMemory),format={video_format}").as_str(),
|
||||
)?;
|
||||
caps_filter.set_property("caps", &gl_caps);
|
||||
gl_caps_filter = Some(caps_filter);
|
||||
// CUDA upload element
|
||||
cudaupload = Some(gstreamer::ElementFactory::make("cudaupload").build()?);
|
||||
}
|
||||
|
||||
// vapostproc for VA compatible encoders
|
||||
let mut vapostproc = None;
|
||||
let mut va_caps_filter = None;
|
||||
@@ -364,7 +377,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
// Video Converter Element
|
||||
let mut video_converter = None;
|
||||
if !args.app.dma_buf {
|
||||
if !args.app.zero_copy {
|
||||
video_converter = Some(gstreamer::ElementFactory::make("videoconvert").build()?);
|
||||
}
|
||||
|
||||
@@ -397,24 +410,34 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
/* Output */
|
||||
// WebRTC sink Element
|
||||
let signaller =
|
||||
NestriSignaller::new(args.app.room, p2p_conn.clone(), video_source.clone()).await?;
|
||||
let signaller = NestriSignaller::new(
|
||||
args.app.room,
|
||||
p2p_conn.clone(),
|
||||
video_source.clone(),
|
||||
controller_manager,
|
||||
rumble_rx,
|
||||
)
|
||||
.await?;
|
||||
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone()));
|
||||
webrtcsink.set_property_from_str("stun-server", "stun://stun.l.google.com:19302");
|
||||
webrtcsink.set_property_from_str("congestion-control", "disabled");
|
||||
webrtcsink.set_property("do-retransmission", false);
|
||||
|
||||
/* Queues */
|
||||
let video_queue = gstreamer::ElementFactory::make("queue2")
|
||||
.property("max-size-buffers", 3u32)
|
||||
.property("max-size-time", 0u64)
|
||||
.property("max-size-bytes", 0u32)
|
||||
let video_source_queue = gstreamer::ElementFactory::make("queue")
|
||||
.property("max-size-buffers", 5u32)
|
||||
.build()?;
|
||||
|
||||
let audio_queue = gstreamer::ElementFactory::make("queue2")
|
||||
.property("max-size-buffers", 3u32)
|
||||
.property("max-size-time", 0u64)
|
||||
.property("max-size-bytes", 0u32)
|
||||
let audio_source_queue = gstreamer::ElementFactory::make("queue")
|
||||
.property("max-size-buffers", 5u32)
|
||||
.build()?;
|
||||
|
||||
let video_queue = gstreamer::ElementFactory::make("queue")
|
||||
.property("max-size-buffers", 5u32)
|
||||
.build()?;
|
||||
|
||||
let audio_queue = gstreamer::ElementFactory::make("queue")
|
||||
.property("max-size-buffers", 5u32)
|
||||
.build()?;
|
||||
|
||||
/* Clock Sync */
|
||||
@@ -433,6 +456,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
&caps_filter,
|
||||
&video_queue,
|
||||
&video_clocksync,
|
||||
&video_source_queue,
|
||||
&video_source,
|
||||
&audio_encoder,
|
||||
&audio_capsfilter,
|
||||
@@ -440,6 +464,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
&audio_clocksync,
|
||||
&audio_rate,
|
||||
&audio_converter,
|
||||
&audio_source_queue,
|
||||
&audio_source,
|
||||
])?;
|
||||
|
||||
@@ -455,24 +480,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
pipeline.add(parser)?;
|
||||
}
|
||||
|
||||
// If DMA-BUF..
|
||||
if args.app.dma_buf {
|
||||
// If zero-copy..
|
||||
if args.app.zero_copy {
|
||||
// VA-API / QSV pipeline
|
||||
if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) {
|
||||
pipeline.add_many(&[vapostproc, va_caps_filter])?;
|
||||
} else {
|
||||
// NVENC pipeline
|
||||
if let (Some(glupload), Some(glconvert), Some(gl_caps_filter), Some(cudaupload)) =
|
||||
(&glupload, &glconvert, &gl_caps_filter, &cudaupload)
|
||||
{
|
||||
pipeline.add_many(&[glupload, glconvert, gl_caps_filter, cudaupload])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Link main audio branch
|
||||
gstreamer::Element::link_many(&[
|
||||
&audio_source,
|
||||
&audio_source_queue,
|
||||
&audio_converter,
|
||||
&audio_rate,
|
||||
&audio_capsfilter,
|
||||
@@ -488,12 +507,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
gstreamer::Element::link_many(&[&audio_encoder, webrtcsink.upcast_ref()])?;
|
||||
}
|
||||
|
||||
// With DMA-BUF..
|
||||
if args.app.dma_buf {
|
||||
// With zero-copy..
|
||||
if args.app.zero_copy {
|
||||
// VA-API / QSV pipeline
|
||||
if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) {
|
||||
gstreamer::Element::link_many(&[
|
||||
&video_source,
|
||||
&video_source_queue,
|
||||
&caps_filter,
|
||||
&video_queue,
|
||||
&video_clocksync,
|
||||
@@ -501,27 +521,19 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
&va_caps_filter,
|
||||
&video_encoder,
|
||||
])?;
|
||||
} else {
|
||||
} else if video_encoder_info.encoder_api == EncoderAPI::NVENC {
|
||||
// NVENC pipeline
|
||||
if let (Some(glupload), Some(glconvert), Some(gl_caps_filter), Some(cudaupload)) =
|
||||
(&glupload, &glconvert, &gl_caps_filter, &cudaupload)
|
||||
{
|
||||
gstreamer::Element::link_many(&[
|
||||
&video_source,
|
||||
&caps_filter,
|
||||
&video_queue,
|
||||
&video_clocksync,
|
||||
&glupload,
|
||||
&glconvert,
|
||||
&gl_caps_filter,
|
||||
&cudaupload,
|
||||
&video_encoder,
|
||||
])?;
|
||||
}
|
||||
gstreamer::Element::link_many(&[
|
||||
&video_source,
|
||||
&video_source_queue,
|
||||
&caps_filter,
|
||||
&video_encoder,
|
||||
])?;
|
||||
}
|
||||
} else {
|
||||
gstreamer::Element::link_many(&[
|
||||
&video_source,
|
||||
&video_source_queue,
|
||||
&caps_filter,
|
||||
&video_queue,
|
||||
&video_clocksync,
|
||||
@@ -537,8 +549,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
gstreamer::Element::link_many(&[&video_encoder, webrtcsink.upcast_ref()])?;
|
||||
}
|
||||
|
||||
// Set QOS
|
||||
video_encoder.set_property("qos", true);
|
||||
// Make sure QOS is disabled to avoid latency
|
||||
video_encoder.set_property("qos", false);
|
||||
|
||||
// Optimize latency of pipeline
|
||||
video_source
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::input::controller::ControllerManager;
|
||||
use crate::messages::{MessageBase, MessageICE, MessageRaw, MessageSDP};
|
||||
use crate::p2p::p2p::NestriConnection;
|
||||
use crate::p2p::p2p_protocol_stream::NestriStreamProtocol;
|
||||
@@ -5,7 +6,7 @@ use crate::proto::proto::proto_input::InputType::{
|
||||
KeyDown, KeyUp, MouseKeyDown, MouseKeyUp, MouseMove, MouseMoveAbs, MouseWheel,
|
||||
};
|
||||
use crate::proto::proto::{ProtoInput, ProtoMessageInput};
|
||||
use atomic_refcell::AtomicRefCell;
|
||||
use anyhow::Result;
|
||||
use glib::subclass::prelude::*;
|
||||
use gstreamer::glib;
|
||||
use gstreamer::prelude::*;
|
||||
@@ -14,6 +15,7 @@ use gstrswebrtc::signaller::{Signallable, SignallableImpl};
|
||||
use parking_lot::RwLock as PLRwLock;
|
||||
use prost::Message;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit;
|
||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||
|
||||
@@ -21,7 +23,9 @@ pub struct Signaller {
|
||||
stream_room: PLRwLock<Option<String>>,
|
||||
stream_protocol: PLRwLock<Option<Arc<NestriStreamProtocol>>>,
|
||||
wayland_src: PLRwLock<Option<Arc<gstreamer::Element>>>,
|
||||
data_channel: AtomicRefCell<Option<gstreamer_webrtc::WebRTCDataChannel>>,
|
||||
data_channel: PLRwLock<Option<Arc<gstreamer_webrtc::WebRTCDataChannel>>>,
|
||||
controller_manager: PLRwLock<Option<Arc<ControllerManager>>>,
|
||||
rumble_rx: Mutex<Option<mpsc::Receiver<(u32, u16, u16, u16)>>>,
|
||||
}
|
||||
impl Default for Signaller {
|
||||
fn default() -> Self {
|
||||
@@ -29,15 +33,14 @@ impl Default for Signaller {
|
||||
stream_room: PLRwLock::new(None),
|
||||
stream_protocol: PLRwLock::new(None),
|
||||
wayland_src: PLRwLock::new(None),
|
||||
data_channel: AtomicRefCell::new(None),
|
||||
data_channel: PLRwLock::new(None),
|
||||
controller_manager: PLRwLock::new(None),
|
||||
rumble_rx: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Signaller {
|
||||
pub async fn set_nestri_connection(
|
||||
&self,
|
||||
nestri_conn: NestriConnection,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub async fn set_nestri_connection(&self, nestri_conn: NestriConnection) -> Result<()> {
|
||||
let stream_protocol = NestriStreamProtocol::new(nestri_conn).await?;
|
||||
*self.stream_protocol.write() = Some(Arc::new(stream_protocol));
|
||||
Ok(())
|
||||
@@ -59,14 +62,29 @@ impl Signaller {
|
||||
self.wayland_src.read().clone()
|
||||
}
|
||||
|
||||
pub fn set_controller_manager(&self, controller_manager: Arc<ControllerManager>) {
|
||||
*self.controller_manager.write() = Some(controller_manager);
|
||||
}
|
||||
|
||||
pub fn get_controller_manager(&self) -> Option<Arc<ControllerManager>> {
|
||||
self.controller_manager.read().clone()
|
||||
}
|
||||
|
||||
pub async fn set_rumble_rx(&self, rumble_rx: mpsc::Receiver<(u32, u16, u16, u16)>) {
|
||||
*self.rumble_rx.lock().await = Some(rumble_rx);
|
||||
}
|
||||
|
||||
// Change getter to take ownership:
|
||||
pub async fn take_rumble_rx(&self) -> Option<mpsc::Receiver<(u32, u16, u16, u16)>> {
|
||||
self.rumble_rx.lock().await.take()
|
||||
}
|
||||
|
||||
pub fn set_data_channel(&self, data_channel: gstreamer_webrtc::WebRTCDataChannel) {
|
||||
match self.data_channel.try_borrow_mut() {
|
||||
Ok(mut dc) => *dc = Some(data_channel),
|
||||
Err(_) => gstreamer::warning!(
|
||||
gstreamer::CAT_DEFAULT,
|
||||
"Failed to set data channel - already borrowed"
|
||||
),
|
||||
}
|
||||
*self.data_channel.write() = Some(Arc::new(data_channel));
|
||||
}
|
||||
|
||||
pub fn get_data_channel(&self) -> Option<Arc<gstreamer_webrtc::WebRTCDataChannel>> {
|
||||
self.data_channel.read().clone()
|
||||
}
|
||||
|
||||
/// Helper method to clean things up
|
||||
@@ -79,15 +97,15 @@ impl Signaller {
|
||||
let self_obj = self.obj().clone();
|
||||
stream_protocol.register_callback("answer", move |data| {
|
||||
if let Ok(message) = serde_json::from_slice::<MessageSDP>(&data) {
|
||||
let sdp =
|
||||
gst_sdp::SDPMessage::parse_buffer(message.sdp.sdp.as_bytes()).unwrap();
|
||||
let sdp = gst_sdp::SDPMessage::parse_buffer(message.sdp.sdp.as_bytes())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid SDP in 'answer': {e:?}"))?;
|
||||
let answer = WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp);
|
||||
self_obj.emit_by_name::<()>(
|
||||
Ok(self_obj.emit_by_name::<()>(
|
||||
"session-description",
|
||||
&[&"unique-session-id", &answer],
|
||||
);
|
||||
))
|
||||
} else {
|
||||
gstreamer::error!(gstreamer::CAT_DEFAULT, "Failed to decode SDP message");
|
||||
anyhow::bail!("Failed to decode SDP message");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -98,7 +116,7 @@ impl Signaller {
|
||||
let candidate = message.candidate;
|
||||
let sdp_m_line_index = candidate.sdp_mline_index.unwrap_or(0) as u32;
|
||||
let sdp_mid = candidate.sdp_mid;
|
||||
self_obj.emit_by_name::<()>(
|
||||
Ok(self_obj.emit_by_name::<()>(
|
||||
"handle-ice",
|
||||
&[
|
||||
&"unique-session-id",
|
||||
@@ -106,9 +124,9 @@ impl Signaller {
|
||||
&sdp_mid,
|
||||
&candidate.candidate,
|
||||
],
|
||||
);
|
||||
))
|
||||
} else {
|
||||
gstreamer::error!(gstreamer::CAT_DEFAULT, "Failed to decode ICE message");
|
||||
anyhow::bail!("Failed to decode ICE message");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -131,16 +149,16 @@ impl Signaller {
|
||||
}
|
||||
|
||||
// Send our SDP offer
|
||||
self_obj.emit_by_name::<()>(
|
||||
Ok(self_obj.emit_by_name::<()>(
|
||||
"session-requested",
|
||||
&[
|
||||
&"unique-session-id",
|
||||
&"consumer-identifier",
|
||||
&None::<WebRTCSessionDescription>,
|
||||
],
|
||||
);
|
||||
))
|
||||
} else {
|
||||
gstreamer::error!(gstreamer::CAT_DEFAULT, "Failed to decode answer");
|
||||
anyhow::bail!("Failed to decode answer");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -173,8 +191,25 @@ impl Signaller {
|
||||
if let Some(data_channel) = data_channel {
|
||||
gstreamer::info!(gstreamer::CAT_DEFAULT, "Data channel created");
|
||||
if let Some(wayland_src) = signaller.imp().get_wayland_src() {
|
||||
setup_data_channel(&data_channel, &*wayland_src);
|
||||
signaller.imp().set_data_channel(data_channel);
|
||||
signaller.imp().set_data_channel(data_channel.clone());
|
||||
|
||||
let signaller = signaller.clone();
|
||||
let data_channel = Arc::new(data_channel);
|
||||
let wayland_src = wayland_src.clone();
|
||||
|
||||
// Spawn async task to take the receiver and set up
|
||||
tokio::spawn(async move {
|
||||
let rumble_rx = signaller.imp().take_rumble_rx().await;
|
||||
let controller_manager =
|
||||
signaller.imp().get_controller_manager();
|
||||
|
||||
setup_data_channel(
|
||||
controller_manager,
|
||||
rumble_rx,
|
||||
data_channel,
|
||||
&wayland_src,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
gstreamer::error!(
|
||||
gstreamer::CAT_DEFAULT,
|
||||
@@ -315,31 +350,83 @@ impl ObjectImpl for Signaller {
|
||||
}
|
||||
|
||||
fn setup_data_channel(
|
||||
data_channel: &gstreamer_webrtc::WebRTCDataChannel,
|
||||
controller_manager: Option<Arc<ControllerManager>>,
|
||||
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16)>>, // (slot, strong, weak, duration_ms)
|
||||
data_channel: Arc<gstreamer_webrtc::WebRTCDataChannel>,
|
||||
wayland_src: &gstreamer::Element,
|
||||
) {
|
||||
let wayland_src = wayland_src.clone();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
||||
|
||||
data_channel.connect_on_message_data(move |_data_channel, data| {
|
||||
if let Some(data) = data {
|
||||
match ProtoMessageInput::decode(data.to_vec().as_slice()) {
|
||||
// Spawn async processor
|
||||
tokio::spawn(async move {
|
||||
while let Some(data) = rx.recv().await {
|
||||
match ProtoMessageInput::decode(data.as_slice()) {
|
||||
Ok(message_input) => {
|
||||
if let Some(input_msg) = message_input.data {
|
||||
// Process the input message and create an event
|
||||
if let Some(event) = handle_input_message(input_msg) {
|
||||
// Send the event to wayland source, result bool is ignored
|
||||
let _ = wayland_src.send_event(event);
|
||||
if let Some(message_base) = message_input.message_base {
|
||||
if message_base.payload_type == "input" {
|
||||
if let Some(input_data) = message_input.data {
|
||||
if let Some(event) = handle_input_message(input_data) {
|
||||
// Send the event to wayland source, result bool is ignored
|
||||
let _ = wayland_src.send_event(event);
|
||||
}
|
||||
}
|
||||
} else if message_base.payload_type == "controllerInput" {
|
||||
if let Some(controller_manager) = &controller_manager {
|
||||
if let Some(input_data) = message_input.data {
|
||||
let _ = controller_manager.send_command(input_data).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Failed to parse InputMessage");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to decode MessageInput: {:?}", e);
|
||||
tracing::error!("Failed to decode input message: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn rumble sender
|
||||
if let Some(mut rumble_rx) = rumble_rx {
|
||||
let data_channel_clone = data_channel.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some((slot, strong, weak, duration_ms)) = rumble_rx.recv().await {
|
||||
let rumble_msg = ProtoMessageInput {
|
||||
message_base: Some(crate::proto::proto::ProtoMessageBase {
|
||||
payload_type: "controllerInput".to_string(),
|
||||
latency: None,
|
||||
}),
|
||||
data: Some(ProtoInput {
|
||||
input_type: Some(
|
||||
crate::proto::proto::proto_input::InputType::ControllerRumble(
|
||||
crate::proto::proto::ProtoControllerRumble {
|
||||
r#type: "ControllerRumble".to_string(),
|
||||
slot: slot as i32,
|
||||
low_frequency: weak as i32,
|
||||
high_frequency: strong as i32,
|
||||
duration: duration_ms as i32,
|
||||
},
|
||||
),
|
||||
),
|
||||
}),
|
||||
};
|
||||
|
||||
let data = rumble_msg.encode_to_vec();
|
||||
let bytes = glib::Bytes::from_owned(data);
|
||||
|
||||
if let Err(e) = data_channel_clone.send_data_full(Some(&bytes)) {
|
||||
tracing::warn!("Failed to send rumble data: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
data_channel.connect_on_message_data(move |_data_channel, data| {
|
||||
if let Some(data) = data {
|
||||
let _ = tx.send(data.to_vec());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_input_message(input_msg: ProtoInput) -> Option<gstreamer::Event> {
|
||||
@@ -401,6 +488,7 @@ fn handle_input_message(input_msg: ProtoInput) -> Option<gstreamer::Event> {
|
||||
|
||||
Some(gstreamer::event::CustomUpstream::new(structure))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::input::controller::ControllerManager;
|
||||
use crate::p2p::p2p::NestriConnection;
|
||||
use gstreamer::glib;
|
||||
use gstreamer::subclass::prelude::*;
|
||||
use gstrswebrtc::signaller::Signallable;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
mod imp;
|
||||
|
||||
@@ -15,11 +17,19 @@ impl NestriSignaller {
|
||||
room: String,
|
||||
nestri_conn: NestriConnection,
|
||||
wayland_src: Arc<gstreamer::Element>,
|
||||
controller_manager: Option<Arc<ControllerManager>>,
|
||||
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16)>>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let obj: Self = glib::Object::new();
|
||||
obj.imp().set_stream_room(room);
|
||||
obj.imp().set_nestri_connection(nestri_conn).await?;
|
||||
obj.imp().set_wayland_src(wayland_src);
|
||||
if let Some(controller_manager) = controller_manager {
|
||||
obj.imp().set_controller_manager(controller_manager);
|
||||
}
|
||||
if let Some(rumble_rx) = rumble_rx {
|
||||
obj.imp().set_rumble_rx(rumble_rx).await;
|
||||
}
|
||||
Ok(obj)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use libp2p::futures::StreamExt;
|
||||
use libp2p::multiaddr::Protocol;
|
||||
use libp2p::{
|
||||
@@ -11,7 +12,6 @@ use libp2p_ping as ping;
|
||||
use libp2p_stream as stream;
|
||||
use libp2p_tcp as tcp;
|
||||
use libp2p_yamux as yamux;
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -46,7 +46,7 @@ pub struct NestriP2P {
|
||||
swarm: Arc<Mutex<Swarm<NestriBehaviour>>>,
|
||||
}
|
||||
impl NestriP2P {
|
||||
pub async fn new() -> Result<Self, Box<dyn Error>> {
|
||||
pub async fn new() -> Result<Self> {
|
||||
let swarm = Arc::new(Mutex::new(
|
||||
libp2p::SwarmBuilder::with_new_identity()
|
||||
.with_tokio()
|
||||
@@ -69,14 +69,16 @@ impl NestriP2P {
|
||||
Ok(NestriP2P { swarm })
|
||||
}
|
||||
|
||||
pub async fn connect(&self, conn_url: &str) -> Result<NestriConnection, Box<dyn Error>> {
|
||||
pub async fn connect(&self, conn_url: &str) -> Result<NestriConnection> {
|
||||
let conn_addr: Multiaddr = conn_url.parse()?;
|
||||
|
||||
let mut swarm_lock = self.swarm.lock().await;
|
||||
swarm_lock.dial(conn_addr.clone())?;
|
||||
|
||||
let Some(Protocol::P2p(peer_id)) = conn_addr.clone().iter().last() else {
|
||||
return Err("Invalid connection URL: missing peer ID".into());
|
||||
return Err(anyhow::Error::msg(
|
||||
"Invalid multiaddr: missing /p2p/<peer_id>",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(NestriConnection {
|
||||
@@ -88,10 +90,7 @@ impl NestriP2P {
|
||||
|
||||
async fn swarm_loop(swarm: Arc<Mutex<Swarm<NestriBehaviour>>>) {
|
||||
loop {
|
||||
let event = {
|
||||
let mut swarm_lock = swarm.lock().await;
|
||||
swarm_lock.select_next_some().await
|
||||
};
|
||||
let event = swarm.lock().await.select_next_some().await;
|
||||
match event {
|
||||
/* Ping Events */
|
||||
SwarmEvent::Behaviour(NestriBehaviourEvent::Ping(ping::Event {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use crate::p2p::p2p::NestriConnection;
|
||||
use crate::p2p::p2p_safestream::SafeStream;
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use libp2p::StreamProtocol;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{self, Duration};
|
||||
|
||||
// Cloneable callback type
|
||||
pub type CallbackInner = dyn Fn(Vec<u8>) + Send + Sync + 'static;
|
||||
pub type CallbackInner = dyn Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static;
|
||||
pub struct Callback(Arc<CallbackInner>);
|
||||
impl Callback {
|
||||
pub fn new<F>(f: F) -> Self
|
||||
where
|
||||
F: Fn(Vec<u8>) + Send + Sync + 'static,
|
||||
F: Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
Callback(Arc::new(f))
|
||||
}
|
||||
|
||||
pub fn call(&self, data: Vec<u8>) {
|
||||
pub fn call(&self, data: Vec<u8>) -> Result<()> {
|
||||
self.0(data)
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,7 @@ impl NestriStreamProtocol {
|
||||
const NESTRI_PROTOCOL_STREAM_PUSH: StreamProtocol =
|
||||
StreamProtocol::new("/nestri-relay/stream-push/1.0.0");
|
||||
|
||||
pub async fn new(
|
||||
nestri_connection: NestriConnection,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
pub async fn new(nestri_connection: NestriConnection) -> Result<Self> {
|
||||
let mut nestri_connection = nestri_connection.clone();
|
||||
let push_stream = match nestri_connection
|
||||
.control
|
||||
@@ -55,7 +53,10 @@ impl NestriStreamProtocol {
|
||||
{
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
return Err(Box::new(e));
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Failed to open push stream: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,7 +74,7 @@ impl NestriStreamProtocol {
|
||||
Ok(sp)
|
||||
}
|
||||
|
||||
pub fn restart(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn restart(&mut self) -> Result<()> {
|
||||
// Return if tx and handles are already initialized
|
||||
if self.tx.is_some() && self.read_handle.is_some() && self.write_handle.is_some() {
|
||||
tracing::warn!("NestriStreamProtocol is already running, restart skipped");
|
||||
@@ -111,13 +112,9 @@ impl NestriStreamProtocol {
|
||||
// we just get the callback directly if it exists
|
||||
if let Some(callback) = callbacks.get(&response_type) {
|
||||
// Execute the callback
|
||||
if let Err(e) =
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
callback.call(data.clone())
|
||||
}))
|
||||
{
|
||||
if let Err(e) = callback.call(data.clone()) {
|
||||
tracing::error!(
|
||||
"Callback for response type '{}' panicked: {:?}",
|
||||
"Callback for response type '{}' errored: {:?}",
|
||||
response_type,
|
||||
e
|
||||
);
|
||||
@@ -133,9 +130,6 @@ impl NestriStreamProtocol {
|
||||
tracing::error!("Failed to decode message: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a small sleep to reduce CPU usage
|
||||
time::sleep(Duration::from_micros(100)).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -156,27 +150,20 @@ impl NestriStreamProtocol {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a small sleep to reduce CPU usage
|
||||
time::sleep(Duration::from_micros(100)).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_message<M: serde::Serialize>(
|
||||
&self,
|
||||
message: &M,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn send_message<M: serde::Serialize>(&self, message: &M) -> Result<()> {
|
||||
let json_data = serde_json::to_vec(message)?;
|
||||
let Some(tx) = &self.tx else {
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::NotConnected,
|
||||
return Err(anyhow::Error::msg(
|
||||
if self.read_handle.is_none() && self.write_handle.is_none() {
|
||||
"NestriStreamProtocol has been shutdown"
|
||||
} else {
|
||||
"NestriStreamProtocol is not properly initialized"
|
||||
},
|
||||
)));
|
||||
));
|
||||
};
|
||||
tx.try_send(json_data)?;
|
||||
Ok(())
|
||||
@@ -184,7 +171,7 @@ impl NestriStreamProtocol {
|
||||
|
||||
pub fn register_callback<F>(&self, response_type: &str, callback: F)
|
||||
where
|
||||
F: Fn(Vec<u8>) + Send + Sync + 'static,
|
||||
F: Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
self.callbacks
|
||||
.insert(response_type.to_string(), Callback::new(callback));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use libp2p::futures::io::{ReadHalf, WriteHalf};
|
||||
use libp2p::futures::{AsyncReadExt, AsyncWriteExt};
|
||||
@@ -19,17 +20,17 @@ impl SafeStream {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_raw(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub async fn send_raw(&self, data: &[u8]) -> Result<()> {
|
||||
self.send_with_length_prefix(data).await
|
||||
}
|
||||
|
||||
pub async fn receive_raw(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
pub async fn receive_raw(&self) -> Result<Vec<u8>> {
|
||||
self.receive_with_length_prefix().await
|
||||
}
|
||||
|
||||
async fn send_with_length_prefix(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn send_with_length_prefix(&self, data: &[u8]) -> Result<()> {
|
||||
if data.len() > MAX_SIZE {
|
||||
return Err("Data exceeds maximum size".into());
|
||||
anyhow::bail!("Data exceeds maximum size");
|
||||
}
|
||||
|
||||
let mut buffer = Vec::with_capacity(4 + data.len());
|
||||
@@ -42,7 +43,7 @@ impl SafeStream {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_with_length_prefix(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
async fn receive_with_length_prefix(&self) -> Result<Vec<u8>> {
|
||||
let mut stream_read = self.stream_read.lock().await;
|
||||
|
||||
// Read length prefix + data in one syscall
|
||||
@@ -51,7 +52,7 @@ impl SafeStream {
|
||||
let length = BigEndian::read_u32(&length_prefix) as usize;
|
||||
|
||||
if length > MAX_SIZE {
|
||||
return Err("Data exceeds maximum size".into());
|
||||
anyhow::bail!("Received data exceeds maximum size");
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; length];
|
||||
|
||||
@@ -3,29 +3,31 @@
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoTimestampEntry {
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub stage: ::prost::alloc::string::String,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub time: ::core::option::Option<::prost_types::Timestamp>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoLatencyTracker {
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub sequence_id: ::prost::alloc::string::String,
|
||||
#[prost(message, repeated, tag = "2")]
|
||||
#[prost(message, repeated, tag="2")]
|
||||
pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>,
|
||||
}
|
||||
// Mouse messages
|
||||
|
||||
/// MouseMove message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseMove {
|
||||
/// Fixed value "MouseMove"
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag = "2")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag = "3")]
|
||||
#[prost(int32, tag="3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseMoveAbs message
|
||||
@@ -33,11 +35,11 @@ pub struct ProtoMouseMove {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseMoveAbs {
|
||||
/// Fixed value "MouseMoveAbs"
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag = "2")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag = "3")]
|
||||
#[prost(int32, tag="3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseWheel message
|
||||
@@ -45,11 +47,11 @@ pub struct ProtoMouseMoveAbs {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseWheel {
|
||||
/// Fixed value "MouseWheel"
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag = "2")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag = "3")]
|
||||
#[prost(int32, tag="3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseKeyDown message
|
||||
@@ -57,9 +59,9 @@ pub struct ProtoMouseWheel {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseKeyDown {
|
||||
/// Fixed value "MouseKeyDown"
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag = "2")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// MouseKeyUp message
|
||||
@@ -67,19 +69,21 @@ pub struct ProtoMouseKeyDown {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseKeyUp {
|
||||
/// Fixed value "MouseKeyUp"
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag = "2")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub key: i32,
|
||||
}
|
||||
// Keyboard messages
|
||||
|
||||
/// KeyDown message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoKeyDown {
|
||||
/// Fixed value "KeyDown"
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag = "2")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// KeyUp message
|
||||
@@ -87,53 +91,185 @@ pub struct ProtoKeyDown {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoKeyUp {
|
||||
/// Fixed value "KeyUp"
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag = "2")]
|
||||
#[prost(int32, tag="2")]
|
||||
pub key: i32,
|
||||
}
|
||||
// Controller messages
|
||||
|
||||
/// ControllerAttach message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerAttach {
|
||||
/// Fixed value "ControllerAttach"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// One of the following enums: "ps", "xbox" or "switch"
|
||||
#[prost(string, tag="2")]
|
||||
pub id: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="3")]
|
||||
pub slot: i32,
|
||||
}
|
||||
/// ControllerDetach message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerDetach {
|
||||
/// Fixed value "ControllerDetach"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
}
|
||||
/// ControllerButton message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerButton {
|
||||
/// Fixed value "ControllerButtons"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Button code (linux input event code)
|
||||
#[prost(int32, tag="3")]
|
||||
pub button: i32,
|
||||
/// true if pressed, false if released
|
||||
#[prost(bool, tag="4")]
|
||||
pub pressed: bool,
|
||||
}
|
||||
/// ControllerTriggers message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerTrigger {
|
||||
/// Fixed value "ControllerTriggers"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Trigger number (0 for left, 1 for right)
|
||||
#[prost(int32, tag="3")]
|
||||
pub trigger: i32,
|
||||
/// trigger value (-32768 to 32767)
|
||||
#[prost(int32, tag="4")]
|
||||
pub value: i32,
|
||||
}
|
||||
/// ControllerSticks message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerStick {
|
||||
/// Fixed value "ControllerStick"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Stick number (0 for left, 1 for right)
|
||||
#[prost(int32, tag="3")]
|
||||
pub stick: i32,
|
||||
/// X axis value (-32768 to 32767)
|
||||
#[prost(int32, tag="4")]
|
||||
pub x: i32,
|
||||
/// Y axis value (-32768 to 32767)
|
||||
#[prost(int32, tag="5")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// ControllerAxis message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerAxis {
|
||||
/// Fixed value "ControllerAxis"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
|
||||
#[prost(int32, tag="3")]
|
||||
pub axis: i32,
|
||||
/// axis value (-1 to 1)
|
||||
#[prost(int32, tag="4")]
|
||||
pub value: i32,
|
||||
}
|
||||
/// ControllerRumble message
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoControllerRumble {
|
||||
/// Fixed value "ControllerRumble"
|
||||
#[prost(string, tag="1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
/// Slot number (0-3)
|
||||
#[prost(int32, tag="2")]
|
||||
pub slot: i32,
|
||||
/// Low frequency rumble (0-65535)
|
||||
#[prost(int32, tag="3")]
|
||||
pub low_frequency: i32,
|
||||
/// High frequency rumble (0-65535)
|
||||
#[prost(int32, tag="4")]
|
||||
pub high_frequency: i32,
|
||||
/// Duration in milliseconds
|
||||
#[prost(int32, tag="5")]
|
||||
pub duration: i32,
|
||||
}
|
||||
/// Union of all Input types
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoInput {
|
||||
#[prost(oneof = "proto_input::InputType", tags = "1, 2, 3, 4, 5, 6, 7")]
|
||||
#[prost(oneof="proto_input::InputType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14")]
|
||||
pub input_type: ::core::option::Option<proto_input::InputType>,
|
||||
}
|
||||
/// Nested message and enum types in `ProtoInput`.
|
||||
pub mod proto_input {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum InputType {
|
||||
#[prost(message, tag = "1")]
|
||||
#[prost(message, tag="1")]
|
||||
MouseMove(super::ProtoMouseMove),
|
||||
#[prost(message, tag = "2")]
|
||||
#[prost(message, tag="2")]
|
||||
MouseMoveAbs(super::ProtoMouseMoveAbs),
|
||||
#[prost(message, tag = "3")]
|
||||
#[prost(message, tag="3")]
|
||||
MouseWheel(super::ProtoMouseWheel),
|
||||
#[prost(message, tag = "4")]
|
||||
#[prost(message, tag="4")]
|
||||
MouseKeyDown(super::ProtoMouseKeyDown),
|
||||
#[prost(message, tag = "5")]
|
||||
#[prost(message, tag="5")]
|
||||
MouseKeyUp(super::ProtoMouseKeyUp),
|
||||
#[prost(message, tag = "6")]
|
||||
#[prost(message, tag="6")]
|
||||
KeyDown(super::ProtoKeyDown),
|
||||
#[prost(message, tag = "7")]
|
||||
#[prost(message, tag="7")]
|
||||
KeyUp(super::ProtoKeyUp),
|
||||
#[prost(message, tag="8")]
|
||||
ControllerAttach(super::ProtoControllerAttach),
|
||||
#[prost(message, tag="9")]
|
||||
ControllerDetach(super::ProtoControllerDetach),
|
||||
#[prost(message, tag="10")]
|
||||
ControllerButton(super::ProtoControllerButton),
|
||||
#[prost(message, tag="11")]
|
||||
ControllerTrigger(super::ProtoControllerTrigger),
|
||||
#[prost(message, tag="12")]
|
||||
ControllerStick(super::ProtoControllerStick),
|
||||
#[prost(message, tag="13")]
|
||||
ControllerAxis(super::ProtoControllerAxis),
|
||||
#[prost(message, tag="14")]
|
||||
ControllerRumble(super::ProtoControllerRumble),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMessageBase {
|
||||
#[prost(string, tag = "1")]
|
||||
#[prost(string, tag="1")]
|
||||
pub payload_type: ::prost::alloc::string::String,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub latency: ::core::option::Option<ProtoLatencyTracker>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMessageInput {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub message_base: ::core::option::Option<ProtoMessageBase>,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub data: ::core::option::Option<ProtoInput>,
|
||||
}
|
||||
// @@protoc_insertion_point(module)
|
||||
|
||||
@@ -4,6 +4,8 @@ option go_package = "relay/internal/proto";
|
||||
|
||||
package proto;
|
||||
|
||||
/* Mouse messages */
|
||||
|
||||
// MouseMove message
|
||||
message ProtoMouseMove {
|
||||
string type = 1; // Fixed value "MouseMove"
|
||||
@@ -37,6 +39,8 @@ message ProtoMouseKeyUp {
|
||||
int32 key = 2;
|
||||
}
|
||||
|
||||
/* Keyboard messages */
|
||||
|
||||
// KeyDown message
|
||||
message ProtoKeyDown {
|
||||
string type = 1; // Fixed value "KeyDown"
|
||||
@@ -49,6 +53,63 @@ message ProtoKeyUp {
|
||||
int32 key = 2;
|
||||
}
|
||||
|
||||
/* Controller messages */
|
||||
|
||||
// ControllerAttach message
|
||||
message ProtoControllerAttach {
|
||||
string type = 1; // Fixed value "ControllerAttach"
|
||||
string id = 2; // One of the following enums: "ps", "xbox" or "switch"
|
||||
int32 slot = 3; // Slot number (0-3)
|
||||
}
|
||||
|
||||
// ControllerDetach message
|
||||
message ProtoControllerDetach {
|
||||
string type = 1; // Fixed value "ControllerDetach"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
}
|
||||
|
||||
// ControllerButton message
|
||||
message ProtoControllerButton {
|
||||
string type = 1; // Fixed value "ControllerButtons"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 button = 3; // Button code (linux input event code)
|
||||
bool pressed = 4; // true if pressed, false if released
|
||||
}
|
||||
|
||||
// ControllerTriggers message
|
||||
message ProtoControllerTrigger {
|
||||
string type = 1; // Fixed value "ControllerTriggers"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 trigger = 3; // Trigger number (0 for left, 1 for right)
|
||||
int32 value = 4; // trigger value (-32768 to 32767)
|
||||
}
|
||||
|
||||
// ControllerSticks message
|
||||
message ProtoControllerStick {
|
||||
string type = 1; // Fixed value "ControllerStick"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 stick = 3; // Stick number (0 for left, 1 for right)
|
||||
int32 x = 4; // X axis value (-32768 to 32767)
|
||||
int32 y = 5; // Y axis value (-32768 to 32767)
|
||||
}
|
||||
|
||||
// ControllerAxis message
|
||||
message ProtoControllerAxis {
|
||||
string type = 1; // Fixed value "ControllerAxis"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 axis = 3; // Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
|
||||
int32 value = 4; // axis value (-1 to 1)
|
||||
}
|
||||
|
||||
// ControllerRumble message
|
||||
message ProtoControllerRumble {
|
||||
string type = 1; // Fixed value "ControllerRumble"
|
||||
int32 slot = 2; // Slot number (0-3)
|
||||
int32 low_frequency = 3; // Low frequency rumble (0-65535)
|
||||
int32 high_frequency = 4; // High frequency rumble (0-65535)
|
||||
int32 duration = 5; // Duration in milliseconds
|
||||
}
|
||||
|
||||
// Union of all Input types
|
||||
message ProtoInput {
|
||||
oneof input_type {
|
||||
@@ -59,5 +120,12 @@ message ProtoInput {
|
||||
ProtoMouseKeyUp mouse_key_up = 5;
|
||||
ProtoKeyDown key_down = 6;
|
||||
ProtoKeyUp key_up = 7;
|
||||
ProtoControllerAttach controller_attach = 8;
|
||||
ProtoControllerDetach controller_detach = 9;
|
||||
ProtoControllerButton controller_button = 10;
|
||||
ProtoControllerTrigger controller_trigger = 11;
|
||||
ProtoControllerStick controller_stick = 12;
|
||||
ProtoControllerAxis controller_axis = 13;
|
||||
ProtoControllerRumble controller_rumble = 14;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user