1 Commits

Author SHA1 Message Date
Philipp Neumann
480370ecae Merge d501b66c11 into a3ee9aadd9 2025-10-13 20:06:54 +01:00
62 changed files with 2286 additions and 4211 deletions

4
.github/CODEOWNERS vendored
View File

@@ -3,13 +3,13 @@
/apps/ @victorpahuus @AquaWolf /apps/ @victorpahuus @AquaWolf
/packages/ui/ @wanjohiryan @victorpahuus @AquaWolf /packages/ui/ @wanjohiryan @victorpahuus @AquaWolf
/protobufs/ @AquaWolf @DatCaptainHorse /protobuf/ @AquaWolf
/infra/ @wanjohiryan /infra/ @wanjohiryan
/packages/core/ @wanjohiryan /packages/core/ @wanjohiryan
/packages/functions/ @wanjohiryan /packages/functions/ @wanjohiryan
/containerfiles/ @DatCaptainHorse /containers/ @DatCaptainHorse
/packages/server/ @DatCaptainHorse /packages/server/ @DatCaptainHorse
/packages/relay/ @DatCaptainHorse /packages/relay/ @DatCaptainHorse
/packages/scripts/ @DatCaptainHorse /packages/scripts/ @DatCaptainHorse

View File

@@ -1,48 +0,0 @@
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"
}
}

View File

@@ -1,11 +1,11 @@
#Tabs not spaces, you moron :) #Tabs not spaces, you moron :)
name: Build nestri-runner name: Build nestri:runner
on: on:
pull_request: pull_request:
paths: paths:
- "containerfiles/runner*.Containerfile" - "containerfiles/runner.Containerfile"
- "packages/scripts/**" - "packages/scripts/**"
- "packages/server/**" - "packages/server/**"
- ".github/workflows/runner.yml" - ".github/workflows/runner.yml"
@@ -14,7 +14,7 @@ on:
push: push:
branches: [dev, production] branches: [dev, production]
paths: paths:
- "containerfiles/runner*.Containerfile" - "containerfiles/runner.Containerfile"
- ".github/workflows/runner.yml" - ".github/workflows/runner.yml"
- "packages/scripts/**" - "packages/scripts/**"
- "packages/server/**" - "packages/server/**"
@@ -26,6 +26,7 @@ on:
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: nestrilabs/nestri IMAGE_NAME: nestrilabs/nestri
BASE_TAG_PREFIX: runner
BASE_IMAGE: docker.io/cachyos/cachyos:latest BASE_IMAGE: docker.io/cachyos/cachyos:latest
# This makes our release ci quit prematurely # This makes our release ci quit prematurely
@@ -35,13 +36,16 @@ env:
jobs: jobs:
build-docker-pr: build-docker-pr:
name: Build images on PR name: Build image on PR
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
steps: steps:
-
name: Checkout repo
uses: actions/checkout@v4
- -
name: Setup Docker Buildx name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -51,30 +55,24 @@ jobs:
with: with:
swap-size-gb: 20 swap-size-gb: 20
- -
name: Build images using bake name: Build Docker image
uses: docker/bake-action@v6 uses: docker/build-push-action@v6
env:
BASE_IMAGE: ${{ env.BASE_IMAGE }}
with: with:
files: | file: containerfiles/runner.Containerfile
./.github/workflows/docker-bake.hcl context: ./
targets: runner
push: false push: false
load: true load: true
tags: nestri:runner
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
build-and-push-docker: build-and-push-docker:
name: Build and push images name: Build and push image
if: ${{ github.ref == 'refs/heads/production' || github.ref == 'refs/heads/dev' }} if: ${{ github.ref == 'refs/heads/production' || github.ref == 'refs/heads/dev' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write 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" } # Disabled until GHA has this
steps: steps:
- -
name: Checkout repo name: Checkout repo
@@ -87,18 +85,20 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ github.token }} password: ${{ github.token }}
- -
name: Extract runner metadata name: Extract Container metadata
id: meta-runner id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.BASE_TAG_PREFIX }}
#
#tag on release, and a nightly build for 'dev'
tags: | tags: |
type=raw,value=nightly${{ matrix.variant.suffix }},enable={{is_default_branch}} type=raw,value=nightly,enable={{is_default_branch}}
type=raw,value={{branch}}${{ matrix.variant.suffix }} type=raw,value={{branch}}
type=raw,value=latest${{ matrix.variant.suffix }},enable=${{ github.ref == format('refs/heads/{0}', 'production') }} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'production') }}
type=semver,pattern={{version}}${{ matrix.variant.suffix }} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}${{ matrix.variant.suffix }} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}${{ matrix.variant.suffix }} type=semver,pattern={{major}}
- -
name: Setup Docker Buildx name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -108,41 +108,14 @@ jobs:
with: with:
swap-size-gb: 20 swap-size-gb: 20
- -
name: Build and push runner-base image name: Build Docker image
uses: docker/build-push-action@v6
with:
file: containerfiles/runner-base.Containerfile
context: ./
push: true
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 uses: docker/build-push-action@v6
with: with:
file: containerfiles/runner.Containerfile file: containerfiles/runner.Containerfile
context: ./ context: ./
push: true push: true
tags: ${{ steps.meta-runner.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta-runner.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | cache-from: type=gha,mode=max
RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }} cache-to: type=gha,mode=max
RUNNER_BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest${{ matrix.variant.suffix }} pull: ${{ github.event_name == 'schedule' }} # Pull base image for scheduled builds
cache-from: type=gha,scope=runner${{ matrix.variant.suffix }},mode=max
cache-to: type=gha,scope=runner${{ matrix.variant.suffix }},mode=max

View File

@@ -3,6 +3,7 @@ FROM docker.io/node:24-alpine AS base
FROM base AS build FROM base AS build
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package.json ./ COPY package.json ./
COPY patches ./patches
COPY packages/input ./packages/input COPY packages/input ./packages/input
COPY packages/play-standalone ./packages/play-standalone COPY packages/play-standalone ./packages/play-standalone
RUN cd packages/play-standalone && npm install && npm run build RUN cd packages/play-standalone && npm install && npm run build

View File

@@ -1,9 +1,9 @@
FROM docker.io/golang:1.25-alpine AS go-build FROM docker.io/golang:1.24-alpine AS go-build
WORKDIR /builder WORKDIR /builder
COPY packages/relay/ /builder/ COPY packages/relay/ /builder/
RUN go build RUN go build
FROM docker.io/golang:1.25-alpine FROM docker.io/golang:1.24-alpine
COPY --from=go-build /builder/relay /relay/relay COPY --from=go-build /builder/relay /relay/relay
WORKDIR /relay WORKDIR /relay
@@ -22,4 +22,8 @@ ENV WEBRTC_NAT_IPS=""
ENV AUTO_ADD_LOCAL_IP=true ENV AUTO_ADD_LOCAL_IP=true
ENV PERSIST_DIR="./persist-data" ENV PERSIST_DIR="./persist-data"
EXPOSE $ENDPOINT_PORT
EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp
EXPOSE $WEBRTC_UDP_MUX/udp
ENTRYPOINT ["/relay/relay"] ENTRYPOINT ["/relay/relay"]

View File

@@ -1,13 +0,0 @@
# 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

View File

@@ -1,218 +0,0 @@
# 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/

View File

@@ -1,13 +1,125 @@
# Container build arguments # # Container build arguments #
ARG RUNNER_BASE_IMAGE=runner-base:latest ARG BASE_IMAGE=docker.io/cachyos/cachyos:latest
ARG RUNNER_BUILDER_IMAGE=runner-builder:latest
#*********************# #******************************************************************************
# Final Runtime Stage # # Base Stage - Updates system packages
#*********************# #******************************************************************************
FROM ${RUNNER_BASE_IMAGE} AS runtime FROM ${BASE_IMAGE} AS base
FROM ${RUNNER_BUILDER_IMAGE} AS builder
FROM runtime 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
### Package Installation ### ### Package Installation ###
# Core system components # Core system components
@@ -15,17 +127,20 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --needed --noconfirm \ pacman -Sy --needed --noconfirm \
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \ vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
vulkan-radeon lib32-vulkan-radeon \ vulkan-radeon lib32-vulkan-radeon \
mesa lib32-mesa \ mesa steam-native-runtime proton-cachyos lib32-mesa \
steam gtk3 lib32-gtk3 \ steam gtk3 lib32-gtk3 \
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \ sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
libssh2 curl wget \
pipewire pipewire-pulse pipewire-alsa wireplumber \ pipewire pipewire-pulse pipewire-alsa wireplumber \
noto-fonts-cjk supervisor jq pacman-contrib \ noto-fonts-cjk supervisor jq chwd lshw pacman-contrib \
hwdata openssh \ hwdata openssh \
# GStreamer stack # GStreamer stack
gst-plugins-good \ gstreamer gst-plugins-base gst-plugins-good \
gst-plugins-bad gst-plugin-pipewire \ gst-plugins-bad gst-plugin-pipewire \
gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \ gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \
gst-plugin-va gst-plugin-qsv && \ 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 && \
# Cleanup # Cleanup
paccache -rk1 && \ paccache -rk1 && \
rm -rf /usr/share/{info,man,doc}/* rm -rf /usr/share/{info,man,doc}/*
@@ -38,7 +153,6 @@ ENV NESTRI_USER="nestri" \
NESTRI_LANG=en_US.UTF-8 \ NESTRI_LANG=en_US.UTF-8 \
NESTRI_XDG_RUNTIME_DIR=/run/user/1000 \ NESTRI_XDG_RUNTIME_DIR=/run/user/1000 \
NESTRI_HOME=/home/nestri \ NESTRI_HOME=/home/nestri \
NESTRI_VIMPUTTI_PATH=/tmp/vimputti-1000 \
NVIDIA_DRIVER_CAPABILITIES=all NVIDIA_DRIVER_CAPABILITIES=all
RUN mkdir -p "/home/${NESTRI_USER}" && \ RUN mkdir -p "/home/${NESTRI_USER}" && \
@@ -60,33 +174,29 @@ RUN mkdir -p /run/dbus && \
-e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \ -e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \
/usr/share/wireplumber/wireplumber.conf /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 && \ RUN mkdir -p /etc/pipewire/pipewire.conf.d && \
mkdir -p /etc/wireplumber/wireplumber.conf.d mkdir -p /etc/wireplumber/wireplumber.conf.d
COPY packages/configs/wireplumber.conf.d/* /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/ COPY packages/configs/pipewire.conf.d/* /etc/pipewire/pipewire.conf.d/
## Steam Configs - Proton (Experimental flavor) ## ## Steam Configs - Proton (CachyOS flavor) ##
RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config" RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config"
COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/" COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/"
### Artifacts from Builder ### ### Artifacts and Verification ###
COPY --from=builder /artifacts/bin/nestri-server /usr/bin/ COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/
COPY --from=builder /artifacts/bin/bwrap /usr/bin/ COPY --from=gst-wayland-cached-builder /artifacts/lib/ /usr/lib/
COPY --from=builder /artifacts/lib/ /usr/lib/ COPY --from=gst-wayland-cached-builder /artifacts/include/ /usr/include/
COPY --from=builder /artifacts/lib32/ /usr/lib32/ RUN which nestri-server && ls -la /usr/lib/ | grep 'gstwaylanddisplay'
COPY --from=builder /artifacts/lib64/ /usr/lib64/
COPY --from=builder /artifacts/bin/vimputti-manager /usr/bin/
### Scripts and Final Configuration ### ### Scripts and Final Configuration ###
COPY packages/scripts/ /etc/nestri/ COPY packages/scripts/ /etc/nestri/
RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \ RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \
chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" && \ chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" && \
sed -i 's/^#\(en_US\.UTF-8\)/\1/' /etc/locale.gen && \ 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 LANG=en_US.UTF-8 locale-gen
# Root for most container engines, nestri-user compatible for apptainer without fakeroot # Root for most container engines, nestri-user compatible for apptainer without fakeroot

View File

@@ -10,7 +10,7 @@
{ {
"0" "0"
{ {
"name" "proton_experimental" "name" "proton-cachyos"
"config" "" "config" ""
"priority" "75" "priority" "75"
} }

View File

@@ -7,23 +7,21 @@
".": "./src/index.ts" ".": "./src/index.ts"
}, },
"devDependencies": { "devDependencies": {
"@bufbuild/buf": "^1.57.2", "@bufbuild/buf": "^1.50.0",
"@bufbuild/protoc-gen-es": "^2.9.0" "@bufbuild/protoc-gen-es": "^2.2.3"
}, },
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.9.0", "@bufbuild/protobuf": "^2.2.3",
"@chainsafe/libp2p-noise": "^16.1.4", "@chainsafe/libp2p-noise": "^16.1.3",
"@chainsafe/libp2p-quic": "^1.1.3", "@chainsafe/libp2p-yamux": "^7.0.1",
"@chainsafe/libp2p-yamux": "^7.0.4", "@libp2p/identify": "^3.0.32",
"@libp2p/identify": "^3.0.39", "@libp2p/interface": "^2.10.2",
"@libp2p/interface": "^2.11.0", "@libp2p/ping": "^2.0.32",
"@libp2p/ping": "^2.0.37", "@libp2p/websockets": "^9.2.13",
"@libp2p/websockets": "^9.2.19", "@multiformats/multiaddr": "^12.4.0",
"@libp2p/webtransport": "^5.0.51",
"@multiformats/multiaddr": "^12.5.1",
"it-length-prefixed": "^10.0.1", "it-length-prefixed": "^10.0.1",
"it-pipe": "^3.0.1", "it-pipe": "^3.0.1",
"libp2p": "^2.10.0", "libp2p": "^2.8.8",
"uint8arraylist": "^2.4.8", "uint8arraylist": "^2.4.8",
"uint8arrays": "^5.1.0" "uint8arrays": "^5.1.0"
} }

View File

@@ -1,107 +1,107 @@
export const keyCodeToLinuxEventCode: { [key: string]: number } = { export const keyCodeToLinuxEventCode: { [key: string]: number } = {
KeyA: 30, 'KeyA': 30,
KeyB: 48, 'KeyB': 48,
KeyC: 46, 'KeyC': 46,
KeyD: 32, 'KeyD': 32,
KeyE: 18, 'KeyE': 18,
KeyF: 33, 'KeyF': 33,
KeyG: 34, 'KeyG': 34,
KeyH: 35, 'KeyH': 35,
KeyI: 23, 'KeyI': 23,
KeyJ: 36, 'KeyJ': 36,
KeyK: 37, 'KeyK': 37,
KeyL: 38, 'KeyL': 38,
KeyM: 50, 'KeyM': 50,
KeyN: 49, 'KeyN': 49,
KeyO: 24, 'KeyO': 24,
KeyP: 25, 'KeyP': 25,
KeyQ: 16, 'KeyQ': 16,
KeyR: 19, 'KeyR': 19,
KeyS: 31, 'KeyS': 31,
KeyT: 20, 'KeyT': 20,
KeyU: 22, 'KeyU': 22,
KeyV: 47, 'KeyV': 47,
KeyW: 17, 'KeyW': 17,
KeyX: 45, 'KeyX': 45,
KeyY: 21, 'KeyY': 21,
KeyZ: 44, 'KeyZ': 44,
Digit1: 2, 'Digit1': 2,
Digit2: 3, 'Digit2': 3,
Digit3: 4, 'Digit3': 4,
Digit4: 5, 'Digit4': 5,
Digit5: 6, 'Digit5': 6,
Digit6: 7, 'Digit6': 7,
Digit7: 8, 'Digit7': 8,
Digit8: 9, 'Digit8': 9,
Digit9: 10, 'Digit9': 10,
Digit0: 11, 'Digit0': 11,
Enter: 28, 'Enter': 28,
Escape: 1, 'Escape': 1,
Backspace: 14, 'Backspace': 14,
Tab: 15, 'Tab': 15,
Space: 57, 'Space': 57,
Minus: 12, 'Minus': 12,
Equal: 13, 'Equal': 13,
BracketLeft: 26, 'BracketLeft': 26,
BracketRight: 27, 'BracketRight': 27,
Backslash: 43, 'Backslash': 43,
Semicolon: 39, 'Semicolon': 39,
Quote: 40, 'Quote': 40,
Backquote: 41, 'Backquote': 41,
Comma: 51, 'Comma': 51,
Period: 52, 'Period': 52,
Slash: 53, 'Slash': 53,
CapsLock: 58, 'CapsLock': 58,
F1: 59, 'F1': 59,
F2: 60, 'F2': 60,
F3: 61, 'F3': 61,
F4: 62, 'F4': 62,
F5: 63, 'F5': 63,
F6: 64, 'F6': 64,
F7: 65, 'F7': 65,
F8: 66, 'F8': 66,
F9: 67, 'F9': 67,
F10: 68, 'F10': 68,
F11: 87, 'F11': 87,
F12: 88, 'F12': 88,
Insert: 110, 'Insert': 110,
Delete: 111, 'Delete': 111,
ArrowUp: 103, 'ArrowUp': 103,
ArrowDown: 108, 'ArrowDown': 108,
ArrowLeft: 105, 'ArrowLeft': 105,
ArrowRight: 106, 'ArrowRight': 106,
Home: 102, 'Home': 102,
End: 107, 'End': 107,
PageUp: 104, 'PageUp': 104,
PageDown: 109, 'PageDown': 109,
NumLock: 69, 'NumLock': 69,
ScrollLock: 70, 'ScrollLock': 70,
Pause: 119, 'Pause': 119,
Numpad0: 82, 'Numpad0': 82,
Numpad1: 79, 'Numpad1': 79,
Numpad2: 80, 'Numpad2': 80,
Numpad3: 81, 'Numpad3': 81,
Numpad4: 75, 'Numpad4': 75,
Numpad5: 76, 'Numpad5': 76,
Numpad6: 77, 'Numpad6': 77,
Numpad7: 71, 'Numpad7': 71,
Numpad8: 72, 'Numpad8': 72,
Numpad9: 73, 'Numpad9': 73,
NumpadDivide: 98, 'NumpadDivide': 98,
NumpadMultiply: 55, 'NumpadMultiply': 55,
NumpadSubtract: 74, 'NumpadSubtract': 74,
NumpadAdd: 78, 'NumpadAdd': 78,
NumpadEnter: 96, 'NumpadEnter': 96,
NumpadDecimal: 83, 'NumpadDecimal': 83,
ControlLeft: 29, 'ControlLeft': 29,
ControlRight: 97, 'ControlRight': 97,
ShiftLeft: 42, 'ShiftLeft': 42,
ShiftRight: 54, 'ShiftRight': 54,
AltLeft: 56, 'AltLeft': 56,
AltRight: 100, 'AltRight': 100,
//'MetaLeft': 125, // Disabled as will break input //'MetaLeft': 125, // Disabled as will break input
//'MetaRight': 126, // Disabled as will break input //'MetaRight': 126, // Disabled as will break input
ContextMenu: 127, 'ContextMenu': 127,
}; };
export const mouseButtonToLinuxEventCode: { [button: number]: number } = { export const mouseButtonToLinuxEventCode: { [button: number]: number } = {
@@ -109,25 +109,5 @@ export const mouseButtonToLinuxEventCode: { [button: number]: number } = {
2: 273, 2: 273,
1: 274, 1: 274,
3: 275, 3: 275,
4: 276, 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,
}; };

View File

@@ -1,509 +0,0 @@
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);
}
}
}

View File

@@ -1,4 +1,3 @@
export * from "./keyboard" export * from "./keyboard"
export * from "./mouse" export * from "./mouse"
export * from "./controller"
export * from "./webrtc-stream" export * from "./webrtc-stream"

View File

@@ -9,23 +9,27 @@ import {
ProtoInputSchema, ProtoInputSchema,
ProtoKeyDownSchema, ProtoKeyDownSchema,
ProtoKeyUpSchema, ProtoKeyUpSchema,
ProtoMouseMoveSchema
} from "./proto/types_pb"; } from "./proto/types_pb";
import {create, toBinary} from "@bufbuild/protobuf"; import {create, toBinary} from "@bufbuild/protobuf";
interface Props { interface Props {
webrtc: WebRTCStream; webrtc: WebRTCStream;
canvas: HTMLCanvasElement;
} }
export class Keyboard { export class Keyboard {
protected wrtc: WebRTCStream; protected wrtc: WebRTCStream;
protected canvas: HTMLCanvasElement;
protected connected!: boolean; protected connected!: boolean;
// Store references to event listeners // Store references to event listeners
private readonly keydownListener: (e: KeyboardEvent) => void; private readonly keydownListener: (e: KeyboardEvent) => void;
private readonly keyupListener: (e: KeyboardEvent) => void; private readonly keyupListener: (e: KeyboardEvent) => void;
constructor({webrtc}: Props) { constructor({webrtc, canvas}: Props) {
this.wrtc = webrtc; this.wrtc = webrtc;
this.canvas = canvas;
this.keydownListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, { this.keydownListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, {
$typeName: "proto.ProtoInput", $typeName: "proto.ProtoInput",
inputType: { inputType: {
@@ -50,12 +54,23 @@ export class Keyboard {
} }
private run() { private run() {
if (this.connected) //calls all the other functions
if (!document.pointerLockElement) {
if (this.connected) {
this.stop() this.stop()
}
return;
}
if (document.pointerLockElement == this.canvas) {
this.connected = true this.connected = true
document.addEventListener("keydown", this.keydownListener, {passive: false}); document.addEventListener("keydown", this.keydownListener, {passive: false});
document.addEventListener("keyup", this.keyupListener, {passive: false}); document.addEventListener("keyup", this.keyupListener, {passive: false});
} else {
if (this.connected) {
this.stop()
}
}
} }
private stop() { private stop() {
@@ -105,6 +120,7 @@ export class Keyboard {
} }
public dispose() { public dispose() {
document.exitPointerLock();
this.stop(); this.stop();
this.connected = false; this.connected = false;
} }

View File

@@ -24,12 +24,13 @@ export class Mouse {
protected canvas: HTMLCanvasElement; protected canvas: HTMLCanvasElement;
protected connected!: boolean; protected connected!: boolean;
private sendInterval = 10 // 100 updates per second
// Store references to event listeners // Store references to event listeners
private sendInterval = 16 //60fps
private readonly mousemoveListener: (e: MouseEvent) => void; private readonly mousemoveListener: (e: MouseEvent) => void;
private movementX: number = 0; private movementX: number = 0;
private movementY: number = 0; private movementY: number = 0;
private isProcessing: boolean = false;
private readonly mousedownListener: (e: MouseEvent) => void; private readonly mousedownListener: (e: MouseEvent) => void;
private readonly mouseupListener: (e: MouseEvent) => void; private readonly mouseupListener: (e: MouseEvent) => void;
@@ -39,7 +40,7 @@ export class Mouse {
this.wrtc = webrtc; this.wrtc = webrtc;
this.canvas = canvas; this.canvas = canvas;
this.sendInterval = 1000 / webrtc.currentFrameRate; this.sendInterval = 1000 / webrtc.currentFrameRate
this.mousemoveListener = (e: MouseEvent) => { this.mousemoveListener = (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
@@ -74,8 +75,8 @@ export class Mouse {
case: "mouseWheel", case: "mouseWheel",
value: create(ProtoMouseWheelSchema, { value: create(ProtoMouseWheelSchema, {
type: "MouseWheel", type: "MouseWheel",
x: Math.round(e.deltaX), x: e.deltaX,
y: Math.round(e.deltaY), y: e.deltaY
}), }),
} }
})); }));
@@ -134,8 +135,8 @@ export class Mouse {
case: "mouseMove", case: "mouseMove",
value: create(ProtoMouseMoveSchema, { value: create(ProtoMouseMoveSchema, {
type: "MouseMove", type: "MouseMove",
x: Math.round(this.movementX), x: this.movementX,
y: Math.round(this.movementY), y: this.movementY,
}), }),
}, },
}); });

View File

@@ -1,9 +1,9 @@
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
// @generated from file latency_tracker.proto (package proto, syntax proto3) // @generated from file latency_tracker.proto (package proto, syntax proto3)
/* eslint-disable */ /* eslint-disable */
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";
import type { Timestamp } from "@bufbuild/protobuf/wkt"; import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf"; import type { Message } from "@bufbuild/protobuf";

View File

@@ -1,9 +1,9 @@
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
// @generated from file messages.proto (package proto, syntax proto3) // @generated from file messages.proto (package proto, syntax proto3)
/* eslint-disable */ /* eslint-disable */
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";
import type { ProtoInput } from "./types_pb"; import type { ProtoInput } from "./types_pb";
import { file_types } from "./types_pb"; import { file_types } from "./types_pb";
import type { ProtoLatencyTracker } from "./latency_tracker_pb"; import type { ProtoLatencyTracker } from "./latency_tracker_pb";

View File

@@ -1,16 +1,16 @@
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
// @generated from file types.proto (package proto, syntax proto3) // @generated from file types.proto (package proto, syntax proto3)
/* eslint-disable */ /* eslint-disable */
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1";
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1";
import type { Message } from "@bufbuild/protobuf"; import type { Message } from "@bufbuild/protobuf";
/** /**
* Describes the file types.proto. * Describes the file types.proto.
*/ */
export const file_types: GenFile = /*@__PURE__*/ export const file_types: GenFile = /*@__PURE__*/
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSI/ChVQcm90b0NvbnRyb2xsZXJBdHRhY2gSDAoEdHlwZRgBIAEoCRIKCgJpZBgCIAEoCRIMCgRzbG90GAMgASgFIjMKFVByb3RvQ29udHJvbGxlckRldGFjaBIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUiVAoVUHJvdG9Db250cm9sbGVyQnV0dG9uEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIOCgZidXR0b24YAyABKAUSDwoHcHJlc3NlZBgEIAEoCCJUChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIPCgd0cmlnZ2VyGAMgASgFEg0KBXZhbHVlGAQgASgFIlcKFFByb3RvQ29udHJvbGxlclN0aWNrEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRINCgVzdGljaxgDIAEoBRIJCgF4GAQgASgFEgkKAXkYBSABKAUiTgoTUHJvdG9Db250cm9sbGVyQXhpcxIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUSDAoEYXhpcxgDIAEoBRINCgV2YWx1ZRgEIAEoBSJ0ChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEdHlwZRgBIAEoCRIMCgRzbG90GAIgASgFEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi9QUKClByb3RvSW5wdXQSKwoKbW91c2VfbW92ZRgBIAEoCzIVLnByb3RvLlByb3RvTW91c2VNb3ZlSAASMgoObW91c2VfbW92ZV9hYnMYAiABKAsyGC5wcm90by5Qcm90b01vdXNlTW92ZUFic0gAEi0KC21vdXNlX3doZWVsGAMgASgLMhYucHJvdG8uUHJvdG9Nb3VzZVdoZWVsSAASMgoObW91c2Vfa2V5X2Rvd24YBCABKAsyGC5wcm90by5Qcm90b01vdXNlS2V5RG93bkgAEi4KDG1vdXNlX2tleV91cBgFIAEoCzIWLnByb3RvLlByb3RvTW91c2VLZXlVcEgAEicKCGtleV9kb3duGAYgASgLMhMucHJvdG8uUHJvdG9LZXlEb3duSAASIwoGa2V5X3VwGAcgASgLMhEucHJvdG8uUHJvdG9LZXlVcEgAEjkKEWNvbnRyb2xsZXJfYXR0YWNoGAggASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyQXR0YWNoSAASOQoRY29udHJvbGxlcl9kZXRhY2gYCSABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJEZXRhY2hIABI5ChFjb250cm9sbGVyX2J1dHRvbhgKIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckJ1dHRvbkgAEjsKEmNvbnRyb2xsZXJfdHJpZ2dlchgLIAEoCzIdLnByb3RvLlByb3RvQ29udHJvbGxlclRyaWdnZXJIABI3ChBjb250cm9sbGVyX3N0aWNrGAwgASgLMhsucHJvdG8uUHJvdG9Db250cm9sbGVyU3RpY2tIABI1Cg9jb250cm9sbGVyX2F4aXMYDSABKAsyGi5wcm90by5Qcm90b0NvbnRyb2xsZXJBeGlzSAASOQoRY29udHJvbGxlcl9ydW1ibGUYDiABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIAEIMCgppbnB1dF90eXBlQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM"); fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSLcAgoKUHJvdG9JbnB1dBIrCgptb3VzZV9tb3ZlGAEgASgLMhUucHJvdG8uUHJvdG9Nb3VzZU1vdmVIABIyCg5tb3VzZV9tb3ZlX2FicxgCIAEoCzIYLnByb3RvLlByb3RvTW91c2VNb3ZlQWJzSAASLQoLbW91c2Vfd2hlZWwYAyABKAsyFi5wcm90by5Qcm90b01vdXNlV2hlZWxIABIyCg5tb3VzZV9rZXlfZG93bhgEIAEoCzIYLnByb3RvLlByb3RvTW91c2VLZXlEb3duSAASLgoMbW91c2Vfa2V5X3VwGAUgASgLMhYucHJvdG8uUHJvdG9Nb3VzZUtleVVwSAASJwoIa2V5X2Rvd24YBiABKAsyEy5wcm90by5Qcm90b0tleURvd25IABIjCgZrZXlfdXAYByABKAsyES5wcm90by5Qcm90b0tleVVwSABCDAoKaW5wdXRfdHlwZUIWWhRyZWxheS9pbnRlcm5hbC9wcm90b2IGcHJvdG8z");
/** /**
* MouseMove message * MouseMove message
@@ -209,293 +209,6 @@ export type ProtoKeyUp = Message<"proto.ProtoKeyUp"> & {
export const ProtoKeyUpSchema: GenMessage<ProtoKeyUp> = /*@__PURE__*/ export const ProtoKeyUpSchema: GenMessage<ProtoKeyUp> = /*@__PURE__*/
messageDesc(file_types, 6); 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 * Union of all Input types
* *
@@ -547,48 +260,6 @@ export type ProtoInput = Message<"proto.ProtoInput"> & {
*/ */
value: ProtoKeyUp; value: ProtoKeyUp;
case: "keyUp"; 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 }; } | { case: undefined; value?: undefined };
}; };
@@ -597,5 +268,5 @@ export type ProtoInput = Message<"proto.ProtoInput"> & {
* Use `create(ProtoInputSchema)` to create a new message. * Use `create(ProtoInputSchema)` to create a new message.
*/ */
export const ProtoInputSchema: GenMessage<ProtoInput> = /*@__PURE__*/ export const ProtoInputSchema: GenMessage<ProtoInput> = /*@__PURE__*/
messageDesc(file_types, 14); messageDesc(file_types, 7);

View File

@@ -5,7 +5,6 @@ import {
SafeStream, SafeStream,
} from "./messages"; } from "./messages";
import { webSockets } from "@libp2p/websockets"; import { webSockets } from "@libp2p/websockets";
import { webTransport } from "@libp2p/webtransport";
import { createLibp2p, Libp2p } from "libp2p"; import { createLibp2p, Libp2p } from "libp2p";
import { noise } from "@chainsafe/libp2p-noise"; import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from "@chainsafe/libp2p-yamux"; import { yamux } from "@chainsafe/libp2p-yamux";
@@ -14,6 +13,9 @@ import { multiaddr } from "@multiformats/multiaddr";
import { Connection } from "@libp2p/interface"; import { Connection } from "@libp2p/interface";
import { ping } from "@libp2p/ping"; import { ping } from "@libp2p/ping";
//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"; const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0";
export class WebRTCStream { export class WebRTCStream {
@@ -28,9 +30,8 @@ export class WebRTCStream {
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined; private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined;
private _serverURL: string | undefined = undefined; private _serverURL: string | undefined = undefined;
private _roomName: string | undefined = undefined; private _roomName: string | undefined = undefined;
private _isConnected: boolean = false; private _isConnected: boolean = false; // Add flag to track connection state
private _dataChannelCallbacks: Array<(data: any) => void> = []; currentFrameRate: number = 60;
currentFrameRate: number = 100;
constructor( constructor(
serverURL: string, serverURL: string,
@@ -58,7 +59,7 @@ export class WebRTCStream {
console.log("Setting up libp2p"); console.log("Setting up libp2p");
this._p2p = await createLibp2p({ this._p2p = await createLibp2p({
transports: [webSockets(), webTransport()], transports: [webSockets()],
connectionEncrypters: [noise()], connectionEncrypters: [noise()],
streamMuxers: [yamux()], streamMuxers: [yamux()],
connectionGater: { connectionGater: {
@@ -218,8 +219,7 @@ export class WebRTCStream {
} }
private _checkConnectionState() { private _checkConnectionState() {
if (!this._pc || !this._p2p || !this._p2pConn) if (!this._pc) return;
return;
console.debug("Checking connection state:", { console.debug("Checking connection state:", {
connectionState: this._pc.connectionState, connectionState: this._pc.connectionState,
@@ -267,9 +267,9 @@ export class WebRTCStream {
this._pc.connectionState === "closed" || this._pc.connectionState === "closed" ||
this._pc.iceConnectionState === "failed" this._pc.iceConnectionState === "failed"
) { ) {
console.log("PeerConnection failed or closed"); console.log("Connection failed or closed, attempting reconnect");
//this._isConnected = false; // Reset connected state this._isConnected = false; // Reset connected state
//this._handleConnectionFailure(); this._handleConnectionFailure();
} }
} }
@@ -318,7 +318,6 @@ export class WebRTCStream {
console.error("Error closing data channel:", err); console.error("Error closing data channel:", err);
} }
this._dataChannel = undefined; this._dataChannel = undefined;
this._dataChannelCallbacks = [];
} }
this._isConnected = false; // Reset connected state during cleanup this._isConnected = false; // Reset connected state during cleanup
} }
@@ -330,31 +329,15 @@ 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() { private _setupDataChannelEvents() {
if (!this._dataChannel) return; if (!this._dataChannel) return;
this._dataChannel.onclose = () => console.log("sendChannel has closed"); this._dataChannel.onclose = () => console.log("sendChannel has closed");
this._dataChannel.onopen = () => console.log("sendChannel has opened"); this._dataChannel.onopen = () => console.log("sendChannel has opened");
this._dataChannel.onmessage = (event => { this._dataChannel.onmessage = (e) =>
// Parse as ProtoBuf message console.log(
const data = event.data; `Message from DataChannel '${this._dataChannel?.label}' payload '${e.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() { private _gatherFrameRate() {

View File

@@ -1,18 +0,0 @@
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

View File

@@ -11,6 +11,6 @@
"dependencies": { "dependencies": {
"@astrojs/node": "^9.4.2", "@astrojs/node": "^9.4.2",
"@nestri/input": "*", "@nestri/input": "*",
"astro": "5.14.5" "astro": "5.13.2"
} }
} }

View File

@@ -24,7 +24,7 @@ if (envs_map.size > 0) {
</DefaultLayout> </DefaultLayout>
<script> <script>
import { Mouse, Keyboard, Controller, WebRTCStream } from "@nestri/input"; import { Mouse, Keyboard, WebRTCStream } from "@nestri/input";
const ENVS = document.getElementById("ENVS")!.dataset.envs as string; const ENVS = document.getElementById("ENVS")!.dataset.envs as string;
let ENVS_MAP: Map<string, string | undefined> | null = null; let ENVS_MAP: Map<string, string | undefined> | null = null;
if (ENVS && ENVS.length > 0) { if (ENVS && ENVS.length > 0) {
@@ -32,11 +32,6 @@ if (envs_map.size > 0) {
console.debug("ENVS_MAP:", ENVS_MAP); console.debug("ENVS_MAP:", ENVS_MAP);
} }
// Method which returns true if mobile device
const isMobile = () => {
return /Mobi|Android/i.test(navigator.userAgent);
};
// Elements // Elements
const canvas = document.getElementById("playCanvas")! as HTMLCanvasElement; const canvas = document.getElementById("playCanvas")! as HTMLCanvasElement;
const offlineText = document.getElementById("offlineText")! as HTMLHeadingElement; const offlineText = document.getElementById("offlineText")! as HTMLHeadingElement;
@@ -87,88 +82,51 @@ if (envs_map.size > 0) {
// Input // Input
let nestriMouse: Mouse | null = null; let nestriMouse: Mouse | null = null;
let nestriKeyboard: Keyboard | 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", () => { document.addEventListener("pointerlockchange", () => {
if (document.pointerLockElement === canvas) { if (document.pointerLockElement === canvas) {
if (nestriMouse) if (nestriMouse || nestriKeyboard)
return; return;
nestriMouse = new Mouse({ nestriMouse = new Mouse({
canvas: canvas, canvas: canvas,
webrtc: stream, webrtc: stream,
}); });
nestriKeyboard = new Keyboard({
canvas: canvas,
webrtc: stream,
});
} else { } else {
if (nestriMouse) { if (nestriMouse) {
nestriMouse.dispose(); nestriMouse.dispose();
nestriMouse = null; nestriMouse = null;
} }
if (nestriKeyboard) {
nestriKeyboard.dispose();
nestriKeyboard = null;
}
} }
}); });
document.addEventListener("fullscreenchange", () => { const lockPlay = async function () {
if (document.fullscreenElement) { await canvas.requestFullscreen();
if (nestriKeyboard) await canvas.requestPointerLock();
return;
nestriKeyboard = new Keyboard({
webrtc: stream,
});
nestriControllers.forEach((c) => c.run());
if (document.fullscreenElement !== null) {
if ("keyboard" in navigator && "lock" in (navigator.keyboard as any)) { if ("keyboard" in navigator && "lock" in (navigator.keyboard as any)) {
const keys = [ const keys = [
"AltLeft", "AltRight", "Tab", "Escape", "AltLeft", "AltRight", "Tab", "Escape",
"ContextMenu", "MetaLeft", "MetaRight" "ContextMenu", "MetaLeft", "MetaRight"
]; ];
(navigator.keyboard as any).lock(keys).then(() => { try {
await (navigator.keyboard as any).lock(keys);
console.log("Keyboard lock acquired"); console.log("Keyboard lock acquired");
}).catch((err: any) => { } catch (e) {
console.error("Failed to acquire keyboard lock:", err); console.warn("Keyboard lock failed:", e);
});
} }
} else {
if (nestriKeyboard) {
nestriKeyboard.dispose();
nestriKeyboard = null;
} }
nestriControllers.forEach((c) => c.stop());
} }
})
const lockPlay = async function () {
if (document.fullscreenElement)
return;
await canvas.requestFullscreen();
if (!isMobile())
await canvas.requestPointerLock();
}; };
canvas.addEventListener("click", lockPlay); canvas.addEventListener("click", lockPlay);

View File

@@ -1 +0,0 @@
persist-data/

View File

@@ -1,50 +1,59 @@
module relay module relay
go 1.25.0 go 1.24
require ( require (
github.com/libp2p/go-libp2p v0.44.0 github.com/libp2p/go-libp2p v0.41.1
github.com/libp2p/go-libp2p-pubsub v0.15.0 github.com/libp2p/go-libp2p-pubsub v0.13.1
github.com/libp2p/go-reuseport v0.4.0 github.com/libp2p/go-reuseport v0.4.0
github.com/multiformats/go-multiaddr v0.16.1 github.com/multiformats/go-multiaddr v0.15.0
github.com/oklog/ulid/v2 v2.1.1 github.com/oklog/ulid/v2 v2.1.1
github.com/pion/ice/v4 v4.0.10 github.com/pion/ice/v4 v4.0.10
github.com/pion/interceptor v0.1.41 github.com/pion/interceptor v0.1.38
github.com/pion/rtp v1.8.24 github.com/pion/rtp v1.8.15
github.com/pion/webrtc/v4 v4.1.6 github.com/pion/webrtc/v4 v4.1.1
github.com/prometheus/client_golang v1.23.2 google.golang.org/protobuf v1.36.6
google.golang.org/protobuf v1.36.10
) )
require ( require (
github.com/benbjohnson/clock v1.3.5 // indirect github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // 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/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // 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/filecoin-project/go-clock v0.1.0 // indirect
github.com/flynn/noise v1.1.0 // indirect github.com/flynn/noise v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // 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/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/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huin/goupnp v1.3.0 // indirect github.com/huin/goupnp v1.3.0 // indirect
github.com/ipfs/go-cid v0.5.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/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/koron/go-ssdp v0.1.0 // indirect github.com/koron/go-ssdp v0.0.6 // indirect
github.com/libp2p/go-buffer-pool 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-flow-metrics v0.3.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // 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-msgio v0.3.0 // indirect
github.com/libp2p/go-netroute v0.3.0 // indirect github.com/libp2p/go-netroute v0.2.2 // indirect
github.com/libp2p/go-yamux/v5 v5.1.0 // indirect github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/miekg/dns v1.1.68 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
@@ -54,51 +63,54 @@ require (
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.10.0 // indirect github.com/multiformats/go-multicodec v0.9.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.6.1 // indirect github.com/multiformats/go-multistream v0.6.0 // indirect
github.com/multiformats/go-varint v0.1.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pion/datachannel v1.5.10 // indirect github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/dtls/v3 v3.0.7 // indirect github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.16 // indirect github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/sctp v1.8.40 // indirect github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.16 // indirect github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.8 // indirect github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun v0.6.1 // indirect github.com/pion/stun v0.6.1 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.0.8 // indirect github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.1.1 // 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/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.1 // indirect github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect github.com/quic-go/quic-go v0.52.0 // indirect
github.com/quic-go/webtransport-go v0.9.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/rogpeppe/go-internal v1.13.1 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/wlynxg/anet v0.0.5 // 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/dig v1.19.0 // indirect
go.uber.org/fx v1.24.0 // indirect go.uber.org/fx v1.24.0 // indirect
go.uber.org/mock v0.6.0 // indirect go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/crypto v0.38.0 // indirect
golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.40.0 // indirect
golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.14.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.25.0 // indirect
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef // indirect golang.org/x/tools v0.33.0 // 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 lukechampine.com/blake3 v1.4.1 // indirect
) )

View File

@@ -9,6 +9,7 @@ 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= 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/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/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 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 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= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -18,8 +19,17 @@ 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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/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 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -29,7 +39,13 @@ 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/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 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 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/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 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= 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= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
@@ -41,7 +57,16 @@ 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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 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-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.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 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 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= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -52,12 +77,17 @@ 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/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/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.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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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-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/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/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-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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
@@ -73,6 +103,8 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 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 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 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 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 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= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
@@ -80,14 +112,15 @@ 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/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/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/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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y= github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU=
github.com/koron/go-ssdp v0.1.0/go.mod h1:GltaDBjtK1kemZOusWYLGotV0kBeEf59Bp0wtSB0uyU= github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -97,41 +130,39 @@ 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 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 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784=
github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo=
github.com/libp2p/go-libp2p v0.44.0 h1:5Gtt8OrF8yiXmH+Mx4+/iBeFRMK1TY3a8OrEBDEqAvs= github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE=
github.com/libp2p/go-libp2p v0.44.0/go.mod h1:NovCojezAt4dnDd4fH048K7PKEqH0UFYYqJRjIIu8zc= github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI=
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= 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-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8=
github.com/libp2p/go-libp2p-pubsub v0.15.0 h1:cG7Cng2BT82WttmPFMi50gDNV+58K626m/wR00vGL1o= github.com/libp2p/go-libp2p-pubsub v0.13.1 h1:tV3ttzzZSCk0EtEXnxVmWIXgjVxXx+20Jwjbs/Ctzjo=
github.com/libp2p/go-libp2p-pubsub v0.15.0/go.mod h1:lr4oE8bFgQaifRcoc2uWhWWiK6tPdOEKpUuR408GFN4= github.com/libp2p/go-libp2p-pubsub v0.13.1/go.mod h1:MKPU5vMI8RRFyTP0HfdsF9cLmL1nHAeJm44AxJGJx44=
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 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-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 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8=
github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE=
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= 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-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q= 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/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/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/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 h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= 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/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/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.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= 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/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
@@ -152,29 +183,36 @@ 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 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 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.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo=
github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multiaddr v0.15.0/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 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M=
github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= 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 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 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 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= 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 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA=
github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg=
github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 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/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/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/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 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= 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/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 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
@@ -184,29 +222,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.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 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= 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/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw= github.com/pion/interceptor v0.1.38 h1:Mgt3XIIq47uR5vcLLahfRucE6tFPjxHak+z5ZZFEzLU=
github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY= github.com/pion/interceptor v0.1.38/go.mod h1:HS9X+Ue5LDE6q2C2tuvOuO83XkBdJFgn6MBDtfoJX4Q=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= 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/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 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.24 h1:+ICyZXUQDv95EsHN70RrA4XKJf5MGWyC6QQc1u6/ynI= github.com/pion/rtp v1.8.15 h1:MuhuGn1cxpVCPLNY1lI7F1tQ8Spntpgf12ob+pOYT8s=
github.com/pion/rtp v1.8.24/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= github.com/pion/rtp v1.8.15/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8= github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo= github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM= github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg= github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= 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 v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
@@ -215,36 +253,43 @@ 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.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= 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/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw= github.com/pion/webrtc/v4 v4.1.1 h1:PMFPtLg1kpD2pVtun+LGUzA3k54JdFl87WO0Z1+HKug=
github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU= github.com/pion/webrtc/v4 v4.1.1/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 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 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 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.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 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/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70= github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao= 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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 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/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 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/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/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= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
@@ -266,8 +311,10 @@ 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/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/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 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/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/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/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/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
@@ -276,13 +323,15 @@ 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.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.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.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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 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/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
@@ -292,20 +341,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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.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 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= 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 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 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/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 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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= 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/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= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -320,20 +369,22 @@ 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.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.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.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-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-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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -355,8 +406,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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 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.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -372,14 +423,17 @@ 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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-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-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-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-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-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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -390,14 +444,13 @@ 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-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.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.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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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.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.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -414,24 +467,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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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.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-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-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-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-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-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-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-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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -452,9 +505,10 @@ 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.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.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=

View File

@@ -30,12 +30,29 @@ func InitWebRTCAPI() error {
return fmt.Errorf("failed to register extensions: %w", err) return fmt.Errorf("failed to register extensions: %w", err)
} }
// Default codecs cover our needs // Default codecs cover most of our needs
err = mediaEngine.RegisterDefaultCodecs() err = mediaEngine.RegisterDefaultCodecs()
if err != nil { if err != nil {
return err 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 // Interceptor registry
interceptorRegistry := &interceptor.Registry{} interceptorRegistry := &interceptor.Registry{}
@@ -81,8 +98,7 @@ func InitWebRTCAPI() error {
slog.Info("Using WebRTC UDP Port Range", "start", flags.WebRTCUDPStart, "end", flags.WebRTCUDPEnd) slog.Info("Using WebRTC UDP Port Range", "start", flags.WebRTCUDPStart, "end", flags.WebRTCUDPEnd)
} }
// Improves speed when sending offers to browsers (https://github.com/pion/webrtc/issues/3174) settingEngine.SetIncludeLoopbackCandidate(true) // Just in case
settingEngine.SetIncludeLoopbackCandidate(true)
// Create a new API object with our customized settings // Create a new API object with our customized settings
globalWebRTCAPI = webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithSettingEngine(settingEngine), webrtc.WithInterceptorRegistry(interceptorRegistry)) globalWebRTCAPI = webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithSettingEngine(settingEngine), webrtc.WithInterceptorRegistry(interceptorRegistry))

View File

@@ -24,8 +24,6 @@ type Flags struct {
AutoAddLocalIP bool // Automatically add local IP to NAT 1 to 1 IPs 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 NAT11IP string // WebRTC NAT 1 to 1 IP - allows specifying IP of relay if behind NAT
PersistDir string // Directory to save persistent data to PersistDir string // Directory to save persistent data to
Metrics bool // Enable metrics endpoint
MetricsPort int // Port for metrics endpoint
} }
func (flags *Flags) DebugLog() { func (flags *Flags) DebugLog() {
@@ -41,8 +39,6 @@ func (flags *Flags) DebugLog() {
"autoAddLocalIP", flags.AutoAddLocalIP, "autoAddLocalIP", flags.AutoAddLocalIP,
"webrtcNAT11IPs", flags.NAT11IP, "webrtcNAT11IPs", flags.NAT11IP,
"persistDir", flags.PersistDir, "persistDir", flags.PersistDir,
"metrics", flags.Metrics,
"metricsPort", flags.MetricsPort,
) )
} }
@@ -83,14 +79,12 @@ func InitFlags() {
flag.IntVar(&globalFlags.WebRTCUDPStart, "webrtcUDPStart", getEnvAsInt("WEBRTC_UDP_START", 0), "WebRTC UDP port range start") 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.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.StringVar(&globalFlags.STUNServer, "stunServer", getEnvAsString("STUN_SERVER", "stun.l.google.com:19302"), "WebRTC STUN server")
flag.IntVar(&globalFlags.UDPMuxPort, "webrtcUDPMux", getEnvAsInt("WEBRTC_UDP_MUX", 9099), "WebRTC UDP mux port") flag.IntVar(&globalFlags.UDPMuxPort, "webrtcUDPMux", getEnvAsInt("WEBRTC_UDP_MUX", 8088), "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") flag.BoolVar(&globalFlags.AutoAddLocalIP, "autoAddLocalIP", getEnvAsBool("AUTO_ADD_LOCAL_IP", true), "Automatically add local IP to NAT 1 to 1 IPs")
// String with comma separated IPs // String with comma separated IPs
nat11IP := "" nat11IP := ""
flag.StringVar(&nat11IP, "webrtcNAT11IP", getEnvAsString("WEBRTC_NAT_IP", ""), "WebRTC NAT 1 to 1 IP") 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.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 // Parse flags
flag.Parse() flag.Parse()

View File

@@ -4,31 +4,24 @@ import (
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"net/http"
"os" "os"
"relay/internal/common" "relay/internal/common"
"relay/internal/shared" "relay/internal/shared"
"time"
"github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p"
pubsub "github.com/libp2p/go-libp2p-pubsub" pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer" "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/protocol/ping"
"github.com/libp2p/go-libp2p/p2p/security/noise" "github.com/libp2p/go-libp2p/p2p/security/noise"
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
"github.com/libp2p/go-libp2p/p2p/transport/tcp" "github.com/libp2p/go-libp2p/p2p/transport/tcp"
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket" 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/multiformats/go-multiaddr"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
) )
// -- Variables -- // -- Variables --
@@ -37,9 +30,17 @@ var globalRelay *Relay
// -- Structs -- // -- 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 // Relay structure enhanced with metrics and state
type Relay struct { type Relay struct {
*PeerInfo RelayInfo
Host host.Host // libp2p host for peer-to-peer networking Host host.Host // libp2p host for peer-to-peer networking
PubSub *pubsub.PubSub // PubSub for state synchronization PubSub *pubsub.PubSub // PubSub for state synchronization
@@ -47,6 +48,7 @@ type Relay struct {
// Local // Local
LocalRooms *common.SafeMap[ulid.ULID, *shared.Room] // room ID -> local Room struct (hosted by this relay) LocalRooms *common.SafeMap[ulid.ULID, *shared.Room] // room ID -> local Room struct (hosted by this relay)
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) LocalMeshConnections *common.SafeMap[peer.ID, *webrtc.PeerConnection] // peer ID -> PeerConnection (connected to this relay)
// Protocols // Protocols
@@ -58,43 +60,11 @@ type Relay struct {
} }
func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay, error) { 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{ listenAddrs := []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4 - Raw TCP fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4 - Raw TCP
fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6 - 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("/ip4/0.0.0.0/tcp/%d/ws", port), // IPv4 - TCP WebSocket
fmt.Sprintf("/ip6/::/tcp/%d/ws", port), // IPv6 - 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 var muAddrs []multiaddr.Multiaddr
@@ -108,12 +78,11 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
// Initialize libp2p host // Initialize libp2p host
p2pHost, err := libp2p.New( p2pHost, err := libp2p.New(
libp2p.ChainOptions(metricsOpts...), // TODO: Currently static identity
libp2p.Identity(identityKey), libp2p.Identity(identityKey),
// Enable required transports // Enable required transports
libp2p.Transport(tcp.NewTCPTransport), libp2p.Transport(tcp.NewTCPTransport),
libp2p.Transport(ws.New), libp2p.Transport(ws.New),
libp2p.Transport(webtransport.New),
// Other options // Other options
libp2p.ListenAddrs(muAddrs...), libp2p.ListenAddrs(muAddrs...),
libp2p.Security(noise.ID, noise.New), libp2p.Security(noise.ID, noise.New),
@@ -122,7 +91,6 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
libp2p.EnableNATService(), libp2p.EnableNATService(),
libp2p.EnableAutoNATv2(), libp2p.EnableAutoNATv2(),
libp2p.ShareTCPListener(), libp2p.ShareTCPListener(),
libp2p.QUICReuse(quicreuse.NewConnManager),
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create libp2p host for relay: %w", err) return nil, fmt.Errorf("failed to create libp2p host for relay: %w", err)
@@ -137,13 +105,23 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
// Initialize Ping Service // Initialize Ping Service
pingSvc := ping.NewPingService(p2pHost) pingSvc := ping.NewPingService(p2pHost)
var addresses []string
for _, addr := range p2pHost.Addrs() {
addresses = append(addresses, addr.String())
}
r := &Relay{ r := &Relay{
PeerInfo: NewPeerInfo(p2pHost.ID(), p2pHost.Addrs()), RelayInfo: RelayInfo{
ID: p2pHost.ID(),
MeshAddrs: addresses,
MeshRooms: common.NewSafeMap[string, shared.RoomInfo](),
MeshLatencies: common.NewSafeMap[string, time.Duration](),
},
Host: p2pHost, Host: p2pHost,
PubSub: p2pPubsub, PubSub: p2pPubsub,
PingService: pingSvc, PingService: pingSvc,
LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](), LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](),
LocalMeshConnections: common.NewSafeMap[peer.ID, *webrtc.PeerConnection](), LocalMeshPeers: common.NewSafeMap[peer.ID, *RelayInfo](),
} }
// Add network notifier after relay is initialized // Add network notifier after relay is initialized
@@ -174,7 +152,7 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
return r, nil return r, nil
} }
func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) (*Relay, error) { func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
var err error var err error
persistentDir := common.GetFlags().PersistDir persistentDir := common.GetFlags().PersistDir
@@ -186,7 +164,7 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) (*Relay, error
if hasIdentity { if hasIdentity {
_, err = os.Stat(persistentDir + "/identity.key") _, err = os.Stat(persistentDir + "/identity.key")
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to check identity key file: %w", err) return fmt.Errorf("failed to check identity key file: %w", err)
} else if os.IsNotExist(err) { } else if os.IsNotExist(err) {
hasIdentity = false hasIdentity = false
} }
@@ -194,17 +172,17 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) (*Relay, error
if !hasIdentity { if !hasIdentity {
// Make sure the persistent directory exists // Make sure the persistent directory exists
if err = os.MkdirAll(persistentDir, 0700); err != nil { if err = os.MkdirAll(persistentDir, 0700); err != nil {
return nil, fmt.Errorf("failed to create persistent data directory: %w", err) return fmt.Errorf("failed to create persistent data directory: %w", err)
} }
// Generate // Generate
slog.Info("Generating new identity for relay") slog.Info("Generating new identity for relay")
privKey, err = common.GenerateED25519Key() privKey, err = common.GenerateED25519Key()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate new identity: %w", err) return fmt.Errorf("failed to generate new identity: %w", err)
} }
// Save the key // Save the key
if err = common.SaveED25519Key(privKey, persistentDir+"/identity.key"); err != nil { if err = common.SaveED25519Key(privKey, persistentDir+"/identity.key"); err != nil {
return nil, fmt.Errorf("failed to save identity key: %w", err) return fmt.Errorf("failed to save identity key: %w", err)
} }
slog.Info("New identity generated and saved", "path", persistentDir+"/identity.key") slog.Info("New identity generated and saved", "path", persistentDir+"/identity.key")
} else { } else {
@@ -212,45 +190,25 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) (*Relay, error
// Load the key // Load the key
privKey, err = common.LoadED25519Key(persistentDir + "/identity.key") privKey, err = common.LoadED25519Key(persistentDir + "/identity.key")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load identity key: %w", err) return fmt.Errorf("failed to load identity key: %w", err)
} }
} }
// Convert to libp2p crypto.PrivKey // Convert to libp2p crypto.PrivKey
identityKey, err = crypto.UnmarshalEd25519PrivateKey(privKey) identityKey, err = crypto.UnmarshalEd25519PrivateKey(privKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal ED25519 private key: %w", err) return fmt.Errorf("failed to unmarshal ED25519 private key: %w", err)
} }
globalRelay, err = NewRelay(ctx, common.GetFlags().EndpointPort, identityKey) globalRelay, err = NewRelay(ctx, common.GetFlags().EndpointPort, identityKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create relay: %w", err) return fmt.Errorf("failed to create relay: %w", err)
} }
if err = common.InitWebRTCAPI(); err != nil { if err = common.InitWebRTCAPI(); err != nil {
return nil, err return err
} }
slog.Info("Relay initialized", "id", globalRelay.ID) 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
} }

View File

@@ -19,7 +19,7 @@ type discoveryNotifee struct {
func (d *discoveryNotifee) HandlePeerFound(pi peer.AddrInfo) { func (d *discoveryNotifee) HandlePeerFound(pi peer.AddrInfo) {
if d.relay != nil { if d.relay != nil {
if err := d.relay.connectToPeer(context.Background(), &pi); err != nil { if err := d.relay.connectToRelay(context.Background(), &pi); err != nil {
slog.Error("failed to connect to discovered relay", "peer", pi.ID, "error", err) slog.Error("failed to connect to discovered relay", "peer", pi.ID, "error", err)
} }
} }

View File

@@ -46,7 +46,7 @@ func (r *Relay) publishRelayMetrics(ctx context.Context) error {
// Check all peer latencies // Check all peer latencies
r.checkAllPeerLatencies(ctx) r.checkAllPeerLatencies(ctx)
data, err := json.Marshal(r.PeerInfo) data, err := json.Marshal(r.RelayInfo)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal relay status: %w", err) 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 { if result.Error != nil {
slog.Warn("Latency check failed, removing peer from local peers map", "peer", peerID, "err", result.Error) slog.Warn("Latency check failed, removing peer from local peers map", "peer", peerID, "err", result.Error)
// Remove from MeshPeers if ping failed // Remove from MeshPeers if ping failed
if r.Peers.Has(peerID) { if r.LocalMeshPeers.Has(peerID) {
r.Peers.Delete(peerID) r.LocalMeshPeers.Delete(peerID)
} }
return return
} }
@@ -123,6 +123,6 @@ func (r *Relay) measureLatencyToPeer(ctx context.Context, peerID peer.ID) {
latency = 1 * time.Microsecond latency = 1 * time.Microsecond
} }
r.PeerInfo.Latencies.Set(peerID, latency) r.RelayInfo.MeshLatencies.Set(peerID.String(), latency)
} }
} }

View File

@@ -22,7 +22,7 @@ type networkNotifier struct {
// Connected is called when a connection is established // Connected is called when a connection is established
func (n *networkNotifier) Connected(net network.Network, conn network.Conn) { func (n *networkNotifier) Connected(net network.Network, conn network.Conn) {
if n.relay != nil { if n.relay == nil {
n.relay.onPeerConnected(conn.RemotePeer()) n.relay.onPeerConnected(conn.RemotePeer())
} }
} }
@@ -75,8 +75,8 @@ func (r *Relay) setupPubSub(ctx context.Context) error {
// --- Connection Management --- // --- Connection Management ---
// connectToPeer is internal method to connect to a peer using multiaddresses // connectToRelay is internal method to connect to a relay peer using multiaddresses
func (r *Relay) connectToPeer(ctx context.Context, peerInfo *peer.AddrInfo) error { func (r *Relay) connectToRelay(ctx context.Context, peerInfo *peer.AddrInfo) error {
if peerInfo.ID == r.ID { if peerInfo.ID == r.ID {
return errors.New("cannot connect to self") return errors.New("cannot connect to self")
} }
@@ -94,14 +94,19 @@ func (r *Relay) connectToPeer(ctx context.Context, peerInfo *peer.AddrInfo) erro
return nil return nil
} }
// ConnectToPeer connects to another peer by its multiaddress. // ConnectToRelay connects to another relay by its multiaddress.
func (r *Relay) ConnectToPeer(ctx context.Context, addr multiaddr.Multiaddr) error { func (r *Relay) ConnectToRelay(ctx context.Context, addr string) error {
peerInfo, err := peer.AddrInfoFromP2pAddr(addr) ma, err := multiaddr.NewMultiaddr(addr)
if err != nil {
return fmt.Errorf("invalid multiaddress: %w", err)
}
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
if err != nil { if err != nil {
return fmt.Errorf("failed to extract peer info: %w", err) return fmt.Errorf("failed to extract peer info: %w", err)
} }
return r.connectToPeer(ctx, peerInfo) return r.connectToRelay(ctx, peerInfo)
} }
// printConnectInstructions logs the multiaddresses for connecting to this relay. // printConnectInstructions logs the multiaddresses for connecting to this relay.

View File

@@ -1,77 +0,0 @@
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
}

View File

@@ -40,7 +40,7 @@ type StreamConnection struct {
// StreamProtocol deals with meshed stream forwarding // StreamProtocol deals with meshed stream forwarding
type StreamProtocol struct { type StreamProtocol struct {
relay *Relay relay *Relay
servedConns *common.SafeMap[string, *common.SafeMap[peer.ID, *StreamConnection]] // room name -> (peer ID -> StreamConnection) (for served streams) servedConns *common.SafeMap[peer.ID, *StreamConnection] // peer ID -> StreamConnection (for served streams)
incomingConns *common.SafeMap[string, *StreamConnection] // room name -> StreamConnection (for incoming pushed 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) requestedConns *common.SafeMap[string, *StreamConnection] // room name -> StreamConnection (for requested streams from other relays)
} }
@@ -48,7 +48,7 @@ type StreamProtocol struct {
func NewStreamProtocol(relay *Relay) *StreamProtocol { func NewStreamProtocol(relay *Relay) *StreamProtocol {
protocol := &StreamProtocol{ protocol := &StreamProtocol{
relay: relay, relay: relay,
servedConns: common.NewSafeMap[string, *common.SafeMap[peer.ID, *StreamConnection]](), servedConns: common.NewSafeMap[peer.ID, *StreamConnection](),
incomingConns: common.NewSafeMap[string, *StreamConnection](), incomingConns: common.NewSafeMap[string, *StreamConnection](),
requestedConns: common.NewSafeMap[string, *StreamConnection](), requestedConns: common.NewSafeMap[string, *StreamConnection](),
} }
@@ -66,7 +66,6 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
brw := bufio.NewReadWriter(bufio.NewReader(stream), bufio.NewWriter(stream)) brw := bufio.NewReadWriter(bufio.NewReader(stream), bufio.NewWriter(stream))
safeBRW := common.NewSafeBufioRW(brw) safeBRW := common.NewSafeBufioRW(brw)
var currentRoomName string // Track the current room for this stream
iceHolder := make([]webrtc.ICECandidateInit, 0) iceHolder := make([]webrtc.ICECandidateInit, 0)
for { for {
data, err := safeBRW.Receive() data, err := safeBRW.Receive()
@@ -102,9 +101,7 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
continue continue
} }
currentRoomName = roomName // Store the room name
slog.Info("Received stream request for room", "room", roomName) slog.Info("Received stream request for room", "room", roomName)
room := sp.relay.GetRoomByName(roomName) room := sp.relay.GetRoomByName(roomName)
if room == nil || !room.IsOnline() || room.OwnerID != sp.relay.ID { if room == nil || !room.IsOnline() || room.OwnerID != sp.relay.ID {
// TODO: Allow forward requests to other relays from here? // TODO: Allow forward requests to other relays from here?
@@ -129,12 +126,8 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
pc, err := common.CreatePeerConnection(func() { pc, err := common.CreatePeerConnection(func() {
slog.Info("PeerConnection closed for requested stream", "room", roomName) slog.Info("PeerConnection closed for requested stream", "room", roomName)
// Cleanup the stream connection // Cleanup the stream connection
if roomMap, ok := sp.servedConns.Get(roomName); ok { if ok := sp.servedConns.Has(stream.Conn().RemotePeer()); ok {
roomMap.Delete(stream.Conn().RemotePeer()) sp.servedConns.Delete(stream.Conn().RemotePeer())
// If the room map is empty, delete it
if roomMap.Len() == 0 {
sp.servedConns.Delete(roomName)
}
} }
}) })
if err != nil { if err != nil {
@@ -211,12 +204,7 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
} }
// Store the connection // Store the connection
roomMap, ok := sp.servedConns.Get(roomName) sp.servedConns.Set(stream.Conn().RemotePeer(), &StreamConnection{
if !ok {
roomMap = common.NewSafeMap[peer.ID, *StreamConnection]()
sp.servedConns.Set(roomName, roomMap)
}
roomMap.Set(stream.Conn().RemotePeer(), &StreamConnection{
pc: pc, pc: pc,
ndc: ndc, ndc: ndc,
}) })
@@ -228,10 +216,7 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
slog.Error("Failed to unmarshal ICE message", "err", err) slog.Error("Failed to unmarshal ICE message", "err", err)
continue continue
} }
// Use currentRoomName to get the connection from nested map if conn, ok := sp.servedConns.Get(stream.Conn().RemotePeer()); ok && conn.pc.RemoteDescription() != nil {
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 { if err := conn.pc.AddICECandidate(iceMsg.Candidate); err != nil {
slog.Error("Failed to add ICE candidate", "err", err) slog.Error("Failed to add ICE candidate", "err", err)
} }
@@ -246,21 +231,13 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
// Hold the candidate until remote description is set // Hold the candidate until remote description is set
iceHolder = append(iceHolder, iceMsg.Candidate) iceHolder = append(iceHolder, iceMsg.Candidate)
} }
}
} else {
// Hold the candidate until remote description is set
iceHolder = append(iceHolder, iceMsg.Candidate)
}
case "answer": case "answer":
var answerMsg connections.MessageSDP var answerMsg connections.MessageSDP
if err := json.Unmarshal(data, &answerMsg); err != nil { if err := json.Unmarshal(data, &answerMsg); err != nil {
slog.Error("Failed to unmarshal answer from signaling message", "err", err) slog.Error("Failed to unmarshal answer from signaling message", "err", err)
continue continue
} }
// Use currentRoomName to get the connection from nested map if conn, ok := sp.servedConns.Get(stream.Conn().RemotePeer()); ok {
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 { if err := conn.pc.SetRemoteDescription(answerMsg.SDP); err != nil {
slog.Error("Failed to set remote description for answer", "err", err) slog.Error("Failed to set remote description for answer", "err", err)
continue continue
@@ -270,10 +247,6 @@ func (sp *StreamProtocol) handleStreamRequest(stream network.Stream) {
slog.Warn("Received answer without active PeerConnection") slog.Warn("Received answer without active PeerConnection")
} }
} }
} else {
slog.Warn("Received answer without active PeerConnection")
}
}
} }
} }
@@ -479,7 +452,7 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
data, err := safeBRW.Receive() data, err := safeBRW.Receive()
if err != nil { if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, network.ErrReset) { if errors.Is(err, io.EOF) || errors.Is(err, network.ErrReset) {
slog.Debug("Stream push connection closed by peer", "peer", stream.Conn().RemotePeer(), "error", err) slog.Debug("Stream push connection closed by peer", "peer", stream.Conn().RemotePeer())
return return
} }
@@ -595,21 +568,6 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
room.DataChannel.RegisterOnClose(func() { room.DataChannel.RegisterOnClose(func() {
slog.Debug("DataChannel closed for pushed stream", "room", room.Name) 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 // Set the DataChannel in the incomingConns map
if conn, ok := sp.incomingConns.Get(room.Name); ok { if conn, ok := sp.incomingConns.Get(room.Name); ok {
@@ -729,7 +687,7 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
func (sp *StreamProtocol) RequestStream(ctx context.Context, room *shared.Room, peerID peer.ID) error { func (sp *StreamProtocol) RequestStream(ctx context.Context, room *shared.Room, peerID peer.ID) error {
stream, err := sp.relay.Host.NewStream(ctx, peerID, protocolStreamRequest) stream, err := sp.relay.Host.NewStream(ctx, peerID, protocolStreamRequest)
if err != nil { if err != nil {
return fmt.Errorf("failed to create stream: %w", err) return fmt.Errorf("failed to create stream request: %w", err)
} }
return sp.requestStream(stream, room) return sp.requestStream(stream, room)

View File

@@ -57,17 +57,17 @@ func (r *Relay) DeleteRoomIfEmpty(room *shared.Room) {
// GetRemoteRoomByName returns room from mesh by name // GetRemoteRoomByName returns room from mesh by name
func (r *Relay) GetRemoteRoomByName(roomName string) *shared.RoomInfo { func (r *Relay) GetRemoteRoomByName(roomName string) *shared.RoomInfo {
for _, room := range r.Rooms.Copy() { for _, room := range r.MeshRooms.Copy() {
if room.Name == roomName && room.OwnerID != r.ID { if room.Name == roomName && room.OwnerID != r.ID {
// Make sure connection is alive // Make sure connection is alive
if r.Host.Network().Connectedness(room.OwnerID) == network.Connected { if r.Host.Network().Connectedness(room.OwnerID) == network.Connected {
return &room return &room
} } else {
slog.Debug("Removing stale peer, owns a room without connection", "room", roomName, "peer", room.OwnerID) slog.Debug("Removing stale peer, owns a room without connection", "room", roomName, "peer", room.OwnerID)
r.onPeerDisconnected(room.OwnerID) r.onPeerDisconnected(room.OwnerID)
} }
} }
}
return nil return nil
} }

View File

@@ -72,8 +72,8 @@ func (r *Relay) handleRelayMetricsMessages(ctx context.Context, sub *pubsub.Subs
continue continue
} }
var info PeerInfo var info RelayInfo
if err = json.Unmarshal(msg.Data, &info); err != nil { 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) slog.Error("Failed to unmarshal relay status", "from", msg.GetFrom(), "data_len", len(msg.Data), "err", err)
continue continue
} }
@@ -89,7 +89,7 @@ func (r *Relay) handleRelayMetricsMessages(ctx context.Context, sub *pubsub.Subs
// --- State Check Functions --- // --- State Check Functions ---
// hasConnectedPeer checks if peer is in map and has a valid connection // hasConnectedPeer checks if peer is in map and has a valid connection
func (r *Relay) hasConnectedPeer(peerID peer.ID) bool { func (r *Relay) hasConnectedPeer(peerID peer.ID) bool {
if _, ok := r.Peers.Get(peerID); !ok { if _, ok := r.LocalMeshPeers.Get(peerID); !ok {
return false return false
} }
if r.Host.Network().Connectedness(peerID) != network.Connected { if r.Host.Network().Connectedness(peerID) != network.Connected {
@@ -102,14 +102,14 @@ func (r *Relay) hasConnectedPeer(peerID peer.ID) bool {
// --- State Change Functions --- // --- State Change Functions ---
// onPeerStatus updates the status of a peer based on received metrics, adding local perspective // onPeerStatus updates the status of a peer based on received metrics, adding local perspective
func (r *Relay) onPeerStatus(recvInfo PeerInfo) { func (r *Relay) onPeerStatus(recvInfo RelayInfo) {
r.Peers.Set(recvInfo.ID, &recvInfo) r.LocalMeshPeers.Set(recvInfo.ID, &recvInfo)
} }
// onPeerConnected is called when a new peer connects to the relay // onPeerConnected is called when a new peer connects to the relay
func (r *Relay) onPeerConnected(peerID peer.ID) { func (r *Relay) onPeerConnected(peerID peer.ID) {
// Add to local peer map // Add to local peer map
r.Peers.Set(peerID, &PeerInfo{ r.LocalMeshPeers.Set(peerID, &RelayInfo{
ID: peerID, ID: peerID,
}) })
@@ -131,12 +131,16 @@ func (r *Relay) onPeerConnected(peerID peer.ID) {
func (r *Relay) onPeerDisconnected(peerID peer.ID) { func (r *Relay) onPeerDisconnected(peerID peer.ID) {
slog.Info("Mesh peer disconnected, deleting from local peer map", "peer", peerID) slog.Info("Mesh peer disconnected, deleting from local peer map", "peer", peerID)
// Remove peer from local mesh peers // Remove peer from local mesh peers
if r.Peers.Has(peerID) { if r.LocalMeshPeers.Has(peerID) {
r.Peers.Delete(peerID) r.LocalMeshPeers.Delete(peerID)
} }
// Remove any rooms associated with this peer // Remove any rooms associated with this peer
if r.Rooms.Has(peerID.String()) { if r.MeshRooms.Has(peerID.String()) {
r.Rooms.Delete(peerID.String()) r.MeshRooms.Delete(peerID.String())
}
// Remove any latencies associated with this peer
if r.LocalMeshPeers.Has(peerID) {
r.LocalMeshPeers.Delete(peerID)
} }
// TODO: If any rooms were routed through this peer, handle that case // TODO: If any rooms were routed through this peer, handle that case
@@ -151,7 +155,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 // If previously did not exist, but does now, request a connection if participants exist for our room
existed := r.Rooms.Has(state.ID.String()) existed := r.MeshRooms.Has(state.ID.String())
if !existed { if !existed {
// Request connection to this peer if we have participants in our local room // Request connection to this peer if we have participants in our local room
if room, ok := r.LocalRooms.Get(state.ID); ok { if room, ok := r.LocalRooms.Get(state.ID); ok {
@@ -164,6 +168,6 @@ func (r *Relay) updateMeshRoomStates(peerID peer.ID, states []shared.RoomInfo) {
} }
} }
r.Rooms.Set(state.ID.String(), state) r.MeshRooms.Set(state.ID.String(), state)
} }
} }

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.10 // protoc-gen-go v1.36.6
// protoc (unknown) // protoc (unknown)
// source: latency_tracker.proto // source: latency_tracker.proto

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.10 // protoc-gen-go v1.36.6
// protoc (unknown) // protoc (unknown)
// source: messages.proto // source: messages.proto

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.10 // protoc-gen-go v1.36.6
// protoc (unknown) // protoc (unknown)
// source: types.proto // source: types.proto
@@ -416,481 +416,6 @@ func (x *ProtoKeyUp) GetKey() int32 {
return 0 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 // Union of all Input types
type ProtoInput struct { type ProtoInput struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@@ -903,13 +428,6 @@ type ProtoInput struct {
// *ProtoInput_MouseKeyUp // *ProtoInput_MouseKeyUp
// *ProtoInput_KeyDown // *ProtoInput_KeyDown
// *ProtoInput_KeyUp // *ProtoInput_KeyUp
// *ProtoInput_ControllerAttach
// *ProtoInput_ControllerDetach
// *ProtoInput_ControllerButton
// *ProtoInput_ControllerTrigger
// *ProtoInput_ControllerStick
// *ProtoInput_ControllerAxis
// *ProtoInput_ControllerRumble
InputType isProtoInput_InputType `protobuf_oneof:"input_type"` InputType isProtoInput_InputType `protobuf_oneof:"input_type"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -917,7 +435,7 @@ type ProtoInput struct {
func (x *ProtoInput) Reset() { func (x *ProtoInput) Reset() {
*x = ProtoInput{} *x = ProtoInput{}
mi := &file_types_proto_msgTypes[14] mi := &file_types_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -929,7 +447,7 @@ func (x *ProtoInput) String() string {
func (*ProtoInput) ProtoMessage() {} func (*ProtoInput) ProtoMessage() {}
func (x *ProtoInput) ProtoReflect() protoreflect.Message { func (x *ProtoInput) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[14] mi := &file_types_proto_msgTypes[7]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -942,7 +460,7 @@ func (x *ProtoInput) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProtoInput.ProtoReflect.Descriptor instead. // Deprecated: Use ProtoInput.ProtoReflect.Descriptor instead.
func (*ProtoInput) Descriptor() ([]byte, []int) { func (*ProtoInput) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{14} return file_types_proto_rawDescGZIP(), []int{7}
} }
func (x *ProtoInput) GetInputType() isProtoInput_InputType { func (x *ProtoInput) GetInputType() isProtoInput_InputType {
@@ -1015,69 +533,6 @@ func (x *ProtoInput) GetKeyUp() *ProtoKeyUp {
return nil 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 { type isProtoInput_InputType interface {
isProtoInput_InputType() isProtoInput_InputType()
} }
@@ -1110,34 +565,6 @@ type ProtoInput_KeyUp struct {
KeyUp *ProtoKeyUp `protobuf:"bytes,7,opt,name=key_up,json=keyUp,proto3,oneof"` 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_MouseMove) isProtoInput_InputType() {}
func (*ProtoInput_MouseMoveAbs) isProtoInput_InputType() {} func (*ProtoInput_MouseMoveAbs) isProtoInput_InputType() {}
@@ -1152,20 +579,6 @@ func (*ProtoInput_KeyDown) isProtoInput_InputType() {}
func (*ProtoInput_KeyUp) 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 var File_types_proto protoreflect.FileDescriptor
const file_types_proto_rawDesc = "" + const file_types_proto_rawDesc = "" +
@@ -1195,41 +608,7 @@ const file_types_proto_rawDesc = "" +
"\n" + "\n" +
"ProtoKeyUp\x12\x12\n" + "ProtoKeyUp\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" +
"\x03key\x18\x02 \x01(\x05R\x03key\"O\n" + "\x03key\x18\x02 \x01(\x05R\x03key\"\xab\x03\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" + "\n" +
"ProtoInput\x126\n" + "ProtoInput\x126\n" +
"\n" + "\n" +
@@ -1241,15 +620,7 @@ const file_types_proto_rawDesc = "" +
"\fmouse_key_up\x18\x05 \x01(\v2\x16.proto.ProtoMouseKeyUpH\x00R\n" + "\fmouse_key_up\x18\x05 \x01(\v2\x16.proto.ProtoMouseKeyUpH\x00R\n" +
"mouseKeyUp\x120\n" + "mouseKeyUp\x120\n" +
"\bkey_down\x18\x06 \x01(\v2\x13.proto.ProtoKeyDownH\x00R\akeyDown\x12*\n" + "\bkey_down\x18\x06 \x01(\v2\x13.proto.ProtoKeyDownH\x00R\akeyDown\x12*\n" +
"\x06key_up\x18\a \x01(\v2\x11.proto.ProtoKeyUpH\x00R\x05keyUp\x12K\n" + "\x06key_up\x18\a \x01(\v2\x11.proto.ProtoKeyUpH\x00R\x05keyUpB\f\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" + "\n" +
"input_typeB\x16Z\x14relay/internal/protob\x06proto3" "input_typeB\x16Z\x14relay/internal/protob\x06proto3"
@@ -1265,7 +636,7 @@ func file_types_proto_rawDescGZIP() []byte {
return file_types_proto_rawDescData return file_types_proto_rawDescData
} }
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_types_proto_goTypes = []any{ var file_types_proto_goTypes = []any{
(*ProtoMouseMove)(nil), // 0: proto.ProtoMouseMove (*ProtoMouseMove)(nil), // 0: proto.ProtoMouseMove
(*ProtoMouseMoveAbs)(nil), // 1: proto.ProtoMouseMoveAbs (*ProtoMouseMoveAbs)(nil), // 1: proto.ProtoMouseMoveAbs
@@ -1274,14 +645,7 @@ var file_types_proto_goTypes = []any{
(*ProtoMouseKeyUp)(nil), // 4: proto.ProtoMouseKeyUp (*ProtoMouseKeyUp)(nil), // 4: proto.ProtoMouseKeyUp
(*ProtoKeyDown)(nil), // 5: proto.ProtoKeyDown (*ProtoKeyDown)(nil), // 5: proto.ProtoKeyDown
(*ProtoKeyUp)(nil), // 6: proto.ProtoKeyUp (*ProtoKeyUp)(nil), // 6: proto.ProtoKeyUp
(*ProtoControllerAttach)(nil), // 7: proto.ProtoControllerAttach (*ProtoInput)(nil), // 7: proto.ProtoInput
(*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{ var file_types_proto_depIdxs = []int32{
0, // 0: proto.ProtoInput.mouse_move:type_name -> proto.ProtoMouseMove 0, // 0: proto.ProtoInput.mouse_move:type_name -> proto.ProtoMouseMove
@@ -1291,18 +655,11 @@ var file_types_proto_depIdxs = []int32{
4, // 4: proto.ProtoInput.mouse_key_up:type_name -> proto.ProtoMouseKeyUp 4, // 4: proto.ProtoInput.mouse_key_up:type_name -> proto.ProtoMouseKeyUp
5, // 5: proto.ProtoInput.key_down:type_name -> proto.ProtoKeyDown 5, // 5: proto.ProtoInput.key_down:type_name -> proto.ProtoKeyDown
6, // 6: proto.ProtoInput.key_up:type_name -> proto.ProtoKeyUp 6, // 6: proto.ProtoInput.key_up:type_name -> proto.ProtoKeyUp
7, // 7: proto.ProtoInput.controller_attach:type_name -> proto.ProtoControllerAttach 7, // [7:7] is the sub-list for method output_type
8, // 8: proto.ProtoInput.controller_detach:type_name -> proto.ProtoControllerDetach 7, // [7:7] is the sub-list for method input_type
9, // 9: proto.ProtoInput.controller_button:type_name -> proto.ProtoControllerButton 7, // [7:7] is the sub-list for extension type_name
10, // 10: proto.ProtoInput.controller_trigger:type_name -> proto.ProtoControllerTrigger 7, // [7:7] is the sub-list for extension extendee
11, // 11: proto.ProtoInput.controller_stick:type_name -> proto.ProtoControllerStick 0, // [0:7] is the sub-list for field type_name
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() } func init() { file_types_proto_init() }
@@ -1310,7 +667,7 @@ func file_types_proto_init() {
if File_types_proto != nil { if File_types_proto != nil {
return return
} }
file_types_proto_msgTypes[14].OneofWrappers = []any{ file_types_proto_msgTypes[7].OneofWrappers = []any{
(*ProtoInput_MouseMove)(nil), (*ProtoInput_MouseMove)(nil),
(*ProtoInput_MouseMoveAbs)(nil), (*ProtoInput_MouseMoveAbs)(nil),
(*ProtoInput_MouseWheel)(nil), (*ProtoInput_MouseWheel)(nil),
@@ -1318,13 +675,6 @@ func file_types_proto_init() {
(*ProtoInput_MouseKeyUp)(nil), (*ProtoInput_MouseKeyUp)(nil),
(*ProtoInput_KeyDown)(nil), (*ProtoInput_KeyDown)(nil),
(*ProtoInput_KeyUp)(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{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@@ -1332,7 +682,7 @@ func file_types_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 15, NumMessages: 8,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@@ -49,12 +49,25 @@ 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) // IsOnline checks if the room is online (has both audio and video tracks)
func (r *Room) IsOnline() bool { func (r *Room) IsOnline() bool {
return r.AudioTrack != nil && r.VideoTrack != nil return r.AudioTrack != nil && r.VideoTrack != nil
} }
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) { func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
//oldOnline := r.IsOnline()
switch trackType { switch trackType {
case webrtc.RTPCodecTypeAudio: case webrtc.RTPCodecTypeAudio:
r.AudioTrack = track r.AudioTrack = track
@@ -63,4 +76,69 @@ func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalS
default: default:
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType) 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
}
*/

View File

@@ -32,7 +32,7 @@ func main() {
slog.SetDefault(logger) slog.SetDefault(logger)
// Start relay // Start relay
relay, err := core.InitRelay(mainCtx, mainStopper) err := core.InitRelay(mainCtx, mainStopper)
if err != nil { if err != nil {
slog.Error("Failed to initialize relay", "err", err) slog.Error("Failed to initialize relay", "err", err)
mainStopper() mainStopper()
@@ -41,10 +41,5 @@ func main() {
// Wait for exit signal // Wait for exit signal
<-mainCtx.Done() <-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)
}
} }

View File

@@ -0,0 +1,10 @@
#!/bin/bash
while true; do
case "$1" in
--*) shift ;;
*) break ;;
esac
done
exec "$@"

View File

@@ -21,13 +21,6 @@ chown_user_directory() {
echo "Error: Failed to change ownership of ${NESTRI_HOME} to ${NESTRI_USER}:${NESTRI_USER}" >&2 echo "Error: Failed to change ownership of ${NESTRI_HOME} to ${NESTRI_USER}:${NESTRI_USER}" >&2
return 1 return 1
fi fi
# Also apply to .cache separately
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 return 0
} }
@@ -55,13 +48,13 @@ setup_namespaceless() {
# Ensures cache directory exists # Ensures cache directory exists
setup_cache() { setup_cache() {
log "Setting up cache directory at $CACHE_DIR..." log "Setting up NVIDIA driver cache directory at $CACHE_DIR..."
mkdir -p "$CACHE_DIR" || { mkdir -p "$CACHE_DIR" || {
log "Warning: Failed to create cache directory, continuing.." log "Warning: Failed to create cache directory, continuing without cache."
return 1 return 1
} }
$ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "$CACHE_DIR" 2>/dev/null || { $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..."
} }
} }
@@ -230,7 +223,7 @@ main() {
# Start by getting the container we are running under # Start by getting the container we are running under
get_container_info || { get_container_info || {
log "Warning: Failed to detect container information" log "Warning: Failed to detect container information."
} }
log_container_info log_container_info
@@ -238,9 +231,6 @@ main() {
ENTCMD_PREFIX="sudo -E" ENTCMD_PREFIX="sudo -E"
fi fi
# Setup cache now
setup_cache
# Configure SSH # Configure SSH
if [ -n "${SSH_ENABLE_PORT+x}" ] && [ "${SSH_ENABLE_PORT:-0}" -ne 0 ] && \ if [ -n "${SSH_ENABLE_PORT+x}" ] && [ "${SSH_ENABLE_PORT:-0}" -ne 0 ] && \
[ -n "${SSH_ALLOWED_KEY+x}" ] && [ -n "${SSH_ALLOWED_KEY}" ]; then [ -n "${SSH_ALLOWED_KEY+x}" ] && [ -n "${SSH_ALLOWED_KEY}" ]; then
@@ -254,14 +244,14 @@ main() {
# Get and detect GPU(s) # Get and detect GPU(s)
get_gpu_info || { get_gpu_info || {
log "Error: Failed to detect GPU information" log "Error: Failed to detect GPU information."
exit 1 exit 1
} }
log_gpu_info log_gpu_info
# Handle NVIDIA GPU # Handle NVIDIA GPU
if [[ -n "${vendor_devices[nvidia]:-}" ]]; then 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 # Determine NVIDIA driver version
local nvidia_driver_version="" local nvidia_driver_version=""
@@ -275,15 +265,16 @@ main() {
log "Error: Failed to determine NVIDIA driver version." log "Error: Failed to determine NVIDIA driver version."
# Check for other GPU vendors before exiting # Check for other GPU vendors before exiting
if [[ -n "${vendor_devices[amd]:-}" || -n "${vendor_devices[intel]:-}" ]]; then 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 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 exit 1
fi fi
else else
log "Detected NVIDIA driver version: $nvidia_driver_version" log "Detected NVIDIA driver version: $nvidia_driver_version"
# Get installer # Set up cache and get installer
setup_cache
local arch=$(uname -m) local arch=$(uname -m)
local filename="NVIDIA-Linux-${arch}-${nvidia_driver_version}.run" local filename="NVIDIA-Linux-${arch}-${nvidia_driver_version}.run"
cd "$NVIDIA_INSTALLER_DIR" || { cd "$NVIDIA_INSTALLER_DIR" || {
@@ -293,9 +284,9 @@ main() {
get_nvidia_installer "$nvidia_driver_version" "$arch" || { get_nvidia_installer "$nvidia_driver_version" "$arch" || {
# Check for other GPU vendors before exiting # Check for other GPU vendors before exiting
if [[ -n "${vendor_devices[amd]:-}" || -n "${vendor_devices[intel]:-}" ]]; then 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 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 exit 1
fi fi
} }
@@ -304,9 +295,9 @@ main() {
install_nvidia_driver "$filename" || { install_nvidia_driver "$filename" || {
# Check for other GPU vendors before exiting # Check for other GPU vendors before exiting
if [[ -n "${vendor_devices[amd]:-}" || -n "${vendor_devices[intel]:-}" ]]; then 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 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 exit 1
fi fi
} }
@@ -314,14 +305,14 @@ main() {
fi fi
# Make sure gamescope has CAP_SYS_NICE capabilities if available # 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 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 || { 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 else
log "Skipping CAP_SYS_NICE for gamescope, capability not available" log "Skipping CAP_SYS_NICE for gamescope, capability not available..."
fi fi
# Handle user directory permissions # Handle user directory permissions
@@ -334,19 +325,6 @@ main() {
setup_namespaceless setup_namespaceless
fi 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 # Switch to nestri runner entrypoint
log "Switching to application startup entrypoint..." log "Switching to application startup entrypoint..."
if [[ ! -f /etc/nestri/entrypoint_nestri.sh ]]; then if [[ ! -f /etc/nestri/entrypoint_nestri.sh ]]; then
@@ -361,6 +339,6 @@ main() {
} }
# Trap signals for clean exit # Trap signals for clean exit
trap 'log "Received termination signal, exiting.."; exit 0' SIGINT SIGTERM trap 'log "Received termination signal, exiting..."; exit 1' SIGINT SIGTERM
main main

View File

@@ -31,9 +31,6 @@ parse_resolution() {
MAX_RETRIES=3 MAX_RETRIES=3
RETRY_COUNT=0 RETRY_COUNT=0
WAYLAND_READY_DELAY=3 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 # Kills process if running
kill_if_running() { kill_if_running() {
@@ -46,49 +43,83 @@ kill_if_running() {
fi 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 # Starts nestri-server
start_nestri_server() { start_nestri_server() {
kill_if_running "${NESTRI_PID:-}" "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" 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 for ((i=1; i<=15; i++)); do
if [[ -e "$WAYLAND_SOCKET" ]]; then if [[ -e "$WAYLAND_SOCKET" ]]; then
log "Wayland display $WAYLAND_SOCKET ready" log "Wayland display 'wayland-1' ready"
sleep "${WAYLAND_READY_DELAY:-3}" sleep "${WAYLAND_READY_DELAY:-3}"
start_compositor start_compositor
return return
@@ -96,7 +127,12 @@ start_nestri_server() {
sleep 1 sleep 1
done done
log "Error: Wayland display $WAYLAND_SOCKET not available" 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
increment_retry "nestri-server" increment_retry "nestri-server"
restart_chain restart_chain
} }
@@ -108,17 +144,15 @@ start_compositor() {
# Set default values only if variables are unset (not empty) # Set default values only if variables are unset (not empty)
if [[ -z "${NESTRI_LAUNCH_CMD+x}" ]]; then if [[ -z "${NESTRI_LAUNCH_CMD+x}" ]]; then
NESTRI_LAUNCH_CMD="dbus-launch steam -tenfoot -cef-force-gpu" NESTRI_LAUNCH_CMD="steam-native -tenfoot -cef-force-gpu"
fi fi
if [[ -z "${NESTRI_LAUNCH_COMPOSITOR+x}" ]]; then 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}" NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland --force-grab-cursor -g -f --rt --mangoapp -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}"
fi fi
# If PRELOAD_SHIM_arch's are set and exist, set LD_PRELOAD for 32/64-bit apps # Start Steam patcher only if Steam command is present and if needed for container runtime
local do_ld_preload=false if [[ -n "${NESTRI_LAUNCH_CMD}" ]] && [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]] && [[ "${container_runtime:-}" != "podman" ]]; then
if [[ -f "$PRELOAD_SHIM_64" ]] || [[ -f "$PRELOAD_SHIM_32" ]]; then start_steam_namespaceless_patcher
do_ld_preload=true
log "Using LD_PRELOAD shim(s)"
fi fi
# Launch compositor if configured # Launch compositor if configured
@@ -129,51 +163,32 @@ start_compositor() {
# Check if this is a gamescope command # Check if this is a gamescope command
if [[ "$compositor_cmd" == *"gamescope"* ]]; then if [[ "$compositor_cmd" == *"gamescope"* ]]; then
is_gamescope=true is_gamescope=true
# Append application command for gamescope if needed
if [[ -n "$NESTRI_LAUNCH_CMD" ]] && [[ "$compositor_cmd" != *" -- "* ]]; then if [[ -n "$NESTRI_LAUNCH_CMD" ]] && [[ "$compositor_cmd" != *" -- "* ]]; then
# If steam in launch command, enable gamescope integration via -e # If steam in launch command, enable gamescope integration via -e
if [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then if [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then
compositor_cmd+=" -e" compositor_cmd+=" -e"
fi fi
# If ld_preload is true, add env with LD_PRELOAD compositor_cmd+=" -- $NESTRI_LAUNCH_CMD"
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 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" log "Starting compositor: $compositor_cmd"
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$compositor_cmd" & WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$compositor_cmd" &
COMPOSITOR_PID=$! COMPOSITOR_PID=$!
# If ld_preload is true, export LD_PRELOAD # Wait for appropriate socket based on compositor type
if $do_ld_preload; then if $is_gamescope; then
export LD_PRELOAD='/usr/$LIB/libvimputti_shim.so' 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..."
fi fi
log "Waiting for compositor socket $COMPOSITOR_SOCKET.."
for ((i=1; i<=15; i++)); do for ((i=1; i<=15; i++)); do
if [[ -e "$COMPOSITOR_SOCKET" ]]; then 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 # Patch resolution with wlr-randr for non-gamescope compositors
if ! $is_gamescope; then if ! $is_gamescope; then
local OUTPUT_NAME local OUTPUT_NAME
@@ -190,8 +205,6 @@ start_compositor() {
WAYLAND_DISPLAY=wayland-0 /bin/bash -c "$NESTRI_LAUNCH_CMD" & WAYLAND_DISPLAY=wayland-0 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
APP_PID=$! APP_PID=$!
fi fi
else
log "Gamescope detected, skipping wlr-randr resolution patch"
fi fi
return return
fi fi
@@ -233,6 +246,8 @@ cleanup() {
kill_if_running "${NESTRI_PID:-}" "nestri-server" kill_if_running "${NESTRI_PID:-}" "nestri-server"
kill_if_running "${COMPOSITOR_PID:-}" "compositor" kill_if_running "${COMPOSITOR_PID:-}" "compositor"
kill_if_running "${APP_PID:-}" "application" 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 exit $exit_code
} }
@@ -257,6 +272,11 @@ main_loop() {
log "application died" log "application died"
increment_retry "application" increment_retry "application"
start_compositor 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 fi
done done
} }
@@ -267,8 +287,9 @@ main() {
log "Warning: Failed to detect container information." log "Warning: Failed to detect container information."
} }
if [[ "$container_runtime" != "apptainer" ]]; then # Ensure DBus session env exists
ENTCMD_PREFIX="sudo -E -u ${NESTRI_USER}" if command -v dbus-launch >/dev/null 2>&1 && [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
eval "$(dbus-launch)"
fi fi
restart_chain restart_chain

View File

@@ -41,15 +41,6 @@ priority=5
nice=-10 nice=-10
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s 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] [program:entrypoint]
command=/etc/nestri/entrypoint.sh command=/etc/nestri/entrypoint.sh
autorestart=false autorestart=false

File diff suppressed because it is too large Load Diff

View File

@@ -11,22 +11,22 @@ path = "src/main.rs"
gstreamer = { version = "0.24", features = ["v1_26"] } gstreamer = { version = "0.24", features = ["v1_26"] }
gstreamer-webrtc = { version = "0.24", features = ["v1_26"] } gstreamer-webrtc = { version = "0.24", features = ["v1_26"] }
gst-plugin-webrtc = { version = "0.14" } gst-plugin-webrtc = { version = "0.14" }
serde = { version = "1.0", features = ["derive"] } serde = {version = "1.0", features = ["derive"] }
tokio = { version = "1.48", features = ["full"] } tokio = { version = "1.45", features = ["full"] }
tokio-stream = { version = "0.1", features = ["full"] } tokio-stream = { version = "0.1", features = ["full"] }
clap = { version = "4.5", features = ["env", "derive"] } clap = { version = "4.5", features = ["env", "derive"] }
serde_json = "1.0" serde_json = "1.0"
webrtc = "0.14" webrtc = "0.13"
regex = "1.11" regex = "1.11"
rand = "0.9" rand = "0.9"
rustls = { version = "0.23", features = ["ring"] } rustls = { version = "0.23", features = ["ring"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
vimputti = "0.1.3"
chrono = "0.4" chrono = "0.4"
prost = "0.14" prost = "0.14"
prost-types = "0.14" prost-types = "0.14"
parking_lot = "0.12" parking_lot = "0.12"
atomic_refcell = "0.1"
byteorder = "1.5" byteorder = "1.5"
libp2p = { version = "0.56", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "websocket", "autonat"] } libp2p = { version = "0.56", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "websocket", "autonat"] }
libp2p-identify = "0.47" libp2p-identify = "0.47"
@@ -39,4 +39,3 @@ libp2p-dns = { version = "0.44", features = ["tokio"] }
libp2p-tcp = { version = "0.44", features = ["tokio"] } libp2p-tcp = { version = "0.44", features = ["tokio"] }
libp2p-websocket = "0.45" libp2p-websocket = "0.45"
dashmap = "6.1" dashmap = "6.1"
anyhow = "1.0"

View File

@@ -1,2 +1,2 @@
[toolchain] [toolchain]
channel = "1.90" channel = "1.89"

View File

@@ -58,14 +58,6 @@ impl Args {
.env("NESTRI_ROOM") .env("NESTRI_ROOM")
.help("Nestri room name/identifier"), .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(
Arg::new("gpu-vendor") Arg::new("gpu-vendor")
.short('g') .short('g')
@@ -212,10 +204,10 @@ impl Args {
.default_value("192"), .default_value("192"),
) )
.arg( .arg(
Arg::new("zero-copy") Arg::new("dma-buf")
.long("zero-copy") .long("dma-buf")
.env("ZERO_COPY") .env("DMA_BUF")
.help("Use zero-copy pipeline") .help("Use DMA-BUF for pipeline")
.value_parser(BoolishValueParser::new()) .value_parser(BoolishValueParser::new())
.default_value("false"), .default_value("false"),
) )

View File

@@ -12,12 +12,9 @@ pub struct AppArgs {
/// Nestri room name/identifier /// Nestri room name/identifier
pub room: String, pub room: String,
/// vimputti socket path /// Experimental DMA-BUF support
pub vimputti_path: Option<String>,
/// Experimental zero-copy pipeline support
/// TODO: Move to video encoding flags /// TODO: Move to video encoding flags
pub zero_copy: bool, pub dma_buf: bool,
} }
impl AppArgs { impl AppArgs {
pub fn from_matches(matches: &clap::ArgMatches) -> Self { pub fn from_matches(matches: &clap::ArgMatches) -> Self {
@@ -48,13 +45,7 @@ impl AppArgs {
.get_one::<String>("room") .get_one::<String>("room")
.unwrap_or(&rand::random::<u32>().to_string()) .unwrap_or(&rand::random::<u32>().to_string())
.clone(), .clone(),
vimputti_path: matches dma_buf: matches.get_one::<bool>("dma-buf").unwrap_or(&false).clone(),
.get_one::<String>("vimputti-path")
.map(|s| s.clone()),
zero_copy: matches
.get_one::<bool>("zero-copy")
.unwrap_or(&false)
.clone(),
} }
} }
@@ -69,10 +60,6 @@ impl AppArgs {
tracing::info!("> framerate: {}", self.framerate); tracing::info!("> framerate: {}", self.framerate);
tracing::info!("> relay_url: '{}'", self.relay_url); tracing::info!("> relay_url: '{}'", self.relay_url);
tracing::info!("> room: '{}'", self.room); tracing::info!("> room: '{}'", self.room);
tracing::info!( tracing::info!("> dma_buf: {}", self.dma_buf);
"> vimputti_path: '{}'",
self.vimputti_path.as_ref().map_or("None", |s| s.as_str())
);
tracing::info!("> zero_copy: {}", self.zero_copy);
} }
} }

View File

@@ -11,10 +11,18 @@ pub struct DeviceArgs {
impl DeviceArgs { impl DeviceArgs {
pub fn from_matches(matches: &clap::ArgMatches) -> Self { pub fn from_matches(matches: &clap::ArgMatches) -> Self {
Self { Self {
gpu_vendor: matches.get_one::<String>("gpu-vendor").cloned(), gpu_vendor: matches
gpu_name: matches.get_one::<String>("gpu-name").cloned(), .get_one::<String>("gpu-vendor")
gpu_index: matches.get_one::<u32>("gpu-index").cloned(), .cloned(),
gpu_card_path: matches.get_one::<String>("gpu-card-path").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(),
} }
} }

View File

@@ -276,8 +276,8 @@ pub fn encoder_low_latency_params(
_rate_control: &RateControl, _rate_control: &RateControl,
framerate: u32, framerate: u32,
) -> VideoEncoderInfo { ) -> VideoEncoderInfo {
// 1 second keyframe interval for fast recovery, is this too taxing? // 2 second GOP size, maybe lower to 1 second for fast recovery, if needed?
let mut encoder_optz = encoder_gop_params(encoder, framerate); let mut encoder_optz = encoder_gop_params(encoder, framerate * 2);
match encoder_optz.encoder_api { match encoder_optz.encoder_api {
EncoderAPI::QSV => { EncoderAPI::QSV => {
@@ -291,7 +291,6 @@ pub fn encoder_low_latency_params(
encoder_optz.set_parameter("multi-pass", "disabled"); encoder_optz.set_parameter("multi-pass", "disabled");
encoder_optz.set_parameter("preset", "p1"); encoder_optz.set_parameter("preset", "p1");
encoder_optz.set_parameter("tune", "ultra-low-latency"); encoder_optz.set_parameter("tune", "ultra-low-latency");
encoder_optz.set_parameter("zerolatency", "true");
} }
EncoderAPI::AMF => { EncoderAPI::AMF => {
encoder_optz.set_parameter("preset", "speed"); encoder_optz.set_parameter("preset", "speed");
@@ -401,20 +400,10 @@ pub fn get_compatible_encoders(gpus: &Vec<GPUInfo>) -> Vec<VideoEncoderInfo> {
} }
None None
} else if element.has_property("cuda-device-id") { } else if element.has_property("cuda-device-id") {
let device_id = match element let device_id =
.property_value("cuda-device-id") match element.property_value("cuda-device-id").get::<i32>() {
.get::<i32>()
{
Ok(v) if v >= 0 => Some(v as usize), Ok(v) if v >= 0 => Some(v as usize),
_ => { _ => None,
// 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 // We'll just treat cuda-device-id as an index
@@ -585,7 +574,7 @@ pub fn get_best_working_encoder(
encoders: &Vec<VideoEncoderInfo>, encoders: &Vec<VideoEncoderInfo>,
codec: &Codec, codec: &Codec,
encoder_type: &EncoderType, encoder_type: &EncoderType,
zero_copy: bool, dma_buf: bool,
) -> Result<VideoEncoderInfo, Box<dyn Error>> { ) -> Result<VideoEncoderInfo, Box<dyn Error>> {
let mut candidates = get_encoders_by_videocodec( let mut candidates = get_encoders_by_videocodec(
encoders, encoders,
@@ -601,7 +590,7 @@ pub fn get_best_working_encoder(
while !candidates.is_empty() { while !candidates.is_empty() {
let best = get_best_compatible_encoder(&candidates, codec, encoder_type)?; let best = get_best_compatible_encoder(&candidates, codec, encoder_type)?;
tracing::info!("Testing encoder: {}", best.name,); tracing::info!("Testing encoder: {}", best.name,);
if test_encoder(&best, zero_copy).is_ok() { if test_encoder(&best, dma_buf).is_ok() {
return Ok(best); return Ok(best);
} else { } else {
// Remove this encoder and try next best // Remove this encoder and try next best
@@ -613,7 +602,7 @@ pub fn get_best_working_encoder(
} }
/// Test if a pipeline with the given encoder can be created and set to Playing /// Test if a pipeline with the given encoder can be created and set to Playing
pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), Box<dyn Error>> { pub fn test_encoder(encoder: &VideoEncoderInfo, dma_buf: bool) -> Result<(), Box<dyn Error>> {
let src = gstreamer::ElementFactory::make("waylanddisplaysrc").build()?; let src = gstreamer::ElementFactory::make("waylanddisplaysrc").build()?;
if let Some(gpu_info) = &encoder.gpu_info { if let Some(gpu_info) = &encoder.gpu_info {
src.set_property_from_str("render-node", gpu_info.render_path()); src.set_property_from_str("render-node", gpu_info.render_path());
@@ -621,16 +610,12 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), B
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?; let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
let caps = gstreamer::Caps::from_str(&format!( let caps = gstreamer::Caps::from_str(&format!(
"{},width=1280,height=720,framerate=30/1{}", "{},width=1280,height=720,framerate=30/1{}",
if zero_copy { if dma_buf {
if encoder.encoder_api == EncoderAPI::NVENC {
"video/x-raw(memory:CUDAMemory)"
} else {
"video/x-raw(memory:DMABuf)" "video/x-raw(memory:DMABuf)"
}
} else { } else {
"video/x-raw" "video/x-raw"
}, },
if zero_copy { "" } else { ",format=RGBx" } if dma_buf { "" } else { ",format=RGBx" }
))?; ))?;
caps_filter.set_property("caps", &caps); caps_filter.set_property("caps", &caps);
@@ -642,14 +627,41 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), B
// Create pipeline and link elements // Create pipeline and link elements
let pipeline = gstreamer::Pipeline::new(); let pipeline = gstreamer::Pipeline::new();
if zero_copy { if dma_buf && encoder.encoder_api == EncoderAPI::NVENC {
if encoder.encoder_api == EncoderAPI::NVENC { // GL upload element
// NVENC zero-copy path let glupload = gstreamer::ElementFactory::make("glupload").build()?;
pipeline.add_many(&[&src, &caps_filter, &enc, &sink])?; // GL color convert element
gstreamer::Element::link_many(&[&src, &caps_filter, &enc, &sink])?; 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()?;
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,
])?;
} else { } else {
// VA-API/QSV zero-copy path
let vapostproc = gstreamer::ElementFactory::make("vapostproc").build()?; let vapostproc = gstreamer::ElementFactory::make("vapostproc").build()?;
// VA caps filter
let va_caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?; let va_caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
let va_caps = gstreamer::Caps::from_str("video/x-raw(memory:VAMemory),format=NV12")?; let va_caps = gstreamer::Caps::from_str("video/x-raw(memory:VAMemory),format=NV12")?;
va_caps_filter.set_property("caps", &va_caps); va_caps_filter.set_property("caps", &va_caps);
@@ -671,18 +683,10 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), B
&sink, &sink,
])?; ])?;
} }
} else {
// Non-zero-copy path for all encoders - needs videoconvert
let videoconvert = gstreamer::ElementFactory::make("videoconvert").build()?;
pipeline.add_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
gstreamer::Element::link_many(&[&src, &caps_filter, &videoconvert, &enc, &sink])?;
}
let bus = pipeline.bus().ok_or("Pipeline has no bus")?; let bus = pipeline.bus().ok_or("Pipeline has no bus")?;
pipeline.set_state(gstreamer::State::Playing)?; let _ = pipeline.set_state(gstreamer::State::Playing);
for msg in bus.iter_timed(gstreamer::ClockTime::from_seconds(2)) {
// Wait for either error or async-done (state change complete)
for msg in bus.iter_timed(gstreamer::ClockTime::from_seconds(10)) {
match msg.view() { match msg.view() {
gstreamer::MessageView::Error(err) => { gstreamer::MessageView::Error(err) => {
let err_msg = format!("Pipeline error: {}", err.error()); let err_msg = format!("Pipeline error: {}", err.error());
@@ -690,17 +694,14 @@ pub fn test_encoder(encoder: &VideoEncoderInfo, zero_copy: bool) -> Result<(), B
let _ = pipeline.set_state(gstreamer::State::Null); let _ = pipeline.set_state(gstreamer::State::Null);
return Err(err_msg.into()); return Err(err_msg.into());
} }
gstreamer::MessageView::AsyncDone(_) => { gstreamer::MessageView::Eos(_) => {
// Pipeline successfully reached PLAYING state tracing::info!("Pipeline EOS received");
tracing::debug!("Pipeline reached PLAYING state successfully");
let _ = pipeline.set_state(gstreamer::State::Null); let _ = pipeline.set_state(gstreamer::State::Null);
return Ok(()); return Err("Pipeline EOS received, encoder test failed".into());
} }
_ => {} _ => {}
} }
} }
// If we got here, timeout occurred without reaching PLAYING or error
let _ = pipeline.set_state(gstreamer::State::Null); let _ = pipeline.set_state(gstreamer::State::Null);
Err("Encoder test timed out".into()) Ok(())
} }

View File

@@ -112,25 +112,11 @@ pub fn get_gpus() -> Result<Vec<GPUInfo>, Box<dyn Error>> {
let minor = &caps[1]; let minor = &caps[1];
// Read vendor and device ID // 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_str = vendor_str.trim_start_matches("0x").trim_end_matches('\n');
let vendor = u16::from_str_radix(vendor_str, 16)?; 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'); let device_str = device_str.trim_start_matches("0x").trim_end_matches('\n');
// Look up in hwdata PCI database // Look up in hwdata PCI database
@@ -143,15 +129,7 @@ pub fn get_gpus() -> Result<Vec<GPUInfo>, Box<dyn Error>> {
}; };
// Read PCI bus ID // 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 let pci_bus_id = pci_bus_id
.lines() .lines()
.find_map(|line| { .find_map(|line| {
@@ -213,6 +191,7 @@ 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)> { fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> {
let entries = fs::read_dir("/sys/bus/pci/devices").ok()?; let entries = fs::read_dir("/sys/bus/pci/devices").ok()?;
for entry in entries.flatten() { for entry in entries.flatten() {
if !entry.path().to_string_lossy().contains(&pci_addr) { if !entry.path().to_string_lossy().contains(&pci_addr) {
continue; continue;

View File

@@ -1 +0,0 @@
pub mod controller;

View File

@@ -1,205 +0,0 @@
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
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
mod args; mod args;
mod enc_helper; mod enc_helper;
mod gpu; mod gpu;
mod input;
mod latency; mod latency;
mod messages; mod messages;
mod nestrisink; mod nestrisink;
@@ -11,7 +10,6 @@ mod proto;
use crate::args::encoding_args; use crate::args::encoding_args;
use crate::enc_helper::{EncoderAPI, EncoderType}; use crate::enc_helper::{EncoderAPI, EncoderType};
use crate::gpu::{GPUInfo, GPUVendor}; use crate::gpu::{GPUInfo, GPUVendor};
use crate::input::controller::ControllerManager;
use crate::nestrisink::NestriSignaller; use crate::nestrisink::NestriSignaller;
use crate::p2p::p2p::NestriP2P; use crate::p2p::p2p::NestriP2P;
use gstreamer::prelude::*; use gstreamer::prelude::*;
@@ -120,7 +118,7 @@ fn handle_encoder_video(
&video_encoders, &video_encoders,
&args.encoding.video.codec, &args.encoding.video.codec,
&args.encoding.video.encoder_type, &args.encoding.video.encoder_type,
args.app.zero_copy, args.app.dma_buf,
)?; )?;
} }
tracing::info!("Selected video encoder: '{}'", video_encoder.name); tracing::info!("Selected video encoder: '{}'", video_encoder.name);
@@ -176,6 +174,9 @@ fn handle_encoder_audio(args: &args::Args) -> String {
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
// Parse command line arguments
let mut args = args::Args::new();
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter( .with_env_filter(
EnvFilter::builder() EnvFilter::builder()
@@ -184,9 +185,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
) )
.init(); .init();
// Parse command line arguments
let mut args = args::Args::new();
if args.app.verbose { if args.app.verbose {
args.debug_print(); args.debug_print();
} }
@@ -201,15 +199,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
gstreamer::init()?; gstreamer::init()?;
let _ = gstrswebrtc::plugin_register_static(); // Might be already registered, so we'll pass.. let _ = gstrswebrtc::plugin_register_static(); // Might be already registered, so we'll pass..
if args.app.zero_copy { if args.app.dma_buf {
if args.encoding.video.encoder_type != EncoderType::HARDWARE { if args.encoding.video.encoder_type != EncoderType::HARDWARE {
tracing::warn!( tracing::warn!("DMA-BUF is only supported with hardware encoders, disabling DMA-BUF..");
"zero-copy is only supported with hardware encoders, disabling zero-copy.." args.app.dma_buf = false;
);
args.app.zero_copy = false;
} else { } else {
tracing::warn!( tracing::warn!(
"zero-copy is experimental, it may or may not improve performance, or even work at all." "DMA-BUF is experimental, it may or may not improve performance, or even work at all."
); );
} }
} }
@@ -242,28 +238,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
let nestri_p2p = Arc::new(NestriP2P::new().await?); let nestri_p2p = Arc::new(NestriP2P::new().await?);
let p2p_conn = nestri_p2p.connect(relay_url).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 ***/ /*** PIPELINE CREATION ***/
// Create the pipeline // Create the pipeline
let pipeline = Arc::new(gstreamer::Pipeline::new()); let pipeline = Arc::new(gstreamer::Pipeline::new());
@@ -292,7 +266,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Required to fix gstreamer opus issue, where quality sounds off (due to wrong sample rate) // 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_capsfilter = gstreamer::ElementFactory::make("capsfilter").build()?;
let audio_caps = gstreamer::Caps::from_str("audio/x-raw,rate=48000,channels=2")?; let audio_caps = gstreamer::Caps::from_str("audio/x-raw,rate=48000,channels=2").unwrap();
audio_capsfilter.set_property("caps", &audio_caps); audio_capsfilter.set_property("caps", &audio_caps);
// Audio Encoder Element // Audio Encoder Element
@@ -328,30 +302,22 @@ async fn main() -> Result<(), Box<dyn Error>> {
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?; let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
let caps = gstreamer::Caps::from_str(&format!( let caps = gstreamer::Caps::from_str(&format!(
"{},width={},height={},framerate={}/1{}", "{},width={},height={},framerate={}/1{}",
if args.app.zero_copy { if args.app.dma_buf {
if video_encoder_info.encoder_api == EncoderAPI::NVENC {
"video/x-raw(memory:CUDAMemory)"
} else {
"video/x-raw(memory:DMABuf)" "video/x-raw(memory:DMABuf)"
}
} else { } else {
"video/x-raw" "video/x-raw"
}, },
args.app.resolution.0, args.app.resolution.0,
args.app.resolution.1, args.app.resolution.1,
args.app.framerate, args.app.framerate,
if args.app.zero_copy { if args.app.dma_buf { "" } else { ",format=RGBx" }
""
} else {
",format=RGBx"
}
))?; ))?;
caps_filter.set_property("caps", &caps); caps_filter.set_property("caps", &caps);
// Get bit-depth and choose appropriate format (NV12 or P010_10LE) // Get bit-depth and choose appropriate format (NV12 or P010_10LE)
// H.264 does not support above 8-bit. Also we require DMA-BUF. // H.264 does not support above 8-bit. Also we require DMA-BUF.
let video_format = if args.encoding.video.bit_depth == 10 let video_format = if args.encoding.video.bit_depth == 10
&& args.app.zero_copy && args.app.dma_buf
&& video_encoder_info.codec != enc_helper::VideoCodec::H264 && video_encoder_info.codec != enc_helper::VideoCodec::H264
{ {
"P010_10LE" "P010_10LE"
@@ -359,6 +325,27 @@ async fn main() -> Result<(), Box<dyn Error>> {
"NV12" "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 // vapostproc for VA compatible encoders
let mut vapostproc = None; let mut vapostproc = None;
let mut va_caps_filter = None; let mut va_caps_filter = None;
@@ -377,7 +364,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Video Converter Element // Video Converter Element
let mut video_converter = None; let mut video_converter = None;
if !args.app.zero_copy { if !args.app.dma_buf {
video_converter = Some(gstreamer::ElementFactory::make("videoconvert").build()?); video_converter = Some(gstreamer::ElementFactory::make("videoconvert").build()?);
} }
@@ -410,34 +397,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
/* Output */ /* Output */
// WebRTC sink Element // WebRTC sink Element
let signaller = NestriSignaller::new( let signaller =
args.app.room, NestriSignaller::new(args.app.room, p2p_conn.clone(), video_source.clone()).await?;
p2p_conn.clone(),
video_source.clone(),
controller_manager,
rumble_rx,
)
.await?;
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone())); 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("stun-server", "stun://stun.l.google.com:19302");
webrtcsink.set_property_from_str("congestion-control", "disabled"); webrtcsink.set_property_from_str("congestion-control", "disabled");
webrtcsink.set_property("do-retransmission", false); webrtcsink.set_property("do-retransmission", false);
/* Queues */ /* Queues */
let video_source_queue = gstreamer::ElementFactory::make("queue") let video_queue = gstreamer::ElementFactory::make("queue2")
.property("max-size-buffers", 5u32) .property("max-size-buffers", 3u32)
.property("max-size-time", 0u64)
.property("max-size-bytes", 0u32)
.build()?; .build()?;
let audio_source_queue = gstreamer::ElementFactory::make("queue") let audio_queue = gstreamer::ElementFactory::make("queue2")
.property("max-size-buffers", 5u32) .property("max-size-buffers", 3u32)
.build()?; .property("max-size-time", 0u64)
.property("max-size-bytes", 0u32)
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()?; .build()?;
/* Clock Sync */ /* Clock Sync */
@@ -456,7 +433,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
&caps_filter, &caps_filter,
&video_queue, &video_queue,
&video_clocksync, &video_clocksync,
&video_source_queue,
&video_source, &video_source,
&audio_encoder, &audio_encoder,
&audio_capsfilter, &audio_capsfilter,
@@ -464,7 +440,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
&audio_clocksync, &audio_clocksync,
&audio_rate, &audio_rate,
&audio_converter, &audio_converter,
&audio_source_queue,
&audio_source, &audio_source,
])?; ])?;
@@ -480,18 +455,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
pipeline.add(parser)?; pipeline.add(parser)?;
} }
// If zero-copy.. // If DMA-BUF..
if args.app.zero_copy { if args.app.dma_buf {
// VA-API / QSV pipeline // VA-API / QSV pipeline
if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) { if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) {
pipeline.add_many(&[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 // Link main audio branch
gstreamer::Element::link_many(&[ gstreamer::Element::link_many(&[
&audio_source, &audio_source,
&audio_source_queue,
&audio_converter, &audio_converter,
&audio_rate, &audio_rate,
&audio_capsfilter, &audio_capsfilter,
@@ -507,13 +488,12 @@ async fn main() -> Result<(), Box<dyn Error>> {
gstreamer::Element::link_many(&[&audio_encoder, webrtcsink.upcast_ref()])?; gstreamer::Element::link_many(&[&audio_encoder, webrtcsink.upcast_ref()])?;
} }
// With zero-copy.. // With DMA-BUF..
if args.app.zero_copy { if args.app.dma_buf {
// VA-API / QSV pipeline // VA-API / QSV pipeline
if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) { if let (Some(vapostproc), Some(va_caps_filter)) = (&vapostproc, &va_caps_filter) {
gstreamer::Element::link_many(&[ gstreamer::Element::link_many(&[
&video_source, &video_source,
&video_source_queue,
&caps_filter, &caps_filter,
&video_queue, &video_queue,
&video_clocksync, &video_clocksync,
@@ -521,19 +501,27 @@ async fn main() -> Result<(), Box<dyn Error>> {
&va_caps_filter, &va_caps_filter,
&video_encoder, &video_encoder,
])?; ])?;
} else if video_encoder_info.encoder_api == EncoderAPI::NVENC { } else {
// NVENC pipeline // NVENC pipeline
if let (Some(glupload), Some(glconvert), Some(gl_caps_filter), Some(cudaupload)) =
(&glupload, &glconvert, &gl_caps_filter, &cudaupload)
{
gstreamer::Element::link_many(&[ gstreamer::Element::link_many(&[
&video_source, &video_source,
&video_source_queue,
&caps_filter, &caps_filter,
&video_queue,
&video_clocksync,
&glupload,
&glconvert,
&gl_caps_filter,
&cudaupload,
&video_encoder, &video_encoder,
])?; ])?;
} }
}
} else { } else {
gstreamer::Element::link_many(&[ gstreamer::Element::link_many(&[
&video_source, &video_source,
&video_source_queue,
&caps_filter, &caps_filter,
&video_queue, &video_queue,
&video_clocksync, &video_clocksync,
@@ -549,8 +537,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
gstreamer::Element::link_many(&[&video_encoder, webrtcsink.upcast_ref()])?; gstreamer::Element::link_many(&[&video_encoder, webrtcsink.upcast_ref()])?;
} }
// Make sure QOS is disabled to avoid latency // Set QOS
video_encoder.set_property("qos", false); video_encoder.set_property("qos", true);
// Optimize latency of pipeline // Optimize latency of pipeline
video_source video_source

View File

@@ -1,4 +1,3 @@
use crate::input::controller::ControllerManager;
use crate::messages::{MessageBase, MessageICE, MessageRaw, MessageSDP}; use crate::messages::{MessageBase, MessageICE, MessageRaw, MessageSDP};
use crate::p2p::p2p::NestriConnection; use crate::p2p::p2p::NestriConnection;
use crate::p2p::p2p_protocol_stream::NestriStreamProtocol; use crate::p2p::p2p_protocol_stream::NestriStreamProtocol;
@@ -6,7 +5,7 @@ use crate::proto::proto::proto_input::InputType::{
KeyDown, KeyUp, MouseKeyDown, MouseKeyUp, MouseMove, MouseMoveAbs, MouseWheel, KeyDown, KeyUp, MouseKeyDown, MouseKeyUp, MouseMove, MouseMoveAbs, MouseWheel,
}; };
use crate::proto::proto::{ProtoInput, ProtoMessageInput}; use crate::proto::proto::{ProtoInput, ProtoMessageInput};
use anyhow::Result; use atomic_refcell::AtomicRefCell;
use glib::subclass::prelude::*; use glib::subclass::prelude::*;
use gstreamer::glib; use gstreamer::glib;
use gstreamer::prelude::*; use gstreamer::prelude::*;
@@ -15,7 +14,6 @@ use gstrswebrtc::signaller::{Signallable, SignallableImpl};
use parking_lot::RwLock as PLRwLock; use parking_lot::RwLock as PLRwLock;
use prost::Message; use prost::Message;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use tokio::sync::{Mutex, mpsc};
use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit; use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit;
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
@@ -23,9 +21,7 @@ pub struct Signaller {
stream_room: PLRwLock<Option<String>>, stream_room: PLRwLock<Option<String>>,
stream_protocol: PLRwLock<Option<Arc<NestriStreamProtocol>>>, stream_protocol: PLRwLock<Option<Arc<NestriStreamProtocol>>>,
wayland_src: PLRwLock<Option<Arc<gstreamer::Element>>>, wayland_src: PLRwLock<Option<Arc<gstreamer::Element>>>,
data_channel: PLRwLock<Option<Arc<gstreamer_webrtc::WebRTCDataChannel>>>, data_channel: AtomicRefCell<Option<gstreamer_webrtc::WebRTCDataChannel>>,
controller_manager: PLRwLock<Option<Arc<ControllerManager>>>,
rumble_rx: Mutex<Option<mpsc::Receiver<(u32, u16, u16, u16)>>>,
} }
impl Default for Signaller { impl Default for Signaller {
fn default() -> Self { fn default() -> Self {
@@ -33,14 +29,15 @@ impl Default for Signaller {
stream_room: PLRwLock::new(None), stream_room: PLRwLock::new(None),
stream_protocol: PLRwLock::new(None), stream_protocol: PLRwLock::new(None),
wayland_src: PLRwLock::new(None), wayland_src: PLRwLock::new(None),
data_channel: PLRwLock::new(None), data_channel: AtomicRefCell::new(None),
controller_manager: PLRwLock::new(None),
rumble_rx: Mutex::new(None),
} }
} }
} }
impl Signaller { impl Signaller {
pub async fn set_nestri_connection(&self, nestri_conn: NestriConnection) -> Result<()> { pub async fn set_nestri_connection(
&self,
nestri_conn: NestriConnection,
) -> Result<(), Box<dyn std::error::Error>> {
let stream_protocol = NestriStreamProtocol::new(nestri_conn).await?; let stream_protocol = NestriStreamProtocol::new(nestri_conn).await?;
*self.stream_protocol.write() = Some(Arc::new(stream_protocol)); *self.stream_protocol.write() = Some(Arc::new(stream_protocol));
Ok(()) Ok(())
@@ -62,29 +59,14 @@ impl Signaller {
self.wayland_src.read().clone() 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) { pub fn set_data_channel(&self, data_channel: gstreamer_webrtc::WebRTCDataChannel) {
*self.data_channel.write() = Some(Arc::new(data_channel)); 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"
),
} }
pub fn get_data_channel(&self) -> Option<Arc<gstreamer_webrtc::WebRTCDataChannel>> {
self.data_channel.read().clone()
} }
/// Helper method to clean things up /// Helper method to clean things up
@@ -97,15 +79,15 @@ impl Signaller {
let self_obj = self.obj().clone(); let self_obj = self.obj().clone();
stream_protocol.register_callback("answer", move |data| { stream_protocol.register_callback("answer", move |data| {
if let Ok(message) = serde_json::from_slice::<MessageSDP>(&data) { if let Ok(message) = serde_json::from_slice::<MessageSDP>(&data) {
let sdp = gst_sdp::SDPMessage::parse_buffer(message.sdp.sdp.as_bytes()) let sdp =
.map_err(|e| anyhow::anyhow!("Invalid SDP in 'answer': {e:?}"))?; gst_sdp::SDPMessage::parse_buffer(message.sdp.sdp.as_bytes()).unwrap();
let answer = WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp); let answer = WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp);
Ok(self_obj.emit_by_name::<()>( self_obj.emit_by_name::<()>(
"session-description", "session-description",
&[&"unique-session-id", &answer], &[&"unique-session-id", &answer],
)) );
} else { } else {
anyhow::bail!("Failed to decode SDP message"); gstreamer::error!(gstreamer::CAT_DEFAULT, "Failed to decode SDP message");
} }
}); });
} }
@@ -116,7 +98,7 @@ impl Signaller {
let candidate = message.candidate; let candidate = message.candidate;
let sdp_m_line_index = candidate.sdp_mline_index.unwrap_or(0) as u32; let sdp_m_line_index = candidate.sdp_mline_index.unwrap_or(0) as u32;
let sdp_mid = candidate.sdp_mid; let sdp_mid = candidate.sdp_mid;
Ok(self_obj.emit_by_name::<()>( self_obj.emit_by_name::<()>(
"handle-ice", "handle-ice",
&[ &[
&"unique-session-id", &"unique-session-id",
@@ -124,9 +106,9 @@ impl Signaller {
&sdp_mid, &sdp_mid,
&candidate.candidate, &candidate.candidate,
], ],
)) );
} else { } else {
anyhow::bail!("Failed to decode ICE message"); gstreamer::error!(gstreamer::CAT_DEFAULT, "Failed to decode ICE message");
} }
}); });
} }
@@ -149,16 +131,16 @@ impl Signaller {
} }
// Send our SDP offer // Send our SDP offer
Ok(self_obj.emit_by_name::<()>( self_obj.emit_by_name::<()>(
"session-requested", "session-requested",
&[ &[
&"unique-session-id", &"unique-session-id",
&"consumer-identifier", &"consumer-identifier",
&None::<WebRTCSessionDescription>, &None::<WebRTCSessionDescription>,
], ],
)) );
} else { } else {
anyhow::bail!("Failed to decode answer"); gstreamer::error!(gstreamer::CAT_DEFAULT, "Failed to decode answer");
} }
}); });
} }
@@ -191,25 +173,8 @@ impl Signaller {
if let Some(data_channel) = data_channel { if let Some(data_channel) = data_channel {
gstreamer::info!(gstreamer::CAT_DEFAULT, "Data channel created"); gstreamer::info!(gstreamer::CAT_DEFAULT, "Data channel created");
if let Some(wayland_src) = signaller.imp().get_wayland_src() { if let Some(wayland_src) = signaller.imp().get_wayland_src() {
signaller.imp().set_data_channel(data_channel.clone()); setup_data_channel(&data_channel, &*wayland_src);
signaller.imp().set_data_channel(data_channel);
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 { } else {
gstreamer::error!( gstreamer::error!(
gstreamer::CAT_DEFAULT, gstreamer::CAT_DEFAULT,
@@ -350,81 +315,29 @@ impl ObjectImpl for Signaller {
} }
fn setup_data_channel( fn setup_data_channel(
controller_manager: Option<Arc<ControllerManager>>, data_channel: &gstreamer_webrtc::WebRTCDataChannel,
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16)>>, // (slot, strong, weak, duration_ms)
data_channel: Arc<gstreamer_webrtc::WebRTCDataChannel>,
wayland_src: &gstreamer::Element, wayland_src: &gstreamer::Element,
) { ) {
let wayland_src = wayland_src.clone(); let wayland_src = wayland_src.clone();
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
// Spawn async processor
tokio::spawn(async move {
while let Some(data) = rx.recv().await {
match ProtoMessageInput::decode(data.as_slice()) {
Ok(message_input) => {
if let Some(message_base) = message_input.message_base {
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;
}
}
}
}
}
Err(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| { data_channel.connect_on_message_data(move |_data_channel, data| {
if let Some(data) = data { if let Some(data) = data {
let _ = tx.send(data.to_vec()); match ProtoMessageInput::decode(data.to_vec().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);
}
} else {
tracing::error!("Failed to parse InputMessage");
}
}
Err(e) => {
tracing::error!("Failed to decode MessageInput: {:?}", e);
}
}
} }
}); });
} }
@@ -488,7 +401,6 @@ fn handle_input_message(input_msg: ProtoInput) -> Option<gstreamer::Event> {
Some(gstreamer::event::CustomUpstream::new(structure)) Some(gstreamer::event::CustomUpstream::new(structure))
} }
_ => None,
} }
} else { } else {
None None

View File

@@ -1,10 +1,8 @@
use crate::input::controller::ControllerManager;
use crate::p2p::p2p::NestriConnection; use crate::p2p::p2p::NestriConnection;
use gstreamer::glib; use gstreamer::glib;
use gstreamer::subclass::prelude::*; use gstreamer::subclass::prelude::*;
use gstrswebrtc::signaller::Signallable; use gstrswebrtc::signaller::Signallable;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc;
mod imp; mod imp;
@@ -17,19 +15,11 @@ impl NestriSignaller {
room: String, room: String,
nestri_conn: NestriConnection, nestri_conn: NestriConnection,
wayland_src: Arc<gstreamer::Element>, 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>> { ) -> Result<Self, Box<dyn std::error::Error>> {
let obj: Self = glib::Object::new(); let obj: Self = glib::Object::new();
obj.imp().set_stream_room(room); obj.imp().set_stream_room(room);
obj.imp().set_nestri_connection(nestri_conn).await?; obj.imp().set_nestri_connection(nestri_conn).await?;
obj.imp().set_wayland_src(wayland_src); 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) Ok(obj)
} }
} }

View File

@@ -1,4 +1,3 @@
use anyhow::Result;
use libp2p::futures::StreamExt; use libp2p::futures::StreamExt;
use libp2p::multiaddr::Protocol; use libp2p::multiaddr::Protocol;
use libp2p::{ use libp2p::{
@@ -12,6 +11,7 @@ use libp2p_ping as ping;
use libp2p_stream as stream; use libp2p_stream as stream;
use libp2p_tcp as tcp; use libp2p_tcp as tcp;
use libp2p_yamux as yamux; use libp2p_yamux as yamux;
use std::error::Error;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -46,7 +46,7 @@ pub struct NestriP2P {
swarm: Arc<Mutex<Swarm<NestriBehaviour>>>, swarm: Arc<Mutex<Swarm<NestriBehaviour>>>,
} }
impl NestriP2P { impl NestriP2P {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self, Box<dyn Error>> {
let swarm = Arc::new(Mutex::new( let swarm = Arc::new(Mutex::new(
libp2p::SwarmBuilder::with_new_identity() libp2p::SwarmBuilder::with_new_identity()
.with_tokio() .with_tokio()
@@ -69,16 +69,14 @@ impl NestriP2P {
Ok(NestriP2P { swarm }) Ok(NestriP2P { swarm })
} }
pub async fn connect(&self, conn_url: &str) -> Result<NestriConnection> { pub async fn connect(&self, conn_url: &str) -> Result<NestriConnection, Box<dyn Error>> {
let conn_addr: Multiaddr = conn_url.parse()?; let conn_addr: Multiaddr = conn_url.parse()?;
let mut swarm_lock = self.swarm.lock().await; let mut swarm_lock = self.swarm.lock().await;
swarm_lock.dial(conn_addr.clone())?; swarm_lock.dial(conn_addr.clone())?;
let Some(Protocol::P2p(peer_id)) = conn_addr.clone().iter().last() else { let Some(Protocol::P2p(peer_id)) = conn_addr.clone().iter().last() else {
return Err(anyhow::Error::msg( return Err("Invalid connection URL: missing peer ID".into());
"Invalid multiaddr: missing /p2p/<peer_id>",
));
}; };
Ok(NestriConnection { Ok(NestriConnection {
@@ -90,7 +88,10 @@ impl NestriP2P {
async fn swarm_loop(swarm: Arc<Mutex<Swarm<NestriBehaviour>>>) { async fn swarm_loop(swarm: Arc<Mutex<Swarm<NestriBehaviour>>>) {
loop { loop {
let event = swarm.lock().await.select_next_some().await; let event = {
let mut swarm_lock = swarm.lock().await;
swarm_lock.select_next_some().await
};
match event { match event {
/* Ping Events */ /* Ping Events */
SwarmEvent::Behaviour(NestriBehaviourEvent::Ping(ping::Event { SwarmEvent::Behaviour(NestriBehaviourEvent::Ping(ping::Event {

View File

@@ -1,23 +1,23 @@
use crate::p2p::p2p::NestriConnection; use crate::p2p::p2p::NestriConnection;
use crate::p2p::p2p_safestream::SafeStream; use crate::p2p::p2p_safestream::SafeStream;
use anyhow::Result;
use dashmap::DashMap; use dashmap::DashMap;
use libp2p::StreamProtocol; use libp2p::StreamProtocol;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::time::{self, Duration};
// Cloneable callback type // Cloneable callback type
pub type CallbackInner = dyn Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static; pub type CallbackInner = dyn Fn(Vec<u8>) + Send + Sync + 'static;
pub struct Callback(Arc<CallbackInner>); pub struct Callback(Arc<CallbackInner>);
impl Callback { impl Callback {
pub fn new<F>(f: F) -> Self pub fn new<F>(f: F) -> Self
where where
F: Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static, F: Fn(Vec<u8>) + Send + Sync + 'static,
{ {
Callback(Arc::new(f)) Callback(Arc::new(f))
} }
pub fn call(&self, data: Vec<u8>) -> Result<()> { pub fn call(&self, data: Vec<u8>) {
self.0(data) self.0(data)
} }
} }
@@ -44,7 +44,9 @@ impl NestriStreamProtocol {
const NESTRI_PROTOCOL_STREAM_PUSH: StreamProtocol = const NESTRI_PROTOCOL_STREAM_PUSH: StreamProtocol =
StreamProtocol::new("/nestri-relay/stream-push/1.0.0"); StreamProtocol::new("/nestri-relay/stream-push/1.0.0");
pub async fn new(nestri_connection: NestriConnection) -> Result<Self> { pub async fn new(
nestri_connection: NestriConnection,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut nestri_connection = nestri_connection.clone(); let mut nestri_connection = nestri_connection.clone();
let push_stream = match nestri_connection let push_stream = match nestri_connection
.control .control
@@ -53,10 +55,7 @@ impl NestriStreamProtocol {
{ {
Ok(stream) => stream, Ok(stream) => stream,
Err(e) => { Err(e) => {
return Err(anyhow::Error::msg(format!( return Err(Box::new(e));
"Failed to open push stream: {}",
e
)));
} }
}; };
@@ -74,7 +73,7 @@ impl NestriStreamProtocol {
Ok(sp) Ok(sp)
} }
pub fn restart(&mut self) -> Result<()> { pub fn restart(&mut self) -> Result<(), Box<dyn std::error::Error>> {
// Return if tx and handles are already initialized // Return if tx and handles are already initialized
if self.tx.is_some() && self.read_handle.is_some() && self.write_handle.is_some() { if self.tx.is_some() && self.read_handle.is_some() && self.write_handle.is_some() {
tracing::warn!("NestriStreamProtocol is already running, restart skipped"); tracing::warn!("NestriStreamProtocol is already running, restart skipped");
@@ -112,9 +111,13 @@ impl NestriStreamProtocol {
// we just get the callback directly if it exists // we just get the callback directly if it exists
if let Some(callback) = callbacks.get(&response_type) { if let Some(callback) = callbacks.get(&response_type) {
// Execute the callback // Execute the callback
if let Err(e) = callback.call(data.clone()) { if let Err(e) =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
callback.call(data.clone())
}))
{
tracing::error!( tracing::error!(
"Callback for response type '{}' errored: {:?}", "Callback for response type '{}' panicked: {:?}",
response_type, response_type,
e e
); );
@@ -130,6 +133,9 @@ impl NestriStreamProtocol {
tracing::error!("Failed to decode message: {}", e); tracing::error!("Failed to decode message: {}", e);
} }
} }
// Add a small sleep to reduce CPU usage
time::sleep(Duration::from_micros(100)).await;
} }
}) })
} }
@@ -150,20 +156,27 @@ impl NestriStreamProtocol {
break; 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<()> { pub fn send_message<M: serde::Serialize>(
&self,
message: &M,
) -> Result<(), Box<dyn std::error::Error>> {
let json_data = serde_json::to_vec(message)?; let json_data = serde_json::to_vec(message)?;
let Some(tx) = &self.tx else { let Some(tx) = &self.tx else {
return Err(anyhow::Error::msg( return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotConnected,
if self.read_handle.is_none() && self.write_handle.is_none() { if self.read_handle.is_none() && self.write_handle.is_none() {
"NestriStreamProtocol has been shutdown" "NestriStreamProtocol has been shutdown"
} else { } else {
"NestriStreamProtocol is not properly initialized" "NestriStreamProtocol is not properly initialized"
}, },
)); )));
}; };
tx.try_send(json_data)?; tx.try_send(json_data)?;
Ok(()) Ok(())
@@ -171,7 +184,7 @@ impl NestriStreamProtocol {
pub fn register_callback<F>(&self, response_type: &str, callback: F) pub fn register_callback<F>(&self, response_type: &str, callback: F)
where where
F: Fn(Vec<u8>) -> Result<()> + Send + Sync + 'static, F: Fn(Vec<u8>) + Send + Sync + 'static,
{ {
self.callbacks self.callbacks
.insert(response_type.to_string(), Callback::new(callback)); .insert(response_type.to_string(), Callback::new(callback));

View File

@@ -1,4 +1,3 @@
use anyhow::Result;
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use libp2p::futures::io::{ReadHalf, WriteHalf}; use libp2p::futures::io::{ReadHalf, WriteHalf};
use libp2p::futures::{AsyncReadExt, AsyncWriteExt}; use libp2p::futures::{AsyncReadExt, AsyncWriteExt};
@@ -20,17 +19,17 @@ impl SafeStream {
} }
} }
pub async fn send_raw(&self, data: &[u8]) -> Result<()> { pub async fn send_raw(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
self.send_with_length_prefix(data).await self.send_with_length_prefix(data).await
} }
pub async fn receive_raw(&self) -> Result<Vec<u8>> { pub async fn receive_raw(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
self.receive_with_length_prefix().await self.receive_with_length_prefix().await
} }
async fn send_with_length_prefix(&self, data: &[u8]) -> Result<()> { async fn send_with_length_prefix(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
if data.len() > MAX_SIZE { if data.len() > MAX_SIZE {
anyhow::bail!("Data exceeds maximum size"); return Err("Data exceeds maximum size".into());
} }
let mut buffer = Vec::with_capacity(4 + data.len()); let mut buffer = Vec::with_capacity(4 + data.len());
@@ -43,7 +42,7 @@ impl SafeStream {
Ok(()) Ok(())
} }
async fn receive_with_length_prefix(&self) -> Result<Vec<u8>> { async fn receive_with_length_prefix(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut stream_read = self.stream_read.lock().await; let mut stream_read = self.stream_read.lock().await;
// Read length prefix + data in one syscall // Read length prefix + data in one syscall
@@ -52,7 +51,7 @@ impl SafeStream {
let length = BigEndian::read_u32(&length_prefix) as usize; let length = BigEndian::read_u32(&length_prefix) as usize;
if length > MAX_SIZE { if length > MAX_SIZE {
anyhow::bail!("Received data exceeds maximum size"); return Err("Data exceeds maximum size".into());
} }
let mut buffer = vec![0u8; length]; let mut buffer = vec![0u8; length];

View File

@@ -3,31 +3,29 @@
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoTimestampEntry { pub struct ProtoTimestampEntry {
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub stage: ::prost::alloc::string::String, pub stage: ::prost::alloc::string::String,
#[prost(message, optional, tag="2")] #[prost(message, optional, tag = "2")]
pub time: ::core::option::Option<::prost_types::Timestamp>, pub time: ::core::option::Option<::prost_types::Timestamp>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoLatencyTracker { pub struct ProtoLatencyTracker {
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub sequence_id: ::prost::alloc::string::String, pub sequence_id: ::prost::alloc::string::String,
#[prost(message, repeated, tag="2")] #[prost(message, repeated, tag = "2")]
pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>, pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>,
} }
// Mouse messages
/// MouseMove message /// MouseMove message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseMove { pub struct ProtoMouseMove {
/// Fixed value "MouseMove" /// Fixed value "MouseMove"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag = "3")]
pub y: i32, pub y: i32,
} }
/// MouseMoveAbs message /// MouseMoveAbs message
@@ -35,11 +33,11 @@ pub struct ProtoMouseMove {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseMoveAbs { pub struct ProtoMouseMoveAbs {
/// Fixed value "MouseMoveAbs" /// Fixed value "MouseMoveAbs"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag = "3")]
pub y: i32, pub y: i32,
} }
/// MouseWheel message /// MouseWheel message
@@ -47,11 +45,11 @@ pub struct ProtoMouseMoveAbs {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseWheel { pub struct ProtoMouseWheel {
/// Fixed value "MouseWheel" /// Fixed value "MouseWheel"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag = "3")]
pub y: i32, pub y: i32,
} }
/// MouseKeyDown message /// MouseKeyDown message
@@ -59,9 +57,9 @@ pub struct ProtoMouseWheel {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyDown { pub struct ProtoMouseKeyDown {
/// Fixed value "MouseKeyDown" /// Fixed value "MouseKeyDown"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub key: i32, pub key: i32,
} }
/// MouseKeyUp message /// MouseKeyUp message
@@ -69,21 +67,19 @@ pub struct ProtoMouseKeyDown {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyUp { pub struct ProtoMouseKeyUp {
/// Fixed value "MouseKeyUp" /// Fixed value "MouseKeyUp"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub key: i32, pub key: i32,
} }
// Keyboard messages
/// KeyDown message /// KeyDown message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoKeyDown { pub struct ProtoKeyDown {
/// Fixed value "KeyDown" /// Fixed value "KeyDown"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub key: i32, pub key: i32,
} }
/// KeyUp message /// KeyUp message
@@ -91,185 +87,53 @@ pub struct ProtoKeyDown {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoKeyUp { pub struct ProtoKeyUp {
/// Fixed value "KeyUp" /// Fixed value "KeyUp"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub key: i32, 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 /// Union of all Input types
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoInput { pub struct ProtoInput {
#[prost(oneof="proto_input::InputType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14")] #[prost(oneof = "proto_input::InputType", tags = "1, 2, 3, 4, 5, 6, 7")]
pub input_type: ::core::option::Option<proto_input::InputType>, pub input_type: ::core::option::Option<proto_input::InputType>,
} }
/// Nested message and enum types in `ProtoInput`. /// Nested message and enum types in `ProtoInput`.
pub mod proto_input { pub mod proto_input {
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)] #[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum InputType { pub enum InputType {
#[prost(message, tag="1")] #[prost(message, tag = "1")]
MouseMove(super::ProtoMouseMove), MouseMove(super::ProtoMouseMove),
#[prost(message, tag="2")] #[prost(message, tag = "2")]
MouseMoveAbs(super::ProtoMouseMoveAbs), MouseMoveAbs(super::ProtoMouseMoveAbs),
#[prost(message, tag="3")] #[prost(message, tag = "3")]
MouseWheel(super::ProtoMouseWheel), MouseWheel(super::ProtoMouseWheel),
#[prost(message, tag="4")] #[prost(message, tag = "4")]
MouseKeyDown(super::ProtoMouseKeyDown), MouseKeyDown(super::ProtoMouseKeyDown),
#[prost(message, tag="5")] #[prost(message, tag = "5")]
MouseKeyUp(super::ProtoMouseKeyUp), MouseKeyUp(super::ProtoMouseKeyUp),
#[prost(message, tag="6")] #[prost(message, tag = "6")]
KeyDown(super::ProtoKeyDown), KeyDown(super::ProtoKeyDown),
#[prost(message, tag="7")] #[prost(message, tag = "7")]
KeyUp(super::ProtoKeyUp), 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)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessageBase { pub struct ProtoMessageBase {
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub payload_type: ::prost::alloc::string::String, pub payload_type: ::prost::alloc::string::String,
#[prost(message, optional, tag="2")] #[prost(message, optional, tag = "2")]
pub latency: ::core::option::Option<ProtoLatencyTracker>, pub latency: ::core::option::Option<ProtoLatencyTracker>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessageInput { pub struct ProtoMessageInput {
#[prost(message, optional, tag="1")] #[prost(message, optional, tag = "1")]
pub message_base: ::core::option::Option<ProtoMessageBase>, pub message_base: ::core::option::Option<ProtoMessageBase>,
#[prost(message, optional, tag="2")] #[prost(message, optional, tag = "2")]
pub data: ::core::option::Option<ProtoInput>, pub data: ::core::option::Option<ProtoInput>,
} }
// @@protoc_insertion_point(module) // @@protoc_insertion_point(module)

View File

@@ -4,8 +4,6 @@ option go_package = "relay/internal/proto";
package proto; package proto;
/* Mouse messages */
// MouseMove message // MouseMove message
message ProtoMouseMove { message ProtoMouseMove {
string type = 1; // Fixed value "MouseMove" string type = 1; // Fixed value "MouseMove"
@@ -39,8 +37,6 @@ message ProtoMouseKeyUp {
int32 key = 2; int32 key = 2;
} }
/* Keyboard messages */
// KeyDown message // KeyDown message
message ProtoKeyDown { message ProtoKeyDown {
string type = 1; // Fixed value "KeyDown" string type = 1; // Fixed value "KeyDown"
@@ -53,63 +49,6 @@ message ProtoKeyUp {
int32 key = 2; 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 // Union of all Input types
message ProtoInput { message ProtoInput {
oneof input_type { oneof input_type {
@@ -120,12 +59,5 @@ message ProtoInput {
ProtoMouseKeyUp mouse_key_up = 5; ProtoMouseKeyUp mouse_key_up = 5;
ProtoKeyDown key_down = 6; ProtoKeyDown key_down = 6;
ProtoKeyUp key_up = 7; 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;
} }
} }