9 Commits

Author SHA1 Message Date
Philipp Neumann
fa9c53eb35 Merge branch 'dev' of github.com:nestrilabs/nestri into capacitor-testings 2025-11-08 13:23:02 +01:00
Philipp Neumann
695ccc4170 Merge branch 'feat/improvements-relay-and-friends' of github.com:nestrilabs/nestri into capacitor-testings 2025-11-01 18:34:22 +01:00
Philipp Neumann
6dd1f124c2 set cookie for room and peerUrl 2025-11-01 18:32:19 +01:00
DatCaptainHorse
8d5895fc5e Some rabbit nitpick fixes 2025-11-01 05:02:23 +02:00
DatCaptainHorse
1d88a03b93 More multi-controller fixes, better controller polling logic, clean up dead relay code 2025-11-01 00:53:15 +02:00
DatCaptainHorse
a54cf759fa Fixed multi-controllers, optimize and improve code in relay and nestri-server 2025-10-25 03:57:26 +03:00
DatCaptainHorse
67f9a7d0a0 Restructure protobufs and use them everywhere 2025-10-21 18:41:45 +03:00
Philipp Neumann
93eaf15739 Merge branch 'dev' of github.com:nestrilabs/nestri into capacitor-testings 2025-10-20 18:57:30 +02:00
Philipp Neumann
33d360b49b try to work with capacitor 2025-10-06 16:54:48 +02:00
99 changed files with 2470 additions and 1184 deletions

View File

@@ -3,14 +3,14 @@ variable "BASE_IMAGE" {
}
group "default" {
targets = ["runner-base", "runner-builder"]
targets = ["runner"]
}
target "runner-base" {
dockerfile = "containerfiles/runner-base.Containerfile"
context = "."
args = {
BASE_IMAGE = BASE_IMAGE
BASE_IMAGE = "${BASE_IMAGE}"
}
cache-from = ["type=gha,scope=runner-base-pr"]
cache-to = ["type=gha,scope=runner-base-pr,mode=max"]
@@ -30,3 +30,19 @@ target "runner-builder" {
runner-base = "target:runner-base"
}
}
target "runner" {
dockerfile = "containerfiles/runner.Containerfile"
context = "."
args = {
RUNNER_BASE_IMAGE = "runner-base:latest"
RUNNER_BUILDER_IMAGE = "runner-builder:latest"
}
cache-from = ["type=gha,scope=runner-pr"]
cache-to = ["type=gha,scope=runner-pr,mode=max"]
tags = ["nestri-runner"]
contexts = {
runner-base = "target:runner-base"
runner-builder = "target:runner-builder"
}
}

View File

@@ -1,83 +0,0 @@
name: Build Nestri standalone playsite
on:
pull_request:
paths:
- "containerfiles/playsite.Containerfile"
- ".github/workflows/play-standalone.yml"
- "packages/play-standalone/**"
- "packages/input/**"
push:
branches: [ dev, production ]
paths:
- "containerfiles/playsite.Containerfile"
- ".github/workflows/play-standalone.yml"
- "packages/play-standalone/**"
- "packages/input/**"
tags:
- v*.*.*
release:
types: [ created ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: nestrilabs/nestri
BASE_TAG_PREFIX: playsite
jobs:
build-docker-pr:
name: Build image on PR
runs-on: ubuntu-latest
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: Build Docker image
uses: docker/build-push-action@v5
with:
file: containerfiles/playsite.Containerfile
context: ./
push: false
load: true
tags: nestri:playsite
build-and-push-docker:
name: Build and push image
if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/production' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Extract Container metadata
id: meta
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'
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}}
- name: Build Docker image
uses: docker/build-push-action@v5
with:
file: containerfiles/playsite.Containerfile
context: ./
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,5 +1,6 @@
name: Build Nestri relay
#Tabs not spaces, you moron :)
name: Build nestri:relay
on:
pull_request:
paths:

View File

@@ -1,73 +0,0 @@
name: Build Nestri runner base images
on: [ workflow_call ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: nestrilabs/nestri
BASE_IMAGE: docker.io/cachyos/cachyos:latest
jobs:
build-and-push-bases:
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: "v2", base: "docker.io/cachyos/cachyos:latest" }
- { suffix: "v3", base: "docker.io/cachyos/cachyos-v3:latest" }
#- { suffix: "v4", base: "docker.io/cachyos/cachyos-v4:latest" } # Disabled until GHA has this
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ github.token }}
- 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 and push runner-base image
uses: docker/build-push-action@v6
with:
file: containerfiles/runner-base.Containerfile
context: ./
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest-${{ matrix.variant.suffix }}
build-args: |
BASE_IMAGE=${{ matrix.variant.base }}
cache-from: type=gha,scope=runner-base-${{ matrix.variant.suffix }},mode=max
cache-to: type=gha,scope=runner-base-${{ matrix.variant.suffix }},mode=max
pull: true
- 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-common image
uses: docker/build-push-action@v6
with:
file: containerfiles/runner-common.Containerfile
context: ./
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-common:latest-${{ matrix.variant.suffix }}
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-common-${{ matrix.variant.suffix }},mode=max
cache-to: type=gha,scope=runner-common-${{ matrix.variant.suffix }},mode=max

View File

@@ -1,86 +0,0 @@
name: Build Nestri runner image variants
on:
workflow_dispatch:
schedule:
- cron: 7 0 * * 1,3,6 # Nightlies
push:
branches: [ dev, production ]
paths:
- "containerfiles/*runner.Containerfile"
- ".github/workflows/runner-variants.yml"
- "packages/scripts/**"
- "packages/configs/**"
tags:
- v*.*.*
release:
types: [ created ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: nestrilabs/nestri
jobs:
bases:
uses: ./.github/workflows/runner-bases.yml
permissions:
contents: read
packages: write
build-and-push-variants:
needs: [ bases ]
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: "v2", base: "docker.io/cachyos/cachyos:latest" }
- { suffix: "v3", base: "docker.io/cachyos/cachyos-v3:latest" }
#- { suffix: "v4", base: "docker.io/cachyos/cachyos-v4:latest" } # Disabled until GHA has this
runner:
- steam
- heroic
- minecraft
# ADD MORE HERE AS NEEDED #
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Extract runner metadata
id: meta-runner
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner
tags: |
type=raw,value=nightly-${{ matrix.runner }}-${{ matrix.variant.suffix }},enable={{is_default_branch}}
type=raw,value={{branch}}-${{ matrix.runner }}-${{ matrix.variant.suffix }}
type=raw,value=latest-${{ matrix.runner }}-${{ matrix.variant.suffix }},enable=${{ github.ref == format('refs/heads/{0}', 'production') }}
type=semver,pattern={{version}}-${{ matrix.runner }}-${{ matrix.variant.suffix }}
type=semver,pattern={{major}}.{{minor}}-${{ matrix.runner }}-${{ matrix.variant.suffix }}
type=semver,pattern={{major}}-${{ matrix.runner }}-${{ matrix.variant.suffix }}
- 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 and push runner image
uses: docker/build-push-action@v6
with:
file: containerfiles/${{ matrix.runner }}-runner.Containerfile
context: ./
push: true
tags: ${{ steps.meta-runner.outputs.tags }}
labels: ${{ steps.meta-runner.outputs.labels }}
build-args: |
RUNNER_COMMON_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-common:latest-${{ matrix.variant.suffix }}
cache-from: type=gha,scope=runner-${{ matrix.runner }}-${{ matrix.variant.suffix }},mode=max
cache-to: type=gha,scope=runner-${{ matrix.runner }}-${{ matrix.variant.suffix }},mode=max

148
.github/workflows/runner.yml vendored Normal file
View File

@@ -0,0 +1,148 @@
#Tabs not spaces, you moron :)
name: Build nestri-runner
on:
pull_request:
paths:
- "containerfiles/runner*.Containerfile"
- "packages/scripts/**"
- "packages/server/**"
- ".github/workflows/runner.yml"
schedule:
- cron: 7 0 * * 1,3,6 # Regularly to keep that build cache warm
push:
branches: [dev, production]
paths:
- "containerfiles/runner*.Containerfile"
- ".github/workflows/runner.yml"
- "packages/scripts/**"
- "packages/server/**"
tags:
- v*.*.*
release:
types: [created]
env:
REGISTRY: ghcr.io
IMAGE_NAME: nestrilabs/nestri
BASE_IMAGE: docker.io/cachyos/cachyos:latest
# This makes our release ci quit prematurely
# concurrency:
# group: ci-${{ github.ref }}
# cancel-in-progress: true
jobs:
build-docker-pr:
name: Build images on PR
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
if: ${{ github.event_name == 'pull_request' }}
steps:
-
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 images using bake
uses: docker/bake-action@v6
env:
BASE_IMAGE: ${{ env.BASE_IMAGE }}
with:
files: |
./.github/workflows/docker-bake.hcl
targets: runner
push: false
load: true
build-and-push-docker:
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" } # Disabled until GHA has this
steps:
-
name: Checkout repo
uses: actions/checkout@v4
-
name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ github.token }}
-
name: Extract runner metadata
id: meta-runner
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner
tags: |
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
-
name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 20
-
name: Build and push runner-base image
uses: docker/build-push-action@v6
with:
file: containerfiles/runner-base.Containerfile
context: ./
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }}
build-args: |
BASE_IMAGE=${{ matrix.variant.base }}
cache-from: type=gha,scope=runner-base${{ matrix.variant.suffix }},mode=max
cache-to: type=gha,scope=runner-base${{ matrix.variant.suffix }},mode=max
pull: ${{ github.event_name == 'schedule' }}
-
name: Build and push runner-builder image
uses: docker/build-push-action@v6
with:
file: containerfiles/runner-builder.Containerfile
context: ./
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest${{ matrix.variant.suffix }}
build-args: |
RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }}
cache-from: type=gha,scope=runner-builder${{ matrix.variant.suffix }},mode=max
cache-to: type=gha,scope=runner-builder${{ matrix.variant.suffix }},mode=max
-
name: Build and push runner image
uses: docker/build-push-action@v6
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

View File

@@ -15,4 +15,3 @@ plugins:
# Rust (nestri-server)
- remote: buf.build/community/neoeinstein-prost
out: packages/server/src/proto
opt: flat_output_dir=true

541
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +0,0 @@
# Container build arguments #
ARG RUNNER_COMMON_IMAGE=runner-common:latest
#*********************#
# Final Runtime Stage #
#*********************#
FROM ${RUNNER_COMMON_IMAGE}
### FLAVOR/VARIANT CONFIGURATION ###
## HEROIC LAUNCHER ##
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --noconfirm heroic-games-launcher-bin && \
# Cleanup
paccache -rk1 && \
rm -rf /usr/share/{info,man,doc}/*
## FLAVOR/VARIANT LAUNCH COMMAND ##
ENV NESTRI_LAUNCH_CMD="heroic"
### END OF FLAVOR/VARIANT CONFIGURATION ###
### REQUIRED DEFAULT ENTRYPOINT FOR FLAVOR/VARIANT ###
USER root
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]

View File

@@ -1,24 +0,0 @@
# Container build arguments #
ARG RUNNER_COMMON_IMAGE=runner-common:latest
#*********************#
# Final Runtime Stage #
#*********************#
FROM ${RUNNER_COMMON_IMAGE}
### FLAVOR/VARIANT CONFIGURATION ###
## MINECRAFT ##
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --noconfirm paru && \
sudo -H -u ${NESTRI_USER} paru -S --noconfirm aur/minecraft-launcher && \
# Cleanup
paccache -rk1 && \
rm -rf /usr/share/{info,man,doc}/*
## FLAVOR/VARIANT LAUNCH COMMAND ##
ENV NESTRI_LAUNCH_CMD="minecraft-launcher"
### END OF FLAVOR/VARIANT CONFIGURATION ###
### REQUIRED DEFAULT ENTRYPOINT FOR FLAVOR/VARIANT ###
USER root
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]

View File

@@ -15,7 +15,7 @@ ENV CARGO_HOME=/usr/local/cargo \
# Install build essentials and caching tools
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --noconfirm rustup git base-devel mold \
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)
@@ -28,7 +28,7 @@ RUN rustup default stable
# Install cargo-chef with proper caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo install cargo-chef --locked
cargo install -j $(nproc) cargo-chef --locked
#*******************************#
# vimputti manager build stages #
@@ -38,7 +38,7 @@ WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --noconfirm lib32-gcc-libs
pacman -Sy --noconfirm lib32-gcc-libs
# Clone repository
RUN git clone --depth 1 --rev "2fde5376b6b9a38cdbd94ccc6a80c9d29a81a417" https://github.com/DatCaptainHorse/vimputti.git
@@ -83,7 +83,7 @@ WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --noconfirm gst-plugins-good gst-plugin-rswebrtc
pacman -Sy --noconfirm gst-plugins-good gst-plugin-rswebrtc
#--------------------------------------------------------------------
FROM nestri-server-deps AS nestri-server-planner
@@ -123,14 +123,14 @@ WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --noconfirm libxkbcommon wayland \
pacman -Sy --noconfirm libxkbcommon wayland \
gst-plugins-good gst-plugins-bad libinput
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo install cargo-c
# Clone repository
RUN git clone --depth 1 --rev "67b1183997fd7aaf57398e4b01bd64c4d2433c45" https://github.com/games-on-whales/gst-wayland-display.git
RUN git clone --depth 1 --rev "a4abcfe2cffe2d33b564d1308b58504a5e3012b1" https://github.com/games-on-whales/gst-wayland-display.git
#--------------------------------------------------------------------
FROM gst-wayland-deps AS gst-wayland-planner
@@ -148,7 +148,7 @@ 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 --features cuda
cargo chef cook --release --recipe-path recipe.json
ENV CARGO_TARGET_DIR=/builder/target
@@ -158,7 +158,7 @@ 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 --features cuda
cargo cinstall --prefix=${ARTIFACTS} --release
#*********************************#
# Patched bubblewrap build stages #
@@ -168,7 +168,7 @@ WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --noconfirm libtool libcap libselinux
pacman -Sy --noconfirm libtool libcap libselinux
# Copy patch file from host
COPY packages/patches/bubblewrap/ /builder/patches/

View File

@@ -2,9 +2,9 @@
ARG RUNNER_BASE_IMAGE=runner-base:latest
ARG RUNNER_BUILDER_IMAGE=runner-builder:latest
#**********************#
# Runtime Common Stage #
#**********************#
#*********************#
# Final Runtime Stage #
#*********************#
FROM ${RUNNER_BASE_IMAGE} AS runtime
FROM ${RUNNER_BUILDER_IMAGE} AS builder
FROM runtime
@@ -12,11 +12,11 @@ FROM runtime
### Package Installation ###
# Core system components
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --needed --noconfirm \
pacman -Sy --needed --noconfirm \
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
vulkan-radeon lib32-vulkan-radeon \
mesa lib32-mesa vulkan-mesa-layers lib32-vulkan-mesa-layers \
gtk3 lib32-gtk3 \
mesa lib32-mesa \
steam gtk3 lib32-gtk3 \
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
pipewire pipewire-pulse pipewire-alsa wireplumber \
noto-fonts-cjk supervisor jq pacman-contrib \
@@ -67,8 +67,15 @@ RUN mkdir -p /etc/pipewire/pipewire.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 (Experimental flavor) ##
RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config"
COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/"
## MangoHud Config ##
COPY packages/configs/MangoHud/MangoHud.conf /etc/nestri/configs/MangoHud/
RUN mkdir -p "${NESTRI_HOME}/.config/MangoHud"
COPY packages/configs/MangoHud/MangoHud.conf "${NESTRI_HOME}/.config/MangoHud/"
### Artifacts from Builder ###
COPY --from=builder /artifacts/bin/nestri-server /usr/bin/
@@ -86,3 +93,7 @@ RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \
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
USER root
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]

View File

@@ -1,27 +0,0 @@
# Container build arguments #
ARG RUNNER_COMMON_IMAGE=runner-common:latest
#*********************#
# Final Runtime Stage #
#*********************#
FROM ${RUNNER_COMMON_IMAGE}
### FLAVOR/VARIANT CONFIGURATION ###
## STEAM ##
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -S --noconfirm steam && \
# Cleanup
paccache -rk1 && \
rm -rf /usr/share/{info,man,doc}/*
## 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/"
## FLAVOR/VARIANT LAUNCH COMMAND ##
ENV NESTRI_LAUNCH_CMD="steam -tenfoot -cef-force-gpu"
### END OF FLAVOR/VARIANT CONFIGURATION ###
### REQUIRED DEFAULT ENTRYPOINT FOR FLAVOR/VARIANT ###
USER root
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]

View File

@@ -2,6 +2,6 @@ context.properties = {
default.clock.rate = 48000
default.clock.allowed-rates = [48000]
default.clock.min-quantum = 128
default.clock.max-quantum = 1024
default.clock.quantum = 512
default.clock.max-quantum = 256
default.clock.quantum = 128
}

View File

@@ -22,7 +22,7 @@
],
"apply_properties": {
"pulse.min.req": 128,
"pulse.max.req": 1024,
"pulse.max.req": 256,
"pulse.idle.timeout": 0
}
}

View File

@@ -1,3 +0,0 @@
.idea/
dist/
node_modules/

View File

@@ -36,8 +36,10 @@ export class Keyboard {
if (this.connected) this.stop();
this.connected = true;
document.addEventListener("keydown", this.keydownListener);
document.addEventListener("keyup", this.keyupListener);
document.addEventListener("keydown", this.keydownListener, {
passive: false,
});
document.addEventListener("keyup", this.keyupListener, { passive: false });
}
private stop() {
@@ -71,6 +73,6 @@ export class Keyboard {
private keyToVirtualKeyCode(code: string) {
// Treat Home key as Escape - TODO: Make user-configurable
if (code === "Home") return 1;
return keyCodeToLinuxEventCode[code] || 0;
return keyCodeToLinuxEventCode[code] || undefined;
}
}

View File

@@ -35,6 +35,8 @@ export class Mouse {
this.wrtc = webrtc;
this.canvas = canvas;
this.sendInterval = 1000 / webrtc.currentFrameRate;
this.mousemoveListener = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -75,10 +77,18 @@ export class Mouse {
if (document.pointerLockElement == this.canvas) {
this.connected = true;
this.canvas.addEventListener("mousemove", this.mousemoveListener);
this.canvas.addEventListener("mousedown", this.mousedownListener);
this.canvas.addEventListener("mouseup", this.mouseupListener);
this.canvas.addEventListener("wheel", this.mousewheelListener);
this.canvas.addEventListener("mousemove", this.mousemoveListener, {
passive: false,
});
this.canvas.addEventListener("mousedown", this.mousedownListener, {
passive: false,
});
this.canvas.addEventListener("mouseup", this.mouseupListener, {
passive: false,
});
this.canvas.addEventListener("wheel", this.mousewheelListener, {
passive: false,
});
} else {
if (this.connected) {
this.stop();
@@ -96,7 +106,7 @@ export class Mouse {
private startProcessing() {
setInterval(() => {
if (this.connected) {
if (this.connected && (this.movementX !== 0 || this.movementY !== 0)) {
this.sendAggregatedMouseMove();
this.movementX = 0;
this.movementY = 0;

View File

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

View File

@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated from file messages.proto (package proto, syntax proto3)
/* eslint-disable */
@@ -90,8 +90,6 @@ export type ProtoMessage = Message<"proto.ProtoMessage"> & {
case: "keyDown";
} | {
/**
* ProtoClipboard clipboard = 9;
*
* @generated from field: proto.ProtoKeyUp key_up = 8;
*/
value: ProtoKeyUp;

View File

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

View File

@@ -13,10 +13,9 @@ import {
ProtoClientRequestRoomStream,
ProtoClientRequestRoomStreamSchema,
ProtoICE,
ProtoICESchema,
ProtoRaw,
ProtoICESchema, ProtoRaw,
ProtoSDP,
ProtoSDPSchema,
ProtoSDPSchema
} from "./proto/types_pb";
import { P2PMessageStream } from "./streamwrapper";
@@ -39,6 +38,7 @@ export class WebRTCStream {
private _roomName: string | undefined = undefined;
private _isConnected: boolean = false;
private _dataChannelCallbacks: Array<(data: any) => void> = [];
currentFrameRate: number = 100;
constructor(
serverURL: string,
@@ -126,19 +126,11 @@ export class WebRTCStream {
}
});
this._msgStream.on(
"session-assigned",
(data: ProtoClientRequestRoomStream) => {
this._msgStream.on("session-assigned", (data: ProtoClientRequestRoomStream) => {
this._sessionId = data.sessionId;
localStorage.setItem("nestri-session-id", this._sessionId);
console.log(
"Session ID assigned:",
this._sessionId,
"for room:",
data.roomName,
);
},
);
console.log("Session ID assigned:", this._sessionId, "for room:", data.roomName);
});
this._msgStream.on("offer", async (data: ProtoSDP) => {
if (!this._pc) {
@@ -301,8 +293,26 @@ export class WebRTCStream {
this._onConnected(
new MediaStream([this._audioTrack, this._videoTrack]),
);
// Continuously set low-latency target
this._pc.getReceivers().forEach((receiver: RTCRtpReceiver) => {
let intervalLoop = setInterval(async () => {
if (
receiver.track.readyState !== "live" ||
(receiver.transport && receiver.transport.state !== "connected")
) {
clearInterval(intervalLoop);
return;
} else {
// @ts-ignore
receiver.jitterBufferTarget = receiver.jitterBufferDelayHint = receiver.playoutDelayHint = 0;
}
}, 50);
});
}
}
this._gatherFrameRate();
} else if (
this._pc.connectionState === "failed" ||
this._pc.connectionState === "closed" ||
@@ -402,16 +412,11 @@ export class WebRTCStream {
};
}
private async _gatherStats(): Promise<any> {
if (
this._pc === undefined ||
this._videoTrack === undefined ||
!this._isConnected
)
return null;
private _gatherFrameRate() {
if (this._pc === undefined || this._videoTrack === undefined) return;
return new Promise<any>((resolve) => {
// Keep trying to get stats until gotten
const videoInfoPromise = new Promise<{ fps: number }>((resolve) => {
// Keep trying to get fps until it's found
const interval = setInterval(async () => {
if (this._pc === undefined) {
clearInterval(interval);
@@ -423,11 +428,15 @@ export class WebRTCStream {
if (report.type === "inbound-rtp") {
clearInterval(interval);
resolve({ pli: report.pliCount, nack: report.nackCount });
resolve({ fps: report.framesPerSecond });
}
});
}, 250);
});
videoInfoPromise.then((value) => {
this.currentFrameRate = value.fps;
});
}
// Send binary message through the data channel

View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

View File

@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace "com.nestri.play"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.nestri.play"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,5 @@
package com.nestri.play;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Nestri Play</string>
<string name="title_activity_main">Nestri Play</string>
<string name="package_name">com.nestri.play</string>
<string name="custom_url_scheme">com.nestri.play</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../../node_modules/@capacitor/android/capacitor')

View File

@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
packages/play-standalone/android/gradlew vendored Executable file
View File

@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
}

View File

@@ -2,12 +2,10 @@
import { defineConfig, envField } from "astro/config";
import node from "@astrojs/node";
// https://astro.build/config
export default defineConfig({
adapter: node({
mode: 'standalone',
}),
output: "server",
output: "static",
server: {
"host": "0.0.0.0",
"port": 3000,

View File

@@ -0,0 +1,9 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.nestri.play',
appName: 'Nestri Play',
webDir: 'dist'
};
export default config;

View File

@@ -5,10 +5,16 @@
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build:cap": "astro build && npx cap copy",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"sync:android": "npm run build && npx cap sync android"
},
"dependencies": {
"@capacitor/android": "^7.4.3",
"@capacitor/cli": "^7.4.3",
"@capacitor/core": "^7.4.3",
"@capacitor/ios": "^7.4.3",
"@astrojs/node": "9.5.0",
"@nestri/input": "*",
"astro": "5.15.1"

View File

@@ -0,0 +1,138 @@
---
import { ClientRouter } from "astro:transitions";
import { navigate } from "astro:transitions/client";
---
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>Nestri Play</title>
<ClientRouter />
<style>
body {
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 2rem;
border: 1px solid #ccc;
border-radius: 8px;
}
div {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 0.5rem;
}
input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 0.75rem;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<form id="join-form">
<h1>Nestri Play</h1>
<div>
<label for="room">Room</label>
<input type="text" id="room" name="room" required list="room-list">
<datalist id="room-list"></datalist>
</div>
<div>
<label for="peerURL">peerURL</label>
<input type="text" id="peerURL" name="peerURL" required list="peerURL-list">
<datalist id="peerURL-list"></datalist>
</div>
<button type="submit">Join</button>
</form>
<script>
import { navigate } from "astro:transitions/client";
const roomInput = document.getElementById('room') as HTMLInputElement;
const peerURLInput = document.getElementById('peerURL') as HTMLInputElement;
const roomList = document.getElementById('room-list');
const peerURLList = document.getElementById('peerURL-list');
// Load values from cookies
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
const cookieValue = parts.pop()?.split(';').shift();
return cookieValue ? decodeURIComponent(cookieValue) : undefined;
}
}
function setCookie(name, value, days) {
const d = new Date();
d.setTime(d.getTime() + (days*24*60*60*1000));
const expires = "expires="+ d.toUTCString();
document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/";
}
const storedRooms = JSON.parse(getCookie('nestri-rooms') || '[]');
const storedPeerURLs = JSON.parse(getCookie('nestri-peerURLs') || '[]');
if (roomList) {
storedRooms.forEach(room => {
const option = document.createElement('option');
option.value = room;
roomList.appendChild(option);
});
}
if (peerURLList) {
storedPeerURLs.forEach(peerURL => {
const option = document.createElement('option');
option.value = peerURL;
peerURLList.appendChild(option);
});
}
if (storedRooms.length > 0 && roomInput) {
roomInput.value = storedRooms[0];
}
if (storedPeerURLs.length > 0 && peerURLInput) {
peerURLInput.value = storedPeerURLs[0];
}
document.getElementById('join-form')?.addEventListener('submit', function(event) {
event.preventDefault();
const room = roomInput.value;
const peerURL = peerURLInput.value;
// Save values to cookies
const newRooms = [room, ...storedRooms.filter(r => r !== room)].slice(0, 10);
const newPeerURLs = [peerURL, ...storedPeerURLs.filter(p => p !== peerURL)].slice(0, 10);
setCookie('nestri-rooms', JSON.stringify(newRooms), 365);
setCookie('nestri-peerURLs', JSON.stringify(newPeerURLs), 365);
if (room && peerURL) {
navigate(`/play/index.html?peerURL=${encodeURIComponent(peerURL)}#${room}`);
}
});
</script>
</body>
</html>

View File

@@ -1,6 +1,5 @@
---
import DefaultLayout from "../layouts/DefaultLayout.astro";
const { room } = Astro.params;
// Passing of environment variables to the client side
// gotta love node and it's ecosystem..
@@ -19,7 +18,7 @@ if (envs_map.size > 0) {
<DefaultLayout>
<h1 id="offlineText" class="offline">Offline</h1>
<h1 id="loadingText" class="loading">Warming up the GPU...</h1>
<canvas id="playCanvas" class="playCanvas" data-room={room}></canvas>
<canvas id="playCanvas" class="playCanvas"></canvas>
<div id="ENVS" data-envs={envs}></div>
</DefaultLayout>
@@ -42,9 +41,9 @@ if (envs_map.size > 0) {
const offlineText = document.getElementById("offlineText")! as HTMLHeadingElement;
const loadingText = document.getElementById("loadingText")! as HTMLHeadingElement;
const room = canvas.dataset.room;
const room = window.location.hash.substring(1);
if (!room || room.length <= 0) {
throw new Error("Room parameter is required");
throw new Error("Room parameter is required in URL hash");
}
offlineText.style.display = "flex";
@@ -99,6 +98,9 @@ if (envs_map.size > 0) {
});
window.addEventListener("gamepaddisconnected", (e) => {
console.log("Gamepad disconnected:", e.gamepad);
if (e.gamepad.id.toLowerCase().includes("nestri"))
return;
let disconnected = nestriControllers.find((c) => c.getSlot() === e.gamepad.index);
if (disconnected) {
disconnected.dispose();

View File

@@ -2,7 +2,6 @@ package common
import (
"fmt"
"github.com/pion/interceptor/pkg/nack"
"log/slog"
"strconv"
@@ -31,121 +30,20 @@ func InitWebRTCAPI() error {
return fmt.Errorf("failed to register extensions: %w", err)
}
// Register codecs
for _, codec := range []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, SDPFmtpLine: "minptime=10;useinbandfec=1"},
PayloadType: 111,
},
} {
if err = mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil {
// Default codecs cover our needs
err = mediaEngine.RegisterDefaultCodecs()
if err != nil {
return err
}
}
videoRTCPFeedback := []webrtc.RTCPFeedback{{"nack", ""}, {"nack", "pli"}}
for _, codec := range []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
RTCPFeedback: videoRTCPFeedback,
},
PayloadType: 102,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f",
RTCPFeedback: videoRTCPFeedback,
},
PayloadType: 104,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
RTCPFeedback: videoRTCPFeedback,
},
PayloadType: 106,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
RTCPFeedback: videoRTCPFeedback,
},
PayloadType: 108,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f",
RTCPFeedback: videoRTCPFeedback,
},
PayloadType: 127,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f",
RTCPFeedback: videoRTCPFeedback,
},
PayloadType: 39,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
RTCPFeedback: videoRTCPFeedback,
},
PayloadType: 116,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeAV1, ClockRate: 90000, RTCPFeedback: videoRTCPFeedback},
PayloadType: 45,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP9, ClockRate: 90000, SDPFmtpLine: "profile-id=0", RTCPFeedback: videoRTCPFeedback},
PayloadType: 98,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP9, ClockRate: 90000, SDPFmtpLine: "profile-id=2", RTCPFeedback: videoRTCPFeedback},
PayloadType: 100,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f",
RTCPFeedback: videoRTCPFeedback,
},
PayloadType: 112,
},
} {
if err = mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
// Interceptor registry
interceptorRegistry := &interceptor.Registry{}
// Register our interceptors..
nackGenFactory, err := nack.NewGeneratorInterceptor()
// Use default set
err = webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry)
if err != nil {
return err
}
interceptorRegistry.Add(nackGenFactory)
nackRespFactory, err := nack.NewResponderInterceptor()
if err != nil {
return err
}
interceptorRegistry.Add(nackRespFactory)
if err = webrtc.ConfigureRTCPReports(interceptorRegistry); err != nil {
return err
}
// Setting engine
settingEngine := webrtc.SettingEngine{}
@@ -155,7 +53,7 @@ func InitWebRTCAPI() error {
nat11IP := GetFlags().NAT11IP
if len(nat11IP) > 0 {
settingEngine.SetNAT1To1IPs([]string{nat11IP}, webrtc.ICECandidateTypeHost)
settingEngine.SetNAT1To1IPs([]string{nat11IP}, webrtc.ICECandidateTypeSrflx)
slog.Info("Using NAT 1:1 IP for WebRTC", "nat11_ip", nat11IP)
}

View File

@@ -125,7 +125,7 @@ func getLocalIP() string {
return ""
}
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && !ipnet.IP.IsPrivate() && !ipnet.IP.IsUnspecified() {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil || ipnet.IP != nil {
return ipnet.IP.String()
}

View File

@@ -20,9 +20,9 @@ import (
rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
"github.com/libp2p/go-libp2p/p2p/protocol/ping"
"github.com/libp2p/go-libp2p/p2p/security/noise"
p2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
webtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
"github.com/multiformats/go-multiaddr"
"github.com/oklog/ulid/v2"
@@ -91,10 +91,10 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
listenAddrs := []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4 - Raw TCP
fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6 - Raw TCP
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/ws", port), // IPv4 - TCP WebSocket
fmt.Sprintf("/ip6/::/tcp/%d/ws", port), // IPv6 - TCP WebSocket
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1/webtransport", port), // IPv4 - UDP QUIC WebTransport
fmt.Sprintf("/ip6/::/udp/%d/quic-v1/webtransport", port), // IPv6 - UDP QUIC WebTransport
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", port), // IPv4 - UDP Raw QUIC
fmt.Sprintf("/ip6/::/udp/%d/quic-v1", port), // IPv6 - UDP Raw QUIC
}
var muAddrs []multiaddr.Multiaddr
@@ -112,8 +112,8 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
libp2p.Identity(identityKey),
// Enable required transports
libp2p.Transport(tcp.NewTCPTransport),
libp2p.Transport(ws.New),
libp2p.Transport(webtransport.New),
libp2p.Transport(p2pquic.NewTransport),
// Other options
libp2p.ListenAddrs(muAddrs...),
libp2p.Security(noise.ID, noise.New),

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"log/slog"
"math"
"relay/internal/common"
"relay/internal/connections"
"relay/internal/shared"
@@ -605,8 +606,52 @@ func (sp *StreamProtocol) handleStreamPush(stream network.Stream) {
}
}
// Broadcast
room.BroadcastPacket(remoteTrack.Kind(), rtpPacket)
// Calculate differences
var timeDiff int64
var sequenceDiff int
if remoteTrack.Kind() == webrtc.RTPCodecTypeVideo {
timeDiff = int64(rtpPacket.Timestamp) - int64(room.LastVideoTimestamp)
if !room.VideoTimestampSet {
timeDiff = 0
room.VideoTimestampSet = true
} else if timeDiff < -(math.MaxUint32 / 10) {
timeDiff += math.MaxUint32 + 1
}
sequenceDiff = int(rtpPacket.SequenceNumber) - int(room.LastVideoSequenceNumber)
if !room.VideoSequenceSet {
sequenceDiff = 0
room.VideoSequenceSet = true
} else if sequenceDiff < -(math.MaxUint16 / 10) {
sequenceDiff += math.MaxUint16 + 1
}
room.LastVideoTimestamp = rtpPacket.Timestamp
room.LastVideoSequenceNumber = rtpPacket.SequenceNumber
} else { // Audio
timeDiff = int64(rtpPacket.Timestamp) - int64(room.LastAudioTimestamp)
if !room.AudioTimestampSet {
timeDiff = 0
room.AudioTimestampSet = true
} else if timeDiff < -(math.MaxUint32 / 10) {
timeDiff += math.MaxUint32 + 1
}
sequenceDiff = int(rtpPacket.SequenceNumber) - int(room.LastAudioSequenceNumber)
if !room.AudioSequenceSet {
sequenceDiff = 0
room.AudioSequenceSet = true
} else if sequenceDiff < -(math.MaxUint16 / 10) {
sequenceDiff += math.MaxUint16 + 1
}
room.LastAudioTimestamp = rtpPacket.Timestamp
room.LastAudioSequenceNumber = rtpPacket.SequenceNumber
}
// Broadcast with differences
room.BroadcastPacketRetimed(remoteTrack.Kind(), rtpPacket, timeDiff, sequenceDiff)
}
slog.Debug("Track closed for room", "room", room.Name, "track_kind", remoteTrack.Kind().String())

View File

@@ -106,15 +106,28 @@ func (p *Participant) Close() {
func (p *Participant) packetWriter() {
for pkt := range p.packetQueue {
var track *webrtc.TrackLocalStaticRTP
var sequenceNumber uint16
var timestamp uint32
// No mutex needed - only this goroutine modifies these
if pkt.kind == webrtc.RTPCodecTypeAudio {
track = p.AudioTrack
p.AudioSequenceNumber = uint16(int(p.AudioSequenceNumber) + pkt.sequenceDiff)
p.AudioTimestamp = uint32(int64(p.AudioTimestamp) + pkt.timeDiff)
sequenceNumber = p.AudioSequenceNumber
timestamp = p.AudioTimestamp
} else {
track = p.VideoTrack
p.VideoSequenceNumber = uint16(int(p.VideoSequenceNumber) + pkt.sequenceDiff)
p.VideoTimestamp = uint32(int64(p.VideoTimestamp) + pkt.timeDiff)
sequenceNumber = p.VideoSequenceNumber
timestamp = p.VideoTimestamp
}
if track != nil {
pkt.packet.SequenceNumber = sequenceNumber
pkt.packet.Timestamp = timestamp
if err := track.WriteRTP(pkt.packet); err != nil && !errors.Is(err, io.ErrClosedPipe) {
slog.Error("WriteRTP failed", "participant", p.ID, "kind", pkt.kind, "err", err)
}

View File

@@ -21,6 +21,8 @@ var participantPacketPool = sync.Pool{
type participantPacket struct {
kind webrtc.RTPCodecType
packet *rtp.Packet
timeDiff int64
sequenceDiff int
}
type RoomInfo struct {
@@ -139,7 +141,7 @@ func (r *Room) IsOnline() bool {
return r.PeerConnection != nil
}
func (r *Room) BroadcastPacket(kind webrtc.RTPCodecType, pkt *rtp.Packet) {
func (r *Room) BroadcastPacketRetimed(kind webrtc.RTPCodecType, pkt *rtp.Packet, timeDiff int64, sequenceDiff int) {
// Lock-free load of channel slice
channels := r.participantChannels.Load()
@@ -153,7 +155,9 @@ func (r *Room) BroadcastPacket(kind webrtc.RTPCodecType, pkt *rtp.Packet) {
// Get packet struct from pool
pp := participantPacketPool.Get().(*participantPacket)
pp.kind = kind
pp.packet = pkt
pp.packet = pkt.Clone()
pp.timeDiff = timeDiff
pp.sequenceDiff = sequenceDiff
select {
case ch <- pp:

View File

@@ -348,8 +348,18 @@ main() {
setup_namespaceless
fi
# Wait for vimputti socket before switching to application startup
wait_for_socket "/tmp/vimputti-0" "vimputti" || exit 1
# Make sure /run/udev/ directory exists with /run/udev/control, needed for virtual controller support
if [[ ! -d "/run/udev" || ! -e "/run/udev/control" ]]; then
log "Creating /run/udev directory and control file.."
$ENTCMD_PREFIX mkdir -p /run/udev || {
log "Error: Failed to create /run/udev directory"
exit 1
}
$ENTCMD_PREFIX touch /run/udev/control || {
log "Error: Failed to create /run/udev/control file"
exit 1
}
fi
# Switch to nestri runner entrypoint
log "Switching to application startup entrypoint..."

View File

@@ -106,9 +106,12 @@ start_compositor() {
kill_if_running "${COMPOSITOR_PID:-}" "compositor"
kill_if_running "${APP_PID:-}" "application"
# Set default compositor if unset
# Set default values only if variables are unset (not empty)
if [[ -z "${NESTRI_LAUNCH_CMD+x}" ]]; then
NESTRI_LAUNCH_CMD="dbus-launch steam -tenfoot -cef-force-gpu"
fi
if [[ -z "${NESTRI_LAUNCH_COMPOSITOR+x}" ]]; then
NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland -g -f --rt -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}"
NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland --force-grab-cursor -g -f --rt --mangoapp -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}"
fi
# If PRELOAD_SHIM_arch's are set and exist, set LD_PRELOAD for 32/64-bit apps
@@ -118,16 +121,6 @@ start_compositor() {
log "Using LD_PRELOAD shim(s)"
fi
# Configure launch cmd with dbus if set
local launch_cmd=""
if [[ -n "${NESTRI_LAUNCH_CMD+x}" ]]; then
if $do_ld_preload; then
launch_cmd="LD_PRELOAD='/usr/\$LIB/libvimputti_shim.so' dbus-launch $NESTRI_LAUNCH_CMD"
else
launch_cmd="dbus-launch $NESTRI_LAUNCH_CMD"
fi
fi
# Launch compositor if configured
if [[ -n "${NESTRI_LAUNCH_COMPOSITOR}" ]]; then
local compositor_cmd="$NESTRI_LAUNCH_COMPOSITOR"
@@ -136,12 +129,17 @@ start_compositor() {
# Check if this is a gamescope command
if [[ "$compositor_cmd" == *"gamescope"* ]]; then
is_gamescope=true
if [[ -n "$launch_cmd" ]] && [[ "$compositor_cmd" != *" -- "* ]]; then
# If steam in launch command, enable gamescope integration via -e and enable mangohud
if [[ "$launch_cmd" == *"steam"* ]]; then
compositor_cmd+=" --mangoapp -e"
if [[ -n "$NESTRI_LAUNCH_CMD" ]] && [[ "$compositor_cmd" != *" -- "* ]]; then
# If steam in launch command, enable gamescope integration via -e
if [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then
compositor_cmd+=" -e"
fi
# If ld_preload is true, add env with LD_PRELOAD
if $do_ld_preload; then
compositor_cmd+=" -- env LD_PRELOAD='/usr/\$LIB/libvimputti_shim.so' bash -c $(printf %q "$NESTRI_LAUNCH_CMD")"
else
compositor_cmd+=" -- bash -c $(printf %q "$NESTRI_LAUNCH_CMD")"
fi
compositor_cmd+=" -- bash -c $(printf %q "$launch_cmd")"
fi
fi
@@ -187,9 +185,9 @@ start_compositor() {
WAYLAND_DISPLAY=wayland-0 wlr-randr --output "$OUTPUT_NAME" --custom-mode "$WIDTH"x"$HEIGHT"
log "Patched resolution with wlr-randr"
if [[ -n "$launch_cmd" ]]; then
log "Starting application: $launch_cmd"
WAYLAND_DISPLAY="$COMPOSITOR_SOCKET" bash -c "$launch_cmd" &
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
log "Starting application: $NESTRI_LAUNCH_CMD"
WAYLAND_DISPLAY="$COMPOSITOR_SOCKET" /bin/bash -c "$NESTRI_LAUNCH_CMD" &
APP_PID=$!
fi
else
@@ -202,9 +200,9 @@ start_compositor() {
log "Warning: Compositor socket not found after 15 seconds ($COMPOSITOR_SOCKET)"
else
# Launch standalone application if no compositor
if [[ -n "$launch_cmd" ]]; then
log "Starting standalone application: $launch_cmd"
WAYLAND_DISPLAY=wayland-1 bash -c "$launch_cmd" &
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
log "Starting application: $NESTRI_LAUNCH_CMD"
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
APP_PID=$!
else
log "No compositor or application configured"

View File

@@ -4,6 +4,8 @@ export USER=${NESTRI_USER}
export LANG=${NESTRI_LANG}
export HOME=${NESTRI_HOME}
export XDG_RUNTIME_DIR=${NESTRI_XDG_RUNTIME_DIR}
export XDG_SESSION_TYPE=x11
export DISPLAY=:0
# Causes some setups to break
export PROTON_NO_FSYNC=1
@@ -11,6 +13,3 @@ export PROTON_NO_FSYNC=1
# Make gstreamer GL elements work without display output (NVIDIA issue..)
export GST_GL_API=gles2
export GST_GL_WINDOW=surfaceless
# Gamescope does not respect MangoHud default config location
export MANGOHUD_CONFIGFILE=/etc/nestri/configs/MangoHud/MangoHud.conf

View File

@@ -18,7 +18,7 @@ autorestart=true
autostart=true
startretries=3
priority=3
nice=-2
nice=-10
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
[program:pipewire-pulse]
@@ -28,7 +28,7 @@ autorestart=true
autostart=true
startretries=3
priority=4
nice=-2
nice=-10
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
[program:wireplumber]
@@ -38,7 +38,7 @@ autorestart=true
autostart=true
startretries=3
priority=5
nice=-2
nice=-10
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
[program:vimputti-manager]

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@ prost = "0.14"
prost-types = "0.14"
parking_lot = "0.12"
byteorder = "1.5"
libp2p = { version = "0.56", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "autonat", "quic"] }
libp2p = { version = "0.56", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "websocket", "autonat"] }
libp2p-identify = "0.47"
libp2p-ping = "0.47"
libp2p-autonat = { version = "0.15", features = ["v2"] }
@@ -37,7 +37,7 @@ libp2p-yamux = "0.47"
libp2p-noise = "0.46"
libp2p-dns = { version = "0.44", features = ["tokio"] }
libp2p-tcp = { version = "0.44", features = ["tokio"] }
libp2p-quic = { version = "0.13", features = ["tokio"] }
libp2p-websocket = "0.45"
dashmap = "6.1"
anyhow = "1.0"
unsigned-varint = "0.8"

View File

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

View File

@@ -122,14 +122,6 @@ impl Args {
.value_parser(value_parser!(encoding_args::RateControlMethod))
.default_value("cbr"),
)
.arg(
Arg::new("video-latency-control")
.long("video-latency-control")
.env("VIDEO_LATENCY_CONTROL")
.help("Video latency control")
.value_parser(value_parser!(encoding_args::LatencyControl))
.default_value("lowest-latency"),
)
.arg(
Arg::new("video-cqp")
.long("video-cqp")
@@ -173,14 +165,6 @@ impl Args {
)
.default_value("8"),
)
.arg(
Arg::new("keyframe-dist-secs")
.long("keyframe-dist-secs")
.env("KEYFRAME_DIST_SECS")
.help("Distance between keyframes in seconds")
.value_parser(value_parser!(u32).range(1..))
.default_value("1"),
)
.arg(
Arg::new("audio-capture-method")
.long("audio-capture-method")
@@ -211,14 +195,6 @@ impl Args {
.value_parser(value_parser!(encoding_args::RateControlMethod))
.default_value("cbr"),
)
.arg(
Arg::new("audio-latency-control")
.long("audio-latency-control")
.env("AUDIO_LATENCY_CONTROL")
.help("Audio latency control")
.value_parser(value_parser!(encoding_args::LatencyControl))
.default_value("lowest-latency"),
)
.arg(
Arg::new("audio-bitrate")
.long("audio-bitrate")

View File

@@ -60,12 +60,6 @@ pub enum RateControl {
CBR(RateControlCBR),
}
#[derive(Debug, PartialEq, Eq, Clone, ValueEnum)]
pub enum LatencyControl {
LowestLatency,
HighestQuality,
}
pub struct EncodingOptionsBase {
/// Codec (e.g. "h264", "opus" etc.)
pub codec: Codec,
@@ -73,8 +67,6 @@ pub struct EncodingOptionsBase {
pub encoder: Option<String>,
/// Rate control method (e.g. "cqp", "vbr", "cbr")
pub rate_control: RateControl,
/// Latency control option, what to tweak settings towards (latency or quality)
pub latency_control: LatencyControl,
}
impl EncodingOptionsBase {
pub fn debug_print(&self) {
@@ -95,14 +87,6 @@ impl EncodingOptionsBase {
tracing::info!("-> Target Bitrate: {}", cbr.target_bitrate);
}
}
match &self.latency_control {
LatencyControl::LowestLatency => {
tracing::info!("> Latency Control: Priorizing lowest latency");
}
LatencyControl::HighestQuality => {
tracing::info!("> Latency Control: Priorizing quality at the cost of latency");
}
}
}
}
@@ -110,7 +94,6 @@ pub struct VideoEncodingOptions {
pub base: EncodingOptionsBase,
pub encoder_type: EncoderType,
pub bit_depth: u32,
pub keyframe_dist_secs: u32,
}
impl VideoEncodingOptions {
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
@@ -142,10 +125,6 @@ impl VideoEncodingOptions {
max_bitrate: matches.get_one::<u32>("video-bitrate-max").unwrap().clone(),
}),
},
latency_control: matches
.get_one::<LatencyControl>("video-latency-control")
.unwrap_or(&LatencyControl::LowestLatency)
.clone(),
},
encoder_type: matches
.get_one::<EncoderType>("video-encoder-type")
@@ -155,10 +134,6 @@ impl VideoEncodingOptions {
.get_one::<u32>("video-bit-depth")
.copied()
.unwrap_or(8),
keyframe_dist_secs: matches
.get_one::<u32>("keyframe-dist-secs")
.copied()
.unwrap_or(1),
}
}
@@ -167,7 +142,6 @@ impl VideoEncodingOptions {
self.base.debug_print();
tracing::info!("> Encoder Type: {}", self.encoder_type.as_str());
tracing::info!("> Bit Depth: {}", self.bit_depth);
tracing::info!("> Keyframe Distance Seconds: {}", self.keyframe_dist_secs);
}
}
impl Deref for VideoEncodingOptions {
@@ -234,10 +208,6 @@ impl AudioEncodingOptions {
}),
wot => panic!("Invalid rate control method for audio: {}", wot.as_str()),
},
latency_control: matches
.get_one::<LatencyControl>("audio-latency-control")
.unwrap_or(&LatencyControl::LowestLatency)
.clone(),
},
capture_method: matches
.get_one::<AudioCaptureMethod>("audio-capture-method")

View File

@@ -74,6 +74,7 @@ pub enum EncoderAPI {
QSV,
VAAPI,
NVENC,
AMF,
SOFTWARE,
UNKNOWN,
}
@@ -84,6 +85,7 @@ impl EncoderAPI {
Self::QSV => "Intel QuickSync Video",
Self::VAAPI => "Video Acceleration API",
Self::NVENC => "NVIDIA NVENC",
Self::AMF => "AMD Media Framework",
Self::SOFTWARE => "Software",
Self::UNKNOWN => "Unknown",
}
@@ -165,6 +167,8 @@ fn get_encoder_api(encoder: &str, encoder_type: &EncoderType) -> EncoderAPI {
EncoderAPI::VAAPI
} else if encoder.starts_with("nv") {
EncoderAPI::NVENC
} else if encoder.starts_with("amf") {
EncoderAPI::AMF
} else {
EncoderAPI::UNKNOWN
}
@@ -271,9 +275,9 @@ pub fn encoder_low_latency_params(
encoder: &VideoEncoderInfo,
_rate_control: &RateControl,
framerate: u32,
keyframe_dist_secs: u32,
) -> VideoEncoderInfo {
let mut encoder_optz = encoder_gop_params(encoder, framerate * keyframe_dist_secs);
// 1 second keyframe interval for fast recovery, is this too taxing?
let mut encoder_optz = encoder_gop_params(encoder, framerate);
match encoder_optz.encoder_api {
EncoderAPI::QSV => {
@@ -289,6 +293,16 @@ pub fn encoder_low_latency_params(
encoder_optz.set_parameter("tune", "ultra-low-latency");
encoder_optz.set_parameter("zerolatency", "true");
}
EncoderAPI::AMF => {
encoder_optz.set_parameter("preset", "speed");
let usage = match encoder_optz.codec {
VideoCodec::H264 | VideoCodec::H265 => "ultra-low-latency",
VideoCodec::AV1 => "low-latency",
};
if !usage.is_empty() {
encoder_optz.set_parameter("usage", usage);
}
}
EncoderAPI::SOFTWARE => match encoder_optz.name.as_str() {
"openh264enc" => {
encoder_optz.set_parameter("complexity", "low");
@@ -316,56 +330,6 @@ pub fn encoder_low_latency_params(
encoder_optz
}
pub fn encoder_high_quality_params(
encoder: &VideoEncoderInfo,
_rate_control: &RateControl,
framerate: u32,
keyframe_dist_secs: u32,
) -> VideoEncoderInfo {
let mut encoder_optz = encoder_gop_params(encoder, framerate * keyframe_dist_secs);
match encoder_optz.encoder_api {
EncoderAPI::QSV => {
encoder_optz.set_parameter("low-latency", "false");
encoder_optz.set_parameter("target-usage", "1");
}
EncoderAPI::VAAPI => {
encoder_optz.set_parameter("target-usage", "1");
}
EncoderAPI::NVENC => {
encoder_optz.set_parameter("multi-pass", "two-pass");
encoder_optz.set_parameter("preset", "p7");
encoder_optz.set_parameter("tune", "high-quality");
encoder_optz.set_parameter("zerolatency", "false");
encoder_optz.set_parameter("spatial-aq", "true");
encoder_optz.set_parameter("rc-lookahead", "3");
}
EncoderAPI::SOFTWARE => match encoder_optz.name.as_str() {
"openh264enc" => {
encoder_optz.set_parameter("complexity", "high");
encoder_optz.set_parameter("usage-type", "screen");
}
"x264enc" => {
encoder_optz.set_parameter("rc-lookahead", "3");
encoder_optz.set_parameter("speed-preset", "medium");
}
"svtav1enc" => {
encoder_optz.set_parameter("preset", "8");
encoder_optz.set_parameter("parameters-string", "lookahead=3");
}
"av1enc" => {
encoder_optz.set_parameter("usage-profile", "realtime");
encoder_optz.set_parameter("cpu-used", "8");
encoder_optz.set_parameter("lag-in-frames", "3");
}
_ => {}
},
_ => {}
}
encoder_optz
}
pub fn get_compatible_encoders(gpus: &Vec<GPUInfo>) -> Vec<VideoEncoderInfo> {
let mut encoders = Vec::new();
let registry = gstreamer::Registry::get();
@@ -463,6 +427,16 @@ pub fn get_compatible_encoders(gpus: &Vec<GPUInfo>) -> Vec<VideoEncoderInfo> {
None
}
}
EncoderAPI::AMF if element.has_property("device") => {
let device_id = match element.property_value("device").get::<u32>() {
Ok(v) => Some(v as usize),
Err(_) => None,
};
device_id.and_then(|id| {
get_gpus_by_vendor(&gpus, GPUVendor::AMD).get(id).cloned()
})
}
_ => None,
}
})
@@ -575,6 +549,7 @@ pub fn get_best_compatible_encoder(
score += match encoder.encoder_api {
EncoderAPI::NVENC => 3,
EncoderAPI::QSV => 3,
EncoderAPI::AMF => 3,
EncoderAPI::VAAPI => 2,
EncoderAPI::SOFTWARE => 1,
EncoderAPI::UNKNOWN => 0,

View File

@@ -8,7 +8,6 @@ mod p2p;
mod proto;
use crate::args::encoding_args;
use crate::args::encoding_args::LatencyControl;
use crate::enc_helper::{EncoderAPI, EncoderType};
use crate::gpu::{GPUInfo, GPUVendor};
use crate::input::controller::ControllerManager;
@@ -131,20 +130,11 @@ fn handle_encoder_video_settings(
args: &args::Args,
video_encoder: &enc_helper::VideoEncoderInfo,
) -> enc_helper::VideoEncoderInfo {
let mut optimized_encoder = match args.encoding.video.latency_control {
LatencyControl::LowestLatency => enc_helper::encoder_low_latency_params(
let mut optimized_encoder = enc_helper::encoder_low_latency_params(
&video_encoder,
&args.encoding.video.rate_control,
args.app.framerate,
args.encoding.video.keyframe_dist_secs,
),
LatencyControl::HighestQuality => enc_helper::encoder_high_quality_params(
&video_encoder,
&args.encoding.video.rate_control,
args.app.framerate,
args.encoding.video.keyframe_dist_secs,
),
};
);
// Handle rate-control method
match &args.encoding.video.rate_control {
encoding_args::RateControl::CQP(cqp) => {
@@ -439,34 +429,39 @@ async fn main() -> Result<(), Box<dyn Error>> {
webrtcsink.set_property("do-retransmission", false);
/* Queues */
// Sink queues
let video_sink_queue = gstreamer::ElementFactory::make("queue").build()?;
let audio_sink_queue = gstreamer::ElementFactory::make("queue").build()?;
// Source queues
let video_source_queue = gstreamer::ElementFactory::make("queue")
let video_queue = gstreamer::ElementFactory::make("queue")
.property("max-size-buffers", 2u32)
.property("max-size-time", 0u64)
.property("max-size-bytes", 0u32)
.build()?;
let audio_source_queue = gstreamer::ElementFactory::make("queue")
let audio_queue = gstreamer::ElementFactory::make("queue")
.property("max-size-buffers", 2u32)
.property("max-size-time", 0u64)
.property("max-size-bytes", 0u32)
.build()?;
/* Clock Sync */
let video_clocksync = gstreamer::ElementFactory::make("clocksync")
.property("sync-to-first", true)
.build()?;
let audio_clocksync = gstreamer::ElementFactory::make("clocksync")
.property("sync-to-first", true)
.build()?;
// Add elements to the pipeline
pipeline.add_many(&[
webrtcsink.upcast_ref(),
&video_sink_queue,
&audio_sink_queue,
&video_encoder,
&caps_filter,
&video_source_queue,
&video_queue,
&video_clocksync,
&video_source,
&audio_encoder,
&audio_capsfilter,
&audio_source_queue,
&audio_queue,
&audio_clocksync,
&audio_rate,
&audio_converter,
&audio_source,
@@ -498,24 +493,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
&audio_converter,
&audio_rate,
&audio_capsfilter,
&audio_source_queue,
&audio_queue,
&audio_clocksync,
&audio_encoder,
])?;
// Link audio parser to audio encoder if present, otherwise just webrtcsink
if let Some(parser) = &audio_parser {
gstreamer::Element::link_many(&[
&audio_encoder,
parser,
&audio_sink_queue,
webrtcsink.upcast_ref(),
])?;
gstreamer::Element::link_many(&[&audio_encoder, parser, webrtcsink.upcast_ref()])?;
} else {
gstreamer::Element::link_many(&[
&audio_encoder,
&audio_sink_queue,
webrtcsink.upcast_ref(),
])?;
gstreamer::Element::link_many(&[&audio_encoder, webrtcsink.upcast_ref()])?;
}
// With zero-copy..
@@ -525,20 +512,26 @@ async fn main() -> Result<(), Box<dyn Error>> {
gstreamer::Element::link_many(&[
&video_source,
&caps_filter,
&video_source_queue,
&video_queue,
&video_clocksync,
&vapostproc,
&va_caps_filter,
&video_encoder,
])?;
} else if video_encoder_info.encoder_api == EncoderAPI::NVENC {
// NVENC pipeline
gstreamer::Element::link_many(&[&video_source, &caps_filter, &video_encoder])?;
gstreamer::Element::link_many(&[
&video_source,
&caps_filter,
&video_encoder,
])?;
}
} else {
gstreamer::Element::link_many(&[
&video_source,
&caps_filter,
&video_source_queue,
&video_queue,
&video_clocksync,
&video_converter.unwrap(),
&video_encoder,
])?;
@@ -546,24 +539,21 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Link video parser if present with webrtcsink, otherwise just link webrtc sink
if let Some(parser) = &video_parser {
gstreamer::Element::link_many(&[
&video_encoder,
parser,
&video_sink_queue,
webrtcsink.upcast_ref(),
])?;
gstreamer::Element::link_many(&[&video_encoder, parser, webrtcsink.upcast_ref()])?;
} else {
gstreamer::Element::link_many(&[
&video_encoder,
&video_sink_queue,
webrtcsink.upcast_ref(),
])?;
gstreamer::Element::link_many(&[&video_encoder, webrtcsink.upcast_ref()])?;
}
video_source.set_property("do-timestamp", &false);
audio_source.set_property("do-timestamp", &false);
// Make sure QOS is disabled to avoid latency
video_encoder.set_property("qos", true);
// Optimize latency of pipeline
video_source
.sync_state_with_parent()
.expect("failed to sync with parent");
video_source.set_property("do-timestamp", &true);
audio_source.set_property("do-timestamp", &true);
pipeline.set_property("latency", &0u64);
pipeline.set_property("async-handling", true);
pipeline.set_property("message-forward", true);

View File

@@ -55,8 +55,9 @@ impl NestriP2P {
noise::Config::new,
yamux::Config::default,
)?
.with_quic()
.with_dns()?
.with_websocket(noise::Config::new, yamux::Config::default)
.await?
.with_behaviour(|key| NestriBehaviour::new(key.public()))?
.build(),
));

View File

@@ -1,12 +1,14 @@
// @generated
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoTimestampEntry {
#[prost(string, tag="1")]
pub stage: ::prost::alloc::string::String,
#[prost(message, optional, tag="2")]
pub time: ::core::option::Option<::prost_types::Timestamp>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoLatencyTracker {
#[prost(string, tag="1")]
@@ -17,7 +19,8 @@ pub struct ProtoLatencyTracker {
// Mouse messages
/// MouseMove message
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseMove {
#[prost(int32, tag="1")]
pub x: i32,
@@ -25,7 +28,8 @@ pub struct ProtoMouseMove {
pub y: i32,
}
/// MouseMoveAbs message
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseMoveAbs {
#[prost(int32, tag="1")]
pub x: i32,
@@ -33,7 +37,8 @@ pub struct ProtoMouseMoveAbs {
pub y: i32,
}
/// MouseWheel message
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseWheel {
#[prost(int32, tag="1")]
pub x: i32,
@@ -41,13 +46,15 @@ pub struct ProtoMouseWheel {
pub y: i32,
}
/// MouseKeyDown message
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyDown {
#[prost(int32, tag="1")]
pub key: i32,
}
/// MouseKeyUp message
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyUp {
#[prost(int32, tag="1")]
pub key: i32,
@@ -55,26 +62,24 @@ pub struct ProtoMouseKeyUp {
// Keyboard messages
/// KeyDown message
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoKeyDown {
#[prost(int32, tag="1")]
pub key: i32,
}
/// KeyUp message
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct ProtoKeyUp {
#[prost(int32, tag="1")]
pub key: i32,
}
// Clipboard message
// message ProtoClipboard {
// string content = 1; // Clipboard content
// }
// Controller messages
/// ControllerAttach message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerAttach {
/// One of the following enums: "ps", "xbox" or "switch"
#[prost(string, tag="1")]
@@ -87,7 +92,8 @@ pub struct ProtoControllerAttach {
pub session_id: ::prost::alloc::string::String,
}
/// ControllerDetach message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerDetach {
/// Session specific slot number (0-3)
#[prost(int32, tag="1")]
@@ -97,7 +103,8 @@ pub struct ProtoControllerDetach {
pub session_id: ::prost::alloc::string::String,
}
/// ControllerRumble message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerRumble {
/// Session specific slot number (0-3)
#[prost(int32, tag="1")]
@@ -116,6 +123,7 @@ pub struct ProtoControllerRumble {
pub duration: i32,
}
/// ControllerStateBatch - single message containing full or partial controller state
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerStateBatch {
/// Session specific slot number (0-3)
@@ -180,8 +188,8 @@ pub mod proto_controller_state_batch {
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::FullState => "FULL_STATE",
Self::Delta => "DELTA",
UpdateType::FullState => "FULL_STATE",
UpdateType::Delta => "DELTA",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -196,7 +204,8 @@ pub mod proto_controller_state_batch {
}
// WebRTC + signaling
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RtcIceCandidateInit {
#[prost(string, tag="1")]
pub candidate: ::prost::alloc::string::String,
@@ -207,7 +216,8 @@ pub struct RtcIceCandidateInit {
#[prost(string, optional, tag="4")]
pub username_fragment: ::core::option::Option<::prost::alloc::string::String>,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RtcSessionDescriptionInit {
#[prost(string, tag="1")]
pub sdp: ::prost::alloc::string::String,
@@ -215,25 +225,29 @@ pub struct RtcSessionDescriptionInit {
pub r#type: ::prost::alloc::string::String,
}
/// ProtoICE message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoIce {
#[prost(message, optional, tag="1")]
pub candidate: ::core::option::Option<RtcIceCandidateInit>,
}
/// ProtoSDP message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoSdp {
#[prost(message, optional, tag="1")]
pub sdp: ::core::option::Option<RtcSessionDescriptionInit>,
}
/// ProtoRaw message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoRaw {
#[prost(string, tag="1")]
pub data: ::prost::alloc::string::String,
}
/// ProtoClientRequestRoomStream message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoClientRequestRoomStream {
#[prost(string, tag="1")]
pub room_name: ::prost::alloc::string::String,
@@ -241,7 +255,8 @@ pub struct ProtoClientRequestRoomStream {
pub session_id: ::prost::alloc::string::String,
}
/// ProtoClientDisconnected message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoClientDisconnected {
#[prost(string, tag="1")]
pub session_id: ::prost::alloc::string::String,
@@ -249,11 +264,13 @@ pub struct ProtoClientDisconnected {
pub controller_slots: ::prost::alloc::vec::Vec<i32>,
}
/// ProtoServerPushStream message
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoServerPushStream {
#[prost(string, tag="1")]
pub room_name: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessageBase {
#[prost(string, tag="1")]
@@ -261,6 +278,7 @@ pub struct ProtoMessageBase {
#[prost(message, optional, tag="2")]
pub latency: ::core::option::Option<ProtoLatencyTracker>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessage {
#[prost(message, optional, tag="1")]
@@ -270,6 +288,7 @@ pub struct ProtoMessage {
}
/// Nested message and enum types in `ProtoMessage`.
pub mod proto_message {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum Payload {
/// Input types
@@ -285,7 +304,6 @@ pub mod proto_message {
MouseKeyUp(super::ProtoMouseKeyUp),
#[prost(message, tag="7")]
KeyDown(super::ProtoKeyDown),
/// ProtoClipboard clipboard = 9;
#[prost(message, tag="8")]
KeyUp(super::ProtoKeyUp),
/// Controller input types