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/cloud/infra/auth.ts b/cloud/infra/auth.ts index 3e13203d..9bb0b047 100644 --- a/cloud/infra/auth.ts +++ b/cloud/infra/auth.ts @@ -15,4 +15,11 @@ export const auth = new sst.cloudflare.Worker("Auth", { secret.DISCORD_CLIENT_ID, secret.DISCORD_CLIENT_SECRET, ], + transform: { + worker: { + placement: { + mode: "smart", + }, + }, + }, }); 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 72e8b396..300971cf 100644 --- a/packages/play-standalone/package.json +++ b/packages/play-standalone/package.json @@ -17,6 +17,6 @@ "@capacitor/core": "^7.4.3", "@capacitor/ios": "^7.4.3", "@nestri/input": "*", - "astro": "5.13.2" + "astro": "5.14.5" } } \ No newline at end of file diff --git a/packages/play-standalone/src/pages/play.astro b/packages/play-standalone/src/pages/play.astro index e9003108..8c77f55d 100644 --- a/packages/play-standalone/src/pages/play.astro +++ b/packages/play-standalone/src/pages/play.astro @@ -23,7 +23,7 @@ if (envs_map.size > 0) {