From c62a22b552589353c90c7751f9f11a57c3dc980c Mon Sep 17 00:00:00 2001 From: Kristian Ollikainen <14197772+DatCaptainHorse@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:20:05 +0300 Subject: [PATCH] feat: Controller support, performance enchancements, multi-stage images, fixes (#304) ## Description Oops.. another massive PR :smiling_face_with_tear: This PR contains multiple improvements and changes. Firstly, thanks gst-wayland-display's PR [here](https://github.com/games-on-whales/gst-wayland-display/pull/20). NVIDIA path is now way more efficient than before. Secondly, adding controller support was a massive hurdle, requiring me to start another project [vimputti](https://github.com/DatCaptainHorse/vimputti) - which allows simple virtual controller inputs in isolated containers. Well, it's not simple, it includes LD_PRELOAD shims and other craziness, but the library API is simple to use.. Thirdly, split runner image into 3 separate stages, base + build + runtime, should help keep things in check in future, also added GitHub Actions CI builds for v2 to v4 builds (hopefully they pass..). Fourth, replaced the runner's runtime Steam patching with better and simpler bubblewrap patch, massive thanks to `games-on-whales` to figuring it out better! Fifth, relay for once needed some changes, the new changes are still mostly WIP, but I'll deal with them next time I have energy.. I'm spent now. Needed to include these changes as relay needed a minor change to allow rumble events to flow back to client peer. Sixth.. tons of package updates, minor code improvements and the usual. ## Summary by CodeRabbit * **New Features** * End-to-end gamepad/controller support (attach/detach, buttons, sticks, triggers, rumble) with client/server integration and virtual controller plumbing. * Optional Prometheus metrics endpoint and WebTransport support. * Background vimputti manager process added for controller handling. * **Improvements** * Multi-variant container image builds and streamlined runtime images. * Zero-copy video pipeline and encoder improvements for lower latency. * Updated Steam compat mapping and dependency/toolchain refreshes. * **Bug Fixes** * More robust GPU detection, input/fullscreen lifecycle, startup/entrypoint, and container runtime fixes. --------- Co-authored-by: DatCaptainHorse --- .github/CODEOWNERS | 4 +- .github/workflows/docker-bake.hcl | 48 + .github/workflows/runner.yml | 101 +- containerfiles/playsite.Containerfile | 1 - containerfiles/relay.Containerfile | 8 +- containerfiles/runner-base.Containerfile | 13 + containerfiles/runner-builder.Containerfile | 218 +++ containerfiles/runner.Containerfile | 158 +- packages/configs/steam/config.vdf | 2 +- packages/input/package.json | 24 +- packages/input/src/codes.ts | 236 +-- packages/input/src/controller.ts | 509 +++++ packages/input/src/index.ts | 1 + packages/input/src/keyboard.ts | 28 +- packages/input/src/mouse.ts | 15 +- .../input/src/proto/latency_tracker_pb.ts | 6 +- packages/input/src/proto/messages_pb.ts | 6 +- packages/input/src/proto/types_pb.ts | 339 +++- packages/input/src/webrtc-stream.ts | 45 +- .../patches/bubblewrap/bubbleunheck.patch | 18 + packages/play-standalone/package.json | 2 +- .../play-standalone/src/pages/[room].astro | 96 +- packages/relay/.dockerignore | 1 + packages/relay/go.mod | 94 +- packages/relay/go.sum | 226 +-- packages/relay/internal/common/common.go | 22 +- packages/relay/internal/common/flags.go | 10 +- packages/relay/internal/core/core.go | 126 +- packages/relay/internal/core/mdns.go | 2 +- packages/relay/internal/core/metrics.go | 8 +- packages/relay/internal/core/p2p.go | 19 +- packages/relay/internal/core/peer.go | 77 + .../relay/internal/core/protocol_stream.go | 88 +- packages/relay/internal/core/room.go | 8 +- packages/relay/internal/core/state.go | 28 +- .../internal/proto/latency_tracker.pb.go | 2 +- packages/relay/internal/proto/messages.pb.go | 2 +- packages/relay/internal/proto/types.pb.go | 708 ++++++- packages/relay/internal/shared/room.go | 78 - packages/relay/main.go | 9 +- packages/scripts/_v2-entry-point | 10 - packages/scripts/entrypoint.sh | 60 +- packages/scripts/entrypoint_nestri.sh | 179 +- packages/scripts/supervisord.conf | 9 + packages/server/Cargo.lock | 1712 +++++++---------- packages/server/Cargo.toml | 9 +- packages/server/rust-toolchain.toml | 2 +- packages/server/src/args.rs | 16 +- packages/server/src/args/app_args.rs | 21 +- packages/server/src/args/device_args.rs | 16 +- packages/server/src/enc_helper.rs | 143 +- packages/server/src/gpu.rs | 29 +- packages/server/src/input.rs | 1 + packages/server/src/input/controller.rs | 205 ++ packages/server/src/main.rs | 160 +- packages/server/src/nestrisink/imp.rs | 166 +- packages/server/src/nestrisink/mod.rs | 10 + packages/server/src/p2p/p2p.rs | 15 +- .../server/src/p2p/p2p_protocol_stream.rs | 45 +- packages/server/src/p2p/p2p_safestream.rs | 13 +- packages/server/src/proto/proto.rs | 204 +- protobufs/types.proto | 70 +- 62 files changed, 4203 insertions(+), 2278 deletions(-) create mode 100644 .github/workflows/docker-bake.hcl create mode 100644 containerfiles/runner-base.Containerfile create mode 100644 containerfiles/runner-builder.Containerfile create mode 100644 packages/input/src/controller.ts create mode 100644 packages/patches/bubblewrap/bubbleunheck.patch create mode 100644 packages/relay/.dockerignore create mode 100644 packages/relay/internal/core/peer.go delete mode 100755 packages/scripts/_v2-entry-point create mode 100644 packages/server/src/input.rs create mode 100644 packages/server/src/input/controller.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 15696828..ba3d6a01 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,13 +3,13 @@ /apps/ @victorpahuus @AquaWolf /packages/ui/ @wanjohiryan @victorpahuus @AquaWolf -/protobuf/ @AquaWolf +/protobufs/ @AquaWolf @DatCaptainHorse /infra/ @wanjohiryan /packages/core/ @wanjohiryan /packages/functions/ @wanjohiryan -/containers/ @DatCaptainHorse +/containerfiles/ @DatCaptainHorse /packages/server/ @DatCaptainHorse /packages/relay/ @DatCaptainHorse /packages/scripts/ @DatCaptainHorse diff --git a/.github/workflows/docker-bake.hcl b/.github/workflows/docker-bake.hcl new file mode 100644 index 00000000..9cd5212a --- /dev/null +++ b/.github/workflows/docker-bake.hcl @@ -0,0 +1,48 @@ +variable "BASE_IMAGE" { + default = "docker.io/cachyos/cachyos:latest" +} + +group "default" { + targets = ["runner"] +} + +target "runner-base" { + dockerfile = "containerfiles/runner-base.Containerfile" + context = "." + args = { + BASE_IMAGE = "${BASE_IMAGE}" + } + cache-from = ["type=gha,scope=runner-base-pr"] + cache-to = ["type=gha,scope=runner-base-pr,mode=max"] + tags = ["runner-base:latest"] +} + +target "runner-builder" { + dockerfile = "containerfiles/runner-builder.Containerfile" + context = "." + args = { + RUNNER_BASE_IMAGE = "runner-base:latest" + } + cache-from = ["type=gha,scope=runner-builder-pr"] + cache-to = ["type=gha,scope=runner-builder-pr,mode=max"] + tags = ["runner-builder:latest"] + contexts = { + runner-base = "target:runner-base" + } +} + +target "runner" { + dockerfile = "containerfiles/runner.Containerfile" + context = "." + args = { + RUNNER_BASE_IMAGE = "runner-base:latest" + RUNNER_BUILDER_IMAGE = "runner-builder:latest" + } + cache-from = ["type=gha,scope=runner-pr"] + cache-to = ["type=gha,scope=runner-pr,mode=max"] + tags = ["nestri-runner"] + contexts = { + runner-base = "target:runner-base" + runner-builder = "target:runner-builder" + } +} diff --git a/.github/workflows/runner.yml b/.github/workflows/runner.yml index ff6eb02c..89e92145 100644 --- a/.github/workflows/runner.yml +++ b/.github/workflows/runner.yml @@ -1,11 +1,11 @@ #Tabs not spaces, you moron :) -name: Build nestri:runner +name: Build nestri-runner on: pull_request: paths: - - "containerfiles/runner.Containerfile" + - "containerfiles/runner*.Containerfile" - "packages/scripts/**" - "packages/server/**" - ".github/workflows/runner.yml" @@ -14,7 +14,7 @@ on: push: branches: [dev, production] paths: - - "containerfiles/runner.Containerfile" + - "containerfiles/runner*.Containerfile" - ".github/workflows/runner.yml" - "packages/scripts/**" - "packages/server/**" @@ -26,7 +26,6 @@ on: env: REGISTRY: ghcr.io IMAGE_NAME: nestrilabs/nestri - BASE_TAG_PREFIX: runner BASE_IMAGE: docker.io/cachyos/cachyos:latest # This makes our release ci quit prematurely @@ -36,43 +35,46 @@ env: jobs: build-docker-pr: - name: Build image on PR + name: Build images on PR runs-on: ubuntu-latest permissions: contents: read packages: write if: ${{ github.event_name == 'pull_request' }} steps: - - - name: Checkout repo - uses: actions/checkout@v4 - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - - + - name: Set Swap Space uses: pierotofy/set-swap-space@master with: swap-size-gb: 20 - - - name: Build Docker image - uses: docker/build-push-action@v6 + - + name: Build images using bake + uses: docker/bake-action@v6 + env: + BASE_IMAGE: ${{ env.BASE_IMAGE }} with: - file: containerfiles/runner.Containerfile - context: ./ + files: | + ./.github/workflows/docker-bake.hcl + targets: runner push: false load: true - tags: nestri:runner - cache-from: type=gha,mode=max - cache-to: type=gha,mode=max build-and-push-docker: - name: Build and push image + name: Build and push images if: ${{ github.ref == 'refs/heads/production' || github.ref == 'refs/heads/dev' }} runs-on: ubuntu-latest permissions: contents: read packages: write + strategy: + matrix: + variant: + - { suffix: "", base: "docker.io/cachyos/cachyos:latest" } + - { suffix: "-v3", base: "docker.io/cachyos/cachyos-v3:latest" } + - { suffix: "-v4", base: "docker.io/cachyos/cachyos-v4:latest" } steps: - name: Checkout repo @@ -85,21 +87,19 @@ jobs: username: ${{ github.actor }} password: ${{ github.token }} - - name: Extract Container metadata - id: meta + name: Extract runner metadata + id: meta-runner uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.BASE_TAG_PREFIX }} - # - #tag on release, and a nightly build for 'dev' + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner tags: | - type=raw,value=nightly,enable={{is_default_branch}} - type=raw,value={{branch}} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'production') }} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - + type=raw,value=nightly${{ matrix.variant.suffix }},enable={{is_default_branch}} + type=raw,value={{branch}}${{ matrix.variant.suffix }} + type=raw,value=latest${{ matrix.variant.suffix }},enable=${{ github.ref == format('refs/heads/{0}', 'production') }} + type=semver,pattern={{version}}${{ matrix.variant.suffix }} + type=semver,pattern={{major}}.{{minor}}${{ matrix.variant.suffix }} + type=semver,pattern={{major}}${{ matrix.variant.suffix }} + - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - @@ -108,14 +108,41 @@ jobs: with: swap-size-gb: 20 - - name: Build Docker image + name: Build and push runner-base image uses: docker/build-push-action@v6 with: - file: containerfiles/runner.Containerfile + file: containerfiles/runner-base.Containerfile context: ./ push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,mode=max - cache-to: type=gha,mode=max - pull: ${{ github.event_name == 'schedule' }} # Pull base image for scheduled builds + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }} + build-args: | + BASE_IMAGE=${{ matrix.variant.base }} + cache-from: type=gha,scope=runner-base${{ matrix.variant.suffix }},mode=max + cache-to: type=gha,scope=runner-base${{ matrix.variant.suffix }},mode=max + pull: ${{ github.event_name == 'schedule' }} + - + name: Build and push runner-builder image + uses: docker/build-push-action@v6 + with: + file: containerfiles/runner-builder.Containerfile + context: ./ + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest${{ matrix.variant.suffix }} + build-args: | + RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }} + cache-from: type=gha,scope=runner-builder${{ matrix.variant.suffix }},mode=max + cache-to: type=gha,scope=runner-builder${{ matrix.variant.suffix }},mode=max + - + name: Build and push runner image + uses: docker/build-push-action@v6 + with: + file: containerfiles/runner.Containerfile + context: ./ + push: true + tags: ${{ steps.meta-runner.outputs.tags }} + labels: ${{ steps.meta-runner.outputs.labels }} + build-args: | + RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }} + RUNNER_BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest${{ matrix.variant.suffix }} + cache-from: type=gha,scope=runner${{ matrix.variant.suffix }},mode=max + cache-to: type=gha,scope=runner${{ matrix.variant.suffix }},mode=max diff --git a/containerfiles/playsite.Containerfile b/containerfiles/playsite.Containerfile index 18f1afbf..8c3f4bfb 100644 --- a/containerfiles/playsite.Containerfile +++ b/containerfiles/playsite.Containerfile @@ -3,7 +3,6 @@ FROM docker.io/node:24-alpine AS base FROM base AS build WORKDIR /usr/src/app COPY package.json ./ -COPY patches ./patches COPY packages/input ./packages/input COPY packages/play-standalone ./packages/play-standalone RUN cd packages/play-standalone && npm install && npm run build diff --git a/containerfiles/relay.Containerfile b/containerfiles/relay.Containerfile index 2256f110..bc82d95f 100644 --- a/containerfiles/relay.Containerfile +++ b/containerfiles/relay.Containerfile @@ -1,9 +1,9 @@ -FROM docker.io/golang:1.24-alpine AS go-build +FROM docker.io/golang:1.25-alpine AS go-build WORKDIR /builder COPY packages/relay/ /builder/ RUN go build -FROM docker.io/golang:1.24-alpine +FROM docker.io/golang:1.25-alpine COPY --from=go-build /builder/relay /relay/relay WORKDIR /relay @@ -22,8 +22,4 @@ ENV WEBRTC_NAT_IPS="" ENV AUTO_ADD_LOCAL_IP=true ENV PERSIST_DIR="./persist-data" -EXPOSE $ENDPOINT_PORT -EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp -EXPOSE $WEBRTC_UDP_MUX/udp - ENTRYPOINT ["/relay/relay"] \ No newline at end of file diff --git a/containerfiles/runner-base.Containerfile b/containerfiles/runner-base.Containerfile new file mode 100644 index 00000000..43159011 --- /dev/null +++ b/containerfiles/runner-base.Containerfile @@ -0,0 +1,13 @@ +# Container build arguments # +ARG BASE_IMAGE=docker.io/cachyos/cachyos:latest + +#*******************************************# +# Base Stage - Simple with light essentials # +#*******************************************# +FROM ${BASE_IMAGE} AS bases + +# Only lightweight stuff needed by both builder and runtime +RUN --mount=type=cache,target=/var/cache/pacman/pkg \ + pacman -Sy --noconfirm \ + libssh2 curl wget libevdev libc++abi \ + gstreamer gst-plugins-base diff --git a/containerfiles/runner-builder.Containerfile b/containerfiles/runner-builder.Containerfile new file mode 100644 index 00000000..68528fdf --- /dev/null +++ b/containerfiles/runner-builder.Containerfile @@ -0,0 +1,218 @@ +# Container build arguments # +ARG RUNNER_BASE_IMAGE=runner-base:latest + +#**************# +# builder base # +#**************# +FROM ${RUNNER_BASE_IMAGE} AS base-builder + +ENV ARTIFACTS=/artifacts +RUN mkdir -p "${ARTIFACTS}" + +# Environment setup for Rust and Cargo +ENV CARGO_HOME=/usr/local/cargo \ + PATH="${CARGO_HOME}/bin:${PATH}" + +# Install build essentials and caching tools +RUN --mount=type=cache,target=/var/cache/pacman/pkg \ + pacman -Sy --noconfirm rustup git base-devel mold \ + meson pkgconf cmake git gcc make + +# Override various linker with symlink so mold is forcefully used (ld, ld.lld, lld) +RUN ln -sf /usr/bin/mold /usr/bin/ld && \ + ln -sf /usr/bin/mold /usr/bin/ld.lld && \ + ln -sf /usr/bin/mold /usr/bin/lld + +# Install latest Rust using rustup +RUN rustup default stable + +# Install cargo-chef with proper caching +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + cargo install -j $(nproc) cargo-chef --locked + +#*******************************# +# vimputti manager build stages # +#*******************************# +FROM base-builder AS vimputti-manager-deps +WORKDIR /builder + +# Install build dependencies +RUN --mount=type=cache,target=/var/cache/pacman/pkg \ + pacman -Sy --noconfirm lib32-gcc-libs + +# Clone repository +RUN git clone --depth 1 --rev "9e8bfd0217eeab011c5afc368d3ea67a4c239e81" https://github.com/DatCaptainHorse/vimputti.git + +#-------------------------------------------------------------------- +FROM vimputti-manager-deps AS vimputti-manager-planner +WORKDIR /builder/vimputti + +# Prepare recipe for dependency caching +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + cargo chef prepare --recipe-path recipe.json + +#-------------------------------------------------------------------- +FROM vimputti-manager-deps AS vimputti-manager-cached-builder +WORKDIR /builder/vimputti + +COPY --from=vimputti-manager-planner /builder/vimputti/recipe.json . + +# Cache dependencies using cargo-chef +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + cargo chef cook --release --recipe-path recipe.json + +ENV CARGO_TARGET_DIR=/builder/target +COPY --from=vimputti-manager-planner /builder/vimputti/ . + +# Build and install directly to artifacts +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + --mount=type=cache,target=/builder/target \ + cargo build --release --package vimputti-manager && \ + cargo build --release --package vimputti-shim && \ + rustup target add i686-unknown-linux-gnu && \ + cargo build --release --package vimputti-shim --target i686-unknown-linux-gnu && \ + cp "${CARGO_TARGET_DIR}/release/vimputti-manager" "${ARTIFACTS}" && \ + cp "${CARGO_TARGET_DIR}/release/libvimputti_shim.so" "${ARTIFACTS}/libvimputti_shim_64.so" && \ + cp "${CARGO_TARGET_DIR}/i686-unknown-linux-gnu/release/libvimputti_shim.so" "${ARTIFACTS}/libvimputti_shim_32.so" + +#****************************# +# nestri-server build stages # +#****************************# +FROM base-builder AS nestri-server-deps +WORKDIR /builder + +# Install build dependencies +RUN --mount=type=cache,target=/var/cache/pacman/pkg \ + pacman -Sy --noconfirm gst-plugins-good gst-plugin-rswebrtc + +#-------------------------------------------------------------------- +FROM nestri-server-deps AS nestri-server-planner +WORKDIR /builder/nestri + +COPY packages/server/Cargo.toml packages/server/Cargo.lock ./ + +# Prepare recipe for dependency caching +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + cargo chef prepare --recipe-path recipe.json + +#-------------------------------------------------------------------- +FROM nestri-server-deps AS nestri-server-cached-builder +WORKDIR /builder/nestri + +COPY --from=nestri-server-planner /builder/nestri/recipe.json . + +# Cache dependencies using cargo-chef +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + cargo chef cook --release --recipe-path recipe.json + + +ENV CARGO_TARGET_DIR=/builder/target +COPY packages/server/ ./ + +# Build and install directly to artifacts +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + --mount=type=cache,target=/builder/target \ + cargo build --release && \ + cp "${CARGO_TARGET_DIR}/release/nestri-server" "${ARTIFACTS}" + +#**********************************# +# gst-wayland-display build stages # +#**********************************# +FROM base-builder AS gst-wayland-deps +WORKDIR /builder + +# Install build dependencies +RUN --mount=type=cache,target=/var/cache/pacman/pkg \ + pacman -Sy --noconfirm libxkbcommon wayland \ + gst-plugins-good gst-plugins-bad libinput + +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + cargo install cargo-c + +# Grab cudart from NVIDIA.. +RUN wget https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-13.0.96-archive.tar.xz -O cuda_cudart.tar.xz && \ + mkdir cuda_cudart && tar -xf cuda_cudart.tar.xz -C cuda_cudart --strip-components=1 && \ + cp cuda_cudart/lib/libcudart.so cuda_cudart/lib/libcudart.so.* /usr/lib/ && \ + rm -r cuda_cudart && \ + rm cuda_cudart.tar.xz + +# Grab cuda lib from NVIDIA (it's in driver package of all things..) +RUN wget https://developer.download.nvidia.com/compute/cuda/redist/nvidia_driver/linux-x86_64/nvidia_driver-linux-x86_64-580.95.05-archive.tar.xz -O nvidia_driver.tar.xz && \ + mkdir nvidia_driver && tar -xf nvidia_driver.tar.xz -C nvidia_driver --strip-components=1 && \ + cp nvidia_driver/lib/libcuda.so.* /usr/lib/libcuda.so && \ + ln -s /usr/lib/libcuda.so /usr/lib/libcuda.so.1 && \ + rm -r nvidia_driver && \ + rm nvidia_driver.tar.xz + +# Clone repository +RUN git clone --depth 1 --rev "afa853fa03e8403c83bbb3bc0cf39147ad46c266" https://github.com/games-on-whales/gst-wayland-display.git + +#-------------------------------------------------------------------- +FROM gst-wayland-deps AS gst-wayland-planner +WORKDIR /builder/gst-wayland-display + +# Prepare recipe for dependency caching +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + cargo chef prepare --recipe-path recipe.json + +#-------------------------------------------------------------------- +FROM gst-wayland-deps AS gst-wayland-cached-builder +WORKDIR /builder/gst-wayland-display + +COPY --from=gst-wayland-planner /builder/gst-wayland-display/recipe.json . + +# Cache dependencies using cargo-chef +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + cargo chef cook --release --recipe-path recipe.json + + +ENV CARGO_TARGET_DIR=/builder/target + +COPY --from=gst-wayland-planner /builder/gst-wayland-display/ . + +# Build and install directly to artifacts +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + --mount=type=cache,target=/builder/target \ + cargo cinstall --prefix=${ARTIFACTS} --release + +#*********************************# +# Patched bubblewrap build stages # +#*********************************# +FROM base-builder AS bubblewrap-deps +WORKDIR /builder + +# Install build dependencies +RUN --mount=type=cache,target=/var/cache/pacman/pkg \ + pacman -Sy --noconfirm libtool libcap libselinux + +# Copy patch file from host +COPY packages/patches/bubblewrap/ /builder/patches/ + +# Clone repository +RUN git clone --depth 1 --rev "9ca3b05ec787acfb4b17bed37db5719fa777834f" https://github.com/containers/bubblewrap.git && \ + cd bubblewrap && \ + # Apply patch to fix user namespace issue + git apply ../patches/bubbleunheck.patch + +#-------------------------------------------------------------------- +FROM bubblewrap-deps AS bubblewrap-builder +WORKDIR /builder/bubblewrap + +# Build and install directly to artifacts +RUN meson setup build --prefix=${ARTIFACTS} && \ + meson compile -C build && \ + meson install -C build + +#*********************************************# +# Final Export Stage - Collects all artifacts # +#*********************************************# +FROM scratch AS artifacts + +COPY --from=nestri-server-cached-builder /artifacts/nestri-server /artifacts/bin/ +COPY --from=gst-wayland-cached-builder /artifacts/lib/ /artifacts/lib/ +COPY --from=gst-wayland-cached-builder /artifacts/include/ /artifacts/include/ +COPY --from=vimputti-manager-cached-builder /artifacts/vimputti-manager /artifacts/bin/ +COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_64.so /artifacts/lib64/libvimputti_shim.so +COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_32.so /artifacts/lib32/libvimputti_shim.so +COPY --from=gst-wayland-deps /usr/lib/libcuda.so /usr/lib/libcuda.so.* /artifacts/lib/ +COPY --from=bubblewrap-builder /artifacts/bin/bwrap /artifacts/bin/ diff --git a/containerfiles/runner.Containerfile b/containerfiles/runner.Containerfile index f3e9ef5c..7e0de173 100644 --- a/containerfiles/runner.Containerfile +++ b/containerfiles/runner.Containerfile @@ -1,125 +1,13 @@ # Container build arguments # -ARG BASE_IMAGE=docker.io/cachyos/cachyos:latest +ARG RUNNER_BASE_IMAGE=runner-base:latest +ARG RUNNER_BUILDER_IMAGE=runner-builder:latest -#****************************************************************************** -# Base Stage - Updates system packages -#****************************************************************************** -FROM ${BASE_IMAGE} AS base - -RUN --mount=type=cache,target=/var/cache/pacman/pkg \ - pacman --noconfirm -Syu - -#****************************************************************************** -# Base Builder Stage - Prepares core build environment -#****************************************************************************** -FROM base AS base-builder - -# Environment setup for Rust and Cargo -ENV CARGO_HOME=/usr/local/cargo \ - ARTIFACTS=/artifacts \ - PATH="${CARGO_HOME}/bin:${PATH}" \ - RUSTFLAGS="-C link-arg=-fuse-ld=mold" - -# Install build essentials and caching tools -RUN --mount=type=cache,target=/var/cache/pacman/pkg \ - pacman -Sy --noconfirm mold rustup && \ - mkdir -p "${ARTIFACTS}" - -# Install latest Rust using rustup -RUN rustup default stable - -# Install cargo-chef with proper caching -RUN --mount=type=cache,target=${CARGO_HOME}/registry \ - cargo install -j $(nproc) cargo-chef cargo-c --locked - -#****************************************************************************** -# Nestri Server Build Stages -#****************************************************************************** -FROM base-builder AS nestri-server-deps -WORKDIR /builder - -# Install build dependencies -RUN --mount=type=cache,target=/var/cache/pacman/pkg \ - pacman -Sy --noconfirm meson pkgconf cmake git gcc make \ - gstreamer gst-plugins-base gst-plugins-good gst-plugin-rswebrtc - -#-------------------------------------------------------------------- -FROM nestri-server-deps AS nestri-server-planner -WORKDIR /builder/nestri - -COPY packages/server/Cargo.toml packages/server/Cargo.lock ./ - -# Prepare recipe for dependency caching -RUN --mount=type=cache,target=${CARGO_HOME}/registry \ - cargo chef prepare --recipe-path recipe.json - -#-------------------------------------------------------------------- -FROM nestri-server-deps AS nestri-server-cached-builder -WORKDIR /builder/nestri - -COPY --from=nestri-server-planner /builder/nestri/recipe.json . - -# Cache dependencies using cargo-chef -RUN --mount=type=cache,target=${CARGO_HOME}/registry \ - cargo chef cook --release --recipe-path recipe.json - - -ENV CARGO_TARGET_DIR=/builder/target - -COPY packages/server/ ./ - -# Build and install directly to artifacts -RUN --mount=type=cache,target=${CARGO_HOME}/registry \ - --mount=type=cache,target=/builder/target \ - cargo build --release && \ - cp "${CARGO_TARGET_DIR}/release/nestri-server" "${ARTIFACTS}" - -#****************************************************************************** -# GST-Wayland Plugin Build Stages -#****************************************************************************** -FROM base-builder AS gst-wayland-deps -WORKDIR /builder - -# Install build dependencies -RUN --mount=type=cache,target=/var/cache/pacman/pkg \ - pacman -Sy --noconfirm meson pkgconf cmake git gcc make \ - libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput - -# Clone repository -RUN git clone --depth 1 --rev "dfeebb19b48f32207469e166a3955f5d65b5e6c6" https://github.com/games-on-whales/gst-wayland-display.git - -#-------------------------------------------------------------------- -FROM gst-wayland-deps AS gst-wayland-planner -WORKDIR /builder/gst-wayland-display - -# Prepare recipe for dependency caching -RUN --mount=type=cache,target=${CARGO_HOME}/registry \ - cargo chef prepare --recipe-path recipe.json - -#-------------------------------------------------------------------- -FROM gst-wayland-deps AS gst-wayland-cached-builder -WORKDIR /builder/gst-wayland-display - -COPY --from=gst-wayland-planner /builder/gst-wayland-display/recipe.json . - -# Cache dependencies using cargo-chef -RUN --mount=type=cache,target=${CARGO_HOME}/registry \ - cargo chef cook --release --recipe-path recipe.json - - -ENV CARGO_TARGET_DIR=/builder/target - -COPY --from=gst-wayland-planner /builder/gst-wayland-display/ . - -# Build and install directly to artifacts -RUN --mount=type=cache,target=${CARGO_HOME}/registry \ - --mount=type=cache,target=/builder/target \ - cargo cinstall --prefix=${ARTIFACTS} --release - -#****************************************************************************** -# Final Runtime Stage -#****************************************************************************** -FROM base AS runtime +#*********************# +# Final Runtime Stage # +#*********************# +FROM ${RUNNER_BASE_IMAGE} AS runtime +FROM ${RUNNER_BUILDER_IMAGE} AS builder +FROM runtime ### Package Installation ### # Core system components @@ -127,20 +15,17 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \ pacman -Sy --needed --noconfirm \ vulkan-intel lib32-vulkan-intel vpl-gpu-rt \ vulkan-radeon lib32-vulkan-radeon \ - mesa steam-native-runtime proton-cachyos lib32-mesa \ + mesa lib32-mesa \ steam gtk3 lib32-gtk3 \ sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \ - libssh2 curl wget \ pipewire pipewire-pulse pipewire-alsa wireplumber \ - noto-fonts-cjk supervisor jq chwd lshw pacman-contrib \ + noto-fonts-cjk supervisor jq pacman-contrib \ hwdata openssh \ # GStreamer stack - gstreamer gst-plugins-base gst-plugins-good \ + gst-plugins-good \ gst-plugins-bad gst-plugin-pipewire \ gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \ - gst-plugin-va gst-plugin-qsv \ - # lib32 GStreamer stack to fix some games with videos - lib32-gstreamer lib32-gst-plugins-base lib32-gst-plugins-good && \ + gst-plugin-va gst-plugin-qsv && \ # Cleanup paccache -rk1 && \ rm -rf /usr/share/{info,man,doc}/* @@ -153,6 +38,7 @@ ENV NESTRI_USER="nestri" \ NESTRI_LANG=en_US.UTF-8 \ NESTRI_XDG_RUNTIME_DIR=/run/user/1000 \ NESTRI_HOME=/home/nestri \ + NESTRI_VIMPUTTI_PATH=/tmp/vimputti-1000 \ NVIDIA_DRIVER_CAPABILITIES=all RUN mkdir -p "/home/${NESTRI_USER}" && \ @@ -174,29 +60,33 @@ RUN mkdir -p /run/dbus && \ -e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \ /usr/share/wireplumber/wireplumber.conf -### Audio Systems Configs - Latency optimizations + Loopback ### +## Audio Systems Configs - Latency optimizations + Loopback ## RUN mkdir -p /etc/pipewire/pipewire.conf.d && \ mkdir -p /etc/wireplumber/wireplumber.conf.d COPY packages/configs/wireplumber.conf.d/* /etc/wireplumber/wireplumber.conf.d/ COPY packages/configs/pipewire.conf.d/* /etc/pipewire/pipewire.conf.d/ -## Steam Configs - Proton (CachyOS flavor) ## +## Steam Configs - Proton (Experimental flavor) ## RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config" COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/" -### Artifacts and Verification ### -COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/ -COPY --from=gst-wayland-cached-builder /artifacts/lib/ /usr/lib/ -COPY --from=gst-wayland-cached-builder /artifacts/include/ /usr/include/ -RUN which nestri-server && ls -la /usr/lib/ | grep 'gstwaylanddisplay' +### Artifacts from Builder ### +COPY --from=builder /artifacts/bin/nestri-server /usr/bin/ +COPY --from=builder /artifacts/bin/bwrap /usr/bin/ +COPY --from=builder /artifacts/lib/ /usr/lib/ +COPY --from=builder /artifacts/lib32/ /usr/lib32/ +COPY --from=builder /artifacts/lib64/ /usr/lib64/ +COPY --from=builder /artifacts/bin/vimputti-manager /usr/bin/ ### Scripts and Final Configuration ### COPY packages/scripts/ /etc/nestri/ RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \ chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" && \ sed -i 's/^#\(en_US\.UTF-8\)/\1/' /etc/locale.gen && \ + setcap cap_net_admin+ep /usr/bin/vimputti-manager && \ + dbus-uuidgen > /etc/machine-id && \ LANG=en_US.UTF-8 locale-gen # Root for most container engines, nestri-user compatible for apptainer without fakeroot diff --git a/packages/configs/steam/config.vdf b/packages/configs/steam/config.vdf index cfebdb65..47af690d 100644 --- a/packages/configs/steam/config.vdf +++ b/packages/configs/steam/config.vdf @@ -10,7 +10,7 @@ { "0" { - "name" "proton-cachyos" + "name" "proton_experimental" "config" "" "priority" "75" } diff --git a/packages/input/package.json b/packages/input/package.json index 56296335..24dc4b74 100644 --- a/packages/input/package.json +++ b/packages/input/package.json @@ -7,21 +7,23 @@ ".": "./src/index.ts" }, "devDependencies": { - "@bufbuild/buf": "^1.50.0", - "@bufbuild/protoc-gen-es": "^2.2.3" + "@bufbuild/buf": "^1.57.2", + "@bufbuild/protoc-gen-es": "^2.9.0" }, "dependencies": { - "@bufbuild/protobuf": "^2.2.3", - "@chainsafe/libp2p-noise": "^16.1.3", - "@chainsafe/libp2p-yamux": "^7.0.1", - "@libp2p/identify": "^3.0.32", - "@libp2p/interface": "^2.10.2", - "@libp2p/ping": "^2.0.32", - "@libp2p/websockets": "^9.2.13", - "@multiformats/multiaddr": "^12.4.0", + "@bufbuild/protobuf": "^2.9.0", + "@chainsafe/libp2p-noise": "^16.1.4", + "@chainsafe/libp2p-quic": "^1.1.3", + "@chainsafe/libp2p-yamux": "^7.0.4", + "@libp2p/identify": "^3.0.39", + "@libp2p/interface": "^2.11.0", + "@libp2p/ping": "^2.0.37", + "@libp2p/websockets": "^9.2.19", + "@libp2p/webtransport": "^5.0.51", + "@multiformats/multiaddr": "^12.5.1", "it-length-prefixed": "^10.0.1", "it-pipe": "^3.0.1", - "libp2p": "^2.8.8", + "libp2p": "^2.10.0", "uint8arraylist": "^2.4.8", "uint8arrays": "^5.1.0" } diff --git a/packages/input/src/codes.ts b/packages/input/src/codes.ts index c3e5a8d7..2d1cba67 100644 --- a/packages/input/src/codes.ts +++ b/packages/input/src/codes.ts @@ -1,113 +1,133 @@ export const keyCodeToLinuxEventCode: { [key: string]: number } = { - 'KeyA': 30, - 'KeyB': 48, - 'KeyC': 46, - 'KeyD': 32, - 'KeyE': 18, - 'KeyF': 33, - 'KeyG': 34, - 'KeyH': 35, - 'KeyI': 23, - 'KeyJ': 36, - 'KeyK': 37, - 'KeyL': 38, - 'KeyM': 50, - 'KeyN': 49, - 'KeyO': 24, - 'KeyP': 25, - 'KeyQ': 16, - 'KeyR': 19, - 'KeyS': 31, - 'KeyT': 20, - 'KeyU': 22, - 'KeyV': 47, - 'KeyW': 17, - 'KeyX': 45, - 'KeyY': 21, - 'KeyZ': 44, - 'Digit1': 2, - 'Digit2': 3, - 'Digit3': 4, - 'Digit4': 5, - 'Digit5': 6, - 'Digit6': 7, - 'Digit7': 8, - 'Digit8': 9, - 'Digit9': 10, - 'Digit0': 11, - 'Enter': 28, - 'Escape': 1, - 'Backspace': 14, - 'Tab': 15, - 'Space': 57, - 'Minus': 12, - 'Equal': 13, - 'BracketLeft': 26, - 'BracketRight': 27, - 'Backslash': 43, - 'Semicolon': 39, - 'Quote': 40, - 'Backquote': 41, - 'Comma': 51, - 'Period': 52, - 'Slash': 53, - 'CapsLock': 58, - 'F1': 59, - 'F2': 60, - 'F3': 61, - 'F4': 62, - 'F5': 63, - 'F6': 64, - 'F7': 65, - 'F8': 66, - 'F9': 67, - 'F10': 68, - 'F11': 87, - 'F12': 88, - 'Insert': 110, - 'Delete': 111, - 'ArrowUp': 103, - 'ArrowDown': 108, - 'ArrowLeft': 105, - 'ArrowRight': 106, - 'Home': 102, - 'End': 107, - 'PageUp': 104, - 'PageDown': 109, - 'NumLock': 69, - 'ScrollLock': 70, - 'Pause': 119, - 'Numpad0': 82, - 'Numpad1': 79, - 'Numpad2': 80, - 'Numpad3': 81, - 'Numpad4': 75, - 'Numpad5': 76, - 'Numpad6': 77, - 'Numpad7': 71, - 'Numpad8': 72, - 'Numpad9': 73, - 'NumpadDivide': 98, - 'NumpadMultiply': 55, - 'NumpadSubtract': 74, - 'NumpadAdd': 78, - 'NumpadEnter': 96, - 'NumpadDecimal': 83, - 'ControlLeft': 29, - 'ControlRight': 97, - 'ShiftLeft': 42, - 'ShiftRight': 54, - 'AltLeft': 56, - 'AltRight': 100, - //'MetaLeft': 125, // Disabled as will break input - //'MetaRight': 126, // Disabled as will break input - 'ContextMenu': 127, + KeyA: 30, + KeyB: 48, + KeyC: 46, + KeyD: 32, + KeyE: 18, + KeyF: 33, + KeyG: 34, + KeyH: 35, + KeyI: 23, + KeyJ: 36, + KeyK: 37, + KeyL: 38, + KeyM: 50, + KeyN: 49, + KeyO: 24, + KeyP: 25, + KeyQ: 16, + KeyR: 19, + KeyS: 31, + KeyT: 20, + KeyU: 22, + KeyV: 47, + KeyW: 17, + KeyX: 45, + KeyY: 21, + KeyZ: 44, + Digit1: 2, + Digit2: 3, + Digit3: 4, + Digit4: 5, + Digit5: 6, + Digit6: 7, + Digit7: 8, + Digit8: 9, + Digit9: 10, + Digit0: 11, + Enter: 28, + Escape: 1, + Backspace: 14, + Tab: 15, + Space: 57, + Minus: 12, + Equal: 13, + BracketLeft: 26, + BracketRight: 27, + Backslash: 43, + Semicolon: 39, + Quote: 40, + Backquote: 41, + Comma: 51, + Period: 52, + Slash: 53, + CapsLock: 58, + F1: 59, + F2: 60, + F3: 61, + F4: 62, + F5: 63, + F6: 64, + F7: 65, + F8: 66, + F9: 67, + F10: 68, + F11: 87, + F12: 88, + Insert: 110, + Delete: 111, + ArrowUp: 103, + ArrowDown: 108, + ArrowLeft: 105, + ArrowRight: 106, + Home: 102, + End: 107, + PageUp: 104, + PageDown: 109, + NumLock: 69, + ScrollLock: 70, + Pause: 119, + Numpad0: 82, + Numpad1: 79, + Numpad2: 80, + Numpad3: 81, + Numpad4: 75, + Numpad5: 76, + Numpad6: 77, + Numpad7: 71, + Numpad8: 72, + Numpad9: 73, + NumpadDivide: 98, + NumpadMultiply: 55, + NumpadSubtract: 74, + NumpadAdd: 78, + NumpadEnter: 96, + NumpadDecimal: 83, + ControlLeft: 29, + ControlRight: 97, + ShiftLeft: 42, + ShiftRight: 54, + AltLeft: 56, + AltRight: 100, + //'MetaLeft': 125, // Disabled as will break input + //'MetaRight': 126, // Disabled as will break input + ContextMenu: 127, }; export const mouseButtonToLinuxEventCode: { [button: number]: number } = { - 0: 272, - 2: 273, - 1: 274, - 3: 275, - 4: 276 + 0: 272, + 2: 273, + 1: 274, + 3: 275, + 4: 276, +}; + +export const controllerButtonToLinuxEventCode: { [button: number]: number } = { + 0: 0x130, + 1: 0x131, + 2: 0x134, + 3: 0x133, + 4: 0x136, + 5: 0x137, + 6: 0x138, + 7: 0x139, + 8: 0x13a, + 9: 0x13b, + 10: 0x13d, + 11: 0x13e, + 12: 0x220, + 13: 0x221, + 14: 0x222, + 15: 0x223, + 16: 0x13c, }; diff --git a/packages/input/src/controller.ts b/packages/input/src/controller.ts new file mode 100644 index 00000000..8a618498 --- /dev/null +++ b/packages/input/src/controller.ts @@ -0,0 +1,509 @@ +import { controllerButtonToLinuxEventCode } from "./codes"; +import { WebRTCStream } from "./webrtc-stream"; +import { + ProtoMessageBase, + ProtoMessageInput, + ProtoMessageInputSchema, +} from "./proto/messages_pb"; +import { + ProtoInputSchema, + ProtoControllerAttachSchema, + ProtoControllerDetachSchema, + ProtoControllerButtonSchema, + ProtoControllerTriggerSchema, + ProtoControllerAxisSchema, + ProtoControllerStickSchema, + ProtoControllerRumble, +} from "./proto/types_pb"; +import { create, toBinary, fromBinary } from "@bufbuild/protobuf"; + +interface Props { + webrtc: WebRTCStream; + e: GamepadEvent; +} + +interface GamepadState { + buttonState: Map; + 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(), + 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); + } + } +} diff --git a/packages/input/src/index.ts b/packages/input/src/index.ts index 4ead3120..d0c1c511 100644 --- a/packages/input/src/index.ts +++ b/packages/input/src/index.ts @@ -1,3 +1,4 @@ export * from "./keyboard" export * from "./mouse" +export * from "./controller" export * from "./webrtc-stream" \ No newline at end of file diff --git a/packages/input/src/keyboard.ts b/packages/input/src/keyboard.ts index 8613826e..d51a290f 100644 --- a/packages/input/src/keyboard.ts +++ b/packages/input/src/keyboard.ts @@ -9,27 +9,23 @@ import { ProtoInputSchema, ProtoKeyDownSchema, ProtoKeyUpSchema, - ProtoMouseMoveSchema } from "./proto/types_pb"; import {create, toBinary} from "@bufbuild/protobuf"; interface Props { webrtc: WebRTCStream; - canvas: HTMLCanvasElement; } export class Keyboard { protected wrtc: WebRTCStream; - protected canvas: HTMLCanvasElement; protected connected!: boolean; // Store references to event listeners private readonly keydownListener: (e: KeyboardEvent) => void; private readonly keyupListener: (e: KeyboardEvent) => void; - constructor({webrtc, canvas}: Props) { + constructor({webrtc}: Props) { this.wrtc = webrtc; - this.canvas = canvas; this.keydownListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, { $typeName: "proto.ProtoInput", inputType: { @@ -54,23 +50,12 @@ export class Keyboard { } private run() { - //calls all the other functions - if (!document.pointerLockElement) { - if (this.connected) { - this.stop() - } - return; - } + if (this.connected) + this.stop() - if (document.pointerLockElement == this.canvas) { - this.connected = true - document.addEventListener("keydown", this.keydownListener, {passive: false}); - document.addEventListener("keyup", this.keyupListener, {passive: false}); - } else { - if (this.connected) { - this.stop() - } - } + this.connected = true + document.addEventListener("keydown", this.keydownListener, {passive: false}); + document.addEventListener("keyup", this.keyupListener, {passive: false}); } private stop() { @@ -120,7 +105,6 @@ export class Keyboard { } public dispose() { - document.exitPointerLock(); this.stop(); this.connected = false; } diff --git a/packages/input/src/mouse.ts b/packages/input/src/mouse.ts index db0388bd..34b1a1bf 100644 --- a/packages/input/src/mouse.ts +++ b/packages/input/src/mouse.ts @@ -24,13 +24,12 @@ export class Mouse { protected canvas: HTMLCanvasElement; protected connected!: boolean; - // Store references to event listeners - private sendInterval = 16 //60fps + private sendInterval = 10 // 100 updates per second + // Store references to event listeners private readonly mousemoveListener: (e: MouseEvent) => void; private movementX: number = 0; private movementY: number = 0; - private isProcessing: boolean = false; private readonly mousedownListener: (e: MouseEvent) => void; private readonly mouseupListener: (e: MouseEvent) => void; @@ -40,7 +39,7 @@ export class Mouse { this.wrtc = webrtc; this.canvas = canvas; - this.sendInterval = 1000 / webrtc.currentFrameRate + this.sendInterval = 1000 / webrtc.currentFrameRate; this.mousemoveListener = (e: MouseEvent) => { e.preventDefault(); @@ -75,8 +74,8 @@ export class Mouse { case: "mouseWheel", value: create(ProtoMouseWheelSchema, { type: "MouseWheel", - x: e.deltaX, - y: e.deltaY + x: Math.round(e.deltaX), + y: Math.round(e.deltaY), }), } })); @@ -135,8 +134,8 @@ export class Mouse { case: "mouseMove", value: create(ProtoMouseMoveSchema, { type: "MouseMove", - x: this.movementX, - y: this.movementY, + x: Math.round(this.movementX), + y: Math.round(this.movementY), }), }, }); diff --git a/packages/input/src/proto/latency_tracker_pb.ts b/packages/input/src/proto/latency_tracker_pb.ts index f3d94771..27eefd2a 100644 --- a/packages/input/src/proto/latency_tracker_pb.ts +++ b/packages/input/src/proto/latency_tracker_pb.ts @@ -1,9 +1,9 @@ -// @generated by protoc-gen-es v2.2.3 with parameter "target=ts" +// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated from file latency_tracker.proto (package proto, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; -import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import type { Message } from "@bufbuild/protobuf"; diff --git a/packages/input/src/proto/messages_pb.ts b/packages/input/src/proto/messages_pb.ts index 0b918b94..2e432370 100644 --- a/packages/input/src/proto/messages_pb.ts +++ b/packages/input/src/proto/messages_pb.ts @@ -1,9 +1,9 @@ -// @generated by protoc-gen-es v2.2.3 with parameter "target=ts" +// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated from file messages.proto (package proto, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; -import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { ProtoInput } from "./types_pb"; import { file_types } from "./types_pb"; import type { ProtoLatencyTracker } from "./latency_tracker_pb"; diff --git a/packages/input/src/proto/types_pb.ts b/packages/input/src/proto/types_pb.ts index 689bdc1f..a4647fe0 100644 --- a/packages/input/src/proto/types_pb.ts +++ b/packages/input/src/proto/types_pb.ts @@ -1,16 +1,16 @@ -// @generated by protoc-gen-es v2.2.3 with parameter "target=ts" +// @generated by protoc-gen-es v2.9.0 with parameter "target=ts" // @generated from file types.proto (package proto, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; -import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file types.proto. */ export const file_types: GenFile = /*@__PURE__*/ - fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSLcAgoKUHJvdG9JbnB1dBIrCgptb3VzZV9tb3ZlGAEgASgLMhUucHJvdG8uUHJvdG9Nb3VzZU1vdmVIABIyCg5tb3VzZV9tb3ZlX2FicxgCIAEoCzIYLnByb3RvLlByb3RvTW91c2VNb3ZlQWJzSAASLQoLbW91c2Vfd2hlZWwYAyABKAsyFi5wcm90by5Qcm90b01vdXNlV2hlZWxIABIyCg5tb3VzZV9rZXlfZG93bhgEIAEoCzIYLnByb3RvLlByb3RvTW91c2VLZXlEb3duSAASLgoMbW91c2Vfa2V5X3VwGAUgASgLMhYucHJvdG8uUHJvdG9Nb3VzZUtleVVwSAASJwoIa2V5X2Rvd24YBiABKAsyEy5wcm90by5Qcm90b0tleURvd25IABIjCgZrZXlfdXAYByABKAsyES5wcm90by5Qcm90b0tleVVwSABCDAoKaW5wdXRfdHlwZUIWWhRyZWxheS9pbnRlcm5hbC9wcm90b2IGcHJvdG8z"); + fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSI/ChVQcm90b0NvbnRyb2xsZXJBdHRhY2gSDAoEdHlwZRgBIAEoCRIKCgJpZBgCIAEoCRIMCgRzbG90GAMgASgFIjMKFVByb3RvQ29udHJvbGxlckRldGFjaBIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUiVAoVUHJvdG9Db250cm9sbGVyQnV0dG9uEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIOCgZidXR0b24YAyABKAUSDwoHcHJlc3NlZBgEIAEoCCJUChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIPCgd0cmlnZ2VyGAMgASgFEg0KBXZhbHVlGAQgASgFIlcKFFByb3RvQ29udHJvbGxlclN0aWNrEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRINCgVzdGljaxgDIAEoBRIJCgF4GAQgASgFEgkKAXkYBSABKAUiTgoTUHJvdG9Db250cm9sbGVyQXhpcxIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUSDAoEYXhpcxgDIAEoBRINCgV2YWx1ZRgEIAEoBSJ0ChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEdHlwZRgBIAEoCRIMCgRzbG90GAIgASgFEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi9QUKClByb3RvSW5wdXQSKwoKbW91c2VfbW92ZRgBIAEoCzIVLnByb3RvLlByb3RvTW91c2VNb3ZlSAASMgoObW91c2VfbW92ZV9hYnMYAiABKAsyGC5wcm90by5Qcm90b01vdXNlTW92ZUFic0gAEi0KC21vdXNlX3doZWVsGAMgASgLMhYucHJvdG8uUHJvdG9Nb3VzZVdoZWVsSAASMgoObW91c2Vfa2V5X2Rvd24YBCABKAsyGC5wcm90by5Qcm90b01vdXNlS2V5RG93bkgAEi4KDG1vdXNlX2tleV91cBgFIAEoCzIWLnByb3RvLlByb3RvTW91c2VLZXlVcEgAEicKCGtleV9kb3duGAYgASgLMhMucHJvdG8uUHJvdG9LZXlEb3duSAASIwoGa2V5X3VwGAcgASgLMhEucHJvdG8uUHJvdG9LZXlVcEgAEjkKEWNvbnRyb2xsZXJfYXR0YWNoGAggASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyQXR0YWNoSAASOQoRY29udHJvbGxlcl9kZXRhY2gYCSABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJEZXRhY2hIABI5ChFjb250cm9sbGVyX2J1dHRvbhgKIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckJ1dHRvbkgAEjsKEmNvbnRyb2xsZXJfdHJpZ2dlchgLIAEoCzIdLnByb3RvLlByb3RvQ29udHJvbGxlclRyaWdnZXJIABI3ChBjb250cm9sbGVyX3N0aWNrGAwgASgLMhsucHJvdG8uUHJvdG9Db250cm9sbGVyU3RpY2tIABI1Cg9jb250cm9sbGVyX2F4aXMYDSABKAsyGi5wcm90by5Qcm90b0NvbnRyb2xsZXJBeGlzSAASOQoRY29udHJvbGxlcl9ydW1ibGUYDiABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIAEIMCgppbnB1dF90eXBlQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM"); /** * MouseMove message @@ -209,6 +209,293 @@ export type ProtoKeyUp = Message<"proto.ProtoKeyUp"> & { export const ProtoKeyUpSchema: GenMessage = /*@__PURE__*/ messageDesc(file_types, 6); +/** + * ControllerAttach message + * + * @generated from message proto.ProtoControllerAttach + */ +export type ProtoControllerAttach = Message<"proto.ProtoControllerAttach"> & { + /** + * Fixed value "ControllerAttach" + * + * @generated from field: string type = 1; + */ + type: string; + + /** + * One of the following enums: "ps", "xbox" or "switch" + * + * @generated from field: string id = 2; + */ + id: string; + + /** + * Slot number (0-3) + * + * @generated from field: int32 slot = 3; + */ + slot: number; +}; + +/** + * Describes the message proto.ProtoControllerAttach. + * Use `create(ProtoControllerAttachSchema)` to create a new message. + */ +export const ProtoControllerAttachSchema: GenMessage = /*@__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 = /*@__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 = /*@__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 = /*@__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 = /*@__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 = /*@__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 = /*@__PURE__*/ + messageDesc(file_types, 13); + /** * Union of all Input types * @@ -260,6 +547,48 @@ export type ProtoInput = Message<"proto.ProtoInput"> & { */ value: ProtoKeyUp; case: "keyUp"; + } | { + /** + * @generated from field: proto.ProtoControllerAttach controller_attach = 8; + */ + value: ProtoControllerAttach; + case: "controllerAttach"; + } | { + /** + * @generated from field: proto.ProtoControllerDetach controller_detach = 9; + */ + value: ProtoControllerDetach; + case: "controllerDetach"; + } | { + /** + * @generated from field: proto.ProtoControllerButton controller_button = 10; + */ + value: ProtoControllerButton; + case: "controllerButton"; + } | { + /** + * @generated from field: proto.ProtoControllerTrigger controller_trigger = 11; + */ + value: ProtoControllerTrigger; + case: "controllerTrigger"; + } | { + /** + * @generated from field: proto.ProtoControllerStick controller_stick = 12; + */ + value: ProtoControllerStick; + case: "controllerStick"; + } | { + /** + * @generated from field: proto.ProtoControllerAxis controller_axis = 13; + */ + value: ProtoControllerAxis; + case: "controllerAxis"; + } | { + /** + * @generated from field: proto.ProtoControllerRumble controller_rumble = 14; + */ + value: ProtoControllerRumble; + case: "controllerRumble"; } | { case: undefined; value?: undefined }; }; @@ -268,5 +597,5 @@ export type ProtoInput = Message<"proto.ProtoInput"> & { * Use `create(ProtoInputSchema)` to create a new message. */ export const ProtoInputSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_types, 7); + messageDesc(file_types, 14); diff --git a/packages/input/src/webrtc-stream.ts b/packages/input/src/webrtc-stream.ts index bf2dcc81..879cba35 100644 --- a/packages/input/src/webrtc-stream.ts +++ b/packages/input/src/webrtc-stream.ts @@ -5,6 +5,7 @@ import { SafeStream, } from "./messages"; import { webSockets } from "@libp2p/websockets"; +import { webTransport } from "@libp2p/webtransport"; import { createLibp2p, Libp2p } from "libp2p"; import { noise } from "@chainsafe/libp2p-noise"; import { yamux } from "@chainsafe/libp2p-yamux"; @@ -13,9 +14,6 @@ import { multiaddr } from "@multiformats/multiaddr"; import { Connection } from "@libp2p/interface"; import { ping } from "@libp2p/ping"; -//FIXME: Sometimes the room will wait to say offline, then appear to be online after retrying :D -// This works for me, with my trashy internet, does it work for you as well? - const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0"; export class WebRTCStream { @@ -30,8 +28,9 @@ export class WebRTCStream { private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined; private _serverURL: string | undefined = undefined; private _roomName: string | undefined = undefined; - private _isConnected: boolean = false; // Add flag to track connection state - currentFrameRate: number = 60; + private _isConnected: boolean = false; + private _dataChannelCallbacks: Array<(data: any) => void> = []; + currentFrameRate: number = 100; constructor( serverURL: string, @@ -59,7 +58,7 @@ export class WebRTCStream { console.log("Setting up libp2p"); this._p2p = await createLibp2p({ - transports: [webSockets()], + transports: [webSockets(), webTransport()], connectionEncrypters: [noise()], streamMuxers: [yamux()], connectionGater: { @@ -219,7 +218,8 @@ export class WebRTCStream { } private _checkConnectionState() { - if (!this._pc) return; + if (!this._pc || !this._p2p || !this._p2pConn) + return; console.debug("Checking connection state:", { connectionState: this._pc.connectionState, @@ -267,9 +267,9 @@ export class WebRTCStream { this._pc.connectionState === "closed" || this._pc.iceConnectionState === "failed" ) { - console.log("Connection failed or closed, attempting reconnect"); - this._isConnected = false; // Reset connected state - this._handleConnectionFailure(); + console.log("PeerConnection failed or closed"); + //this._isConnected = false; // Reset connected state + //this._handleConnectionFailure(); } } @@ -318,6 +318,7 @@ export class WebRTCStream { console.error("Error closing data channel:", err); } this._dataChannel = undefined; + this._dataChannelCallbacks = []; } this._isConnected = false; // Reset connected state during cleanup } @@ -329,15 +330,31 @@ export class WebRTCStream { } } + public addDataChannelCallback(callback: (data: any) => void) { + this._dataChannelCallbacks.push(callback); + } + + public removeDataChannelCallback(callback: (data: any) => void) { + this._dataChannelCallbacks = this._dataChannelCallbacks.filter(cb => cb !== callback); + } + private _setupDataChannelEvents() { if (!this._dataChannel) return; this._dataChannel.onclose = () => console.log("sendChannel has closed"); this._dataChannel.onopen = () => console.log("sendChannel has opened"); - this._dataChannel.onmessage = (e) => - console.log( - `Message from DataChannel '${this._dataChannel?.label}' payload '${e.data}'`, - ); + this._dataChannel.onmessage = (event => { + // Parse as ProtoBuf message + const data = event.data; + // Call registered callback if exists + this._dataChannelCallbacks.forEach((callback) => { + try { + callback(data); + } catch (err) { + console.error("Error in data channel callback:", err); + } + }); + }); } private _gatherFrameRate() { diff --git a/packages/patches/bubblewrap/bubbleunheck.patch b/packages/patches/bubblewrap/bubbleunheck.patch new file mode 100644 index 00000000..9f03f40e --- /dev/null +++ b/packages/patches/bubblewrap/bubbleunheck.patch @@ -0,0 +1,18 @@ +diff --git a/bubblewrap.c b/bubblewrap.c +index f8728c7..42cfe2e 100644 +--- a/bubblewrap.c ++++ b/bubblewrap.c +@@ -876,13 +876,6 @@ acquire_privs (void) + /* Keep only the required capabilities for setup */ + set_required_caps (); + } +- else if (real_uid != 0 && has_caps ()) +- { +- /* We have some capabilities in the non-setuid case, which should not happen. +- Probably caused by the binary being setcap instead of setuid which we +- don't support anymore */ +- die ("Unexpected capabilities but not setuid, old file caps config?"); +- } + else if (real_uid == 0) + { + /* If our uid is 0, default to inheriting all caps; the caller diff --git a/packages/play-standalone/package.json b/packages/play-standalone/package.json index ef18c040..9db4e167 100644 --- a/packages/play-standalone/package.json +++ b/packages/play-standalone/package.json @@ -11,6 +11,6 @@ "dependencies": { "@astrojs/node": "^9.4.2", "@nestri/input": "*", - "astro": "5.13.2" + "astro": "5.14.5" } } \ No newline at end of file diff --git a/packages/play-standalone/src/pages/[room].astro b/packages/play-standalone/src/pages/[room].astro index a0b6f8ff..fbf0c7e1 100644 --- a/packages/play-standalone/src/pages/[room].astro +++ b/packages/play-standalone/src/pages/[room].astro @@ -24,7 +24,7 @@ if (envs_map.size > 0) {