5 Commits

Author SHA1 Message Date
Philipp Neumann
d3bc1d17e2 Merge d501b66c11 into 32341574dc 2025-11-01 13:47:34 +01:00
DatCaptainHorse
32341574dc fix: Disable v4 runner build in GHA workflow
- Turns out GHA doesn't use AVX-512 CPUs..
2025-10-20 20:04:01 +03:00
Kristian Ollikainen
c62a22b552 feat: Controller support, performance enchancements, multi-stage images, fixes (#304)
## Description
Oops.. another massive PR 🥲 

This PR contains multiple improvements and changes.

Firstly, thanks gst-wayland-display's PR
[here](https://github.com/games-on-whales/gst-wayland-display/pull/20).
NVIDIA path is now way more efficient than before.

Secondly, adding controller support was a massive hurdle, requiring me
to start another project
[vimputti](https://github.com/DatCaptainHorse/vimputti) - which allows
simple virtual controller inputs in isolated containers. Well, it's not
simple, it includes LD_PRELOAD shims and other craziness, but the
library API is simple to use..

Thirdly, split runner image into 3 separate stages, base + build +
runtime, should help keep things in check in future, also added GitHub
Actions CI builds for v2 to v4 builds (hopefully they pass..).

Fourth, replaced the runner's runtime Steam patching with better and
simpler bubblewrap patch, massive thanks to `games-on-whales` to
figuring it out better!

Fifth, relay for once needed some changes, the new changes are still
mostly WIP, but I'll deal with them next time I have energy.. I'm spent
now. Needed to include these changes as relay needed a minor change to
allow rumble events to flow back to client peer.

Sixth.. tons of package updates, minor code improvements and the usual. 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* End-to-end gamepad/controller support (attach/detach, buttons, sticks,
triggers, rumble) with client/server integration and virtual controller
plumbing.
  * Optional Prometheus metrics endpoint and WebTransport support.
  * Background vimputti manager process added for controller handling.

* **Improvements**
  * Multi-variant container image builds and streamlined runtime images.
  * Zero-copy video pipeline and encoder improvements for lower latency.
  * Updated Steam compat mapping and dependency/toolchain refreshes.

* **Bug Fixes**
* More robust GPU detection, input/fullscreen lifecycle,
startup/entrypoint, and container runtime fixes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
2025-10-20 11:20:05 +03:00
Philipp Neumann
d501b66c11 latency test blog entry 2025-05-28 16:49:45 +02:00
Philipp Neumann
dc1b552ac1 added initial blog tryings 2025-05-17 20:39:14 +02:00
101 changed files with 6520 additions and 2278 deletions

4
.github/CODEOWNERS vendored
View File

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

48
.github/workflows/docker-bake.hcl vendored Normal file
View File

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

View File

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

24
apps/blog/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
apps/blog/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"],
"unwantedRecommendations": []
}

11
apps/blog/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

68
apps/blog/README.md Normal file
View File

@@ -0,0 +1,68 @@
# Astro Starter Kit: Blog
```sh
bun create astro@latest -- --template blog
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/blog)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/blog)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/blog/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![blog](https://github.com/withastro/astro/assets/2244813/ff10799f-a816-4703-b967-c78997e8323d)
Features:
- ✅ Minimal styling (make it your own!)
- ✅ 100/100 Lighthouse performance
- ✅ SEO-friendly with canonical URLs and OpenGraph data
- ✅ Sitemap support
- ✅ RSS Feed support
- ✅ Markdown & MDX support
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
├── public/
├── src/
│   ├── components/
│   ├── content/
│   ├── layouts/
│   └── pages/
├── astro.config.mjs
├── README.md
├── package.json
└── tsconfig.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
The `src/content/` directory contains "collections" of related Markdown and MDX documents. Use `getCollection()` to retrieve posts from `src/content/blog/`, and type-check your frontmatter using an optional schema. See [Astro's Content Collections docs](https://docs.astro.build/en/guides/content-collections/) to learn more.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `bun install` | Installs dependencies |
| `bun dev` | Starts local dev server at `localhost:4321` |
| `bun build` | Build your production site to `./dist/` |
| `bun preview` | Preview your build locally, before deploying |
| `bun astro ...` | Run CLI commands like `astro add`, `astro check` |
| `bun astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
## Credit
This theme is based off of the lovely [Bear Blog](https://github.com/HermanMartinus/bearblog/).

View File

@@ -0,0 +1,18 @@
// @ts-check
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import solidJs from '@astrojs/solid-js';
import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({
site: 'https://example.com',
integrations: [mdx(), sitemap(), solidJs()],
vite: {
plugins: [tailwindcss()],
},
});

1022
apps/blog/bun.lock Normal file

File diff suppressed because it is too large Load Diff

21
apps/blog/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.2.6",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.4.0",
"@astrojs/solid-js": "^5.0.10",
"@tailwindcss/vite": "^4.1.7",
"astro": "^5.7.13",
"solid-js": "^1.9.7",
"tailwindcss": "^4.1.7"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -0,0 +1,55 @@
---
// Import the global.css file here so that it is included on
// all pages through the use of the <BaseHead /> component.
import '../styles/global.css';
import { SITE_TITLE } from '../consts';
interface Props {
title: string;
description: string;
image?: string;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
---
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="sitemap" href="/sitemap-index.xml" />
<link
rel="alternate"
type="application/rss+xml"
title={SITE_TITLE}
href={new URL('rss.xml', Astro.site)}
/>
<meta name="generator" content={Astro.generator} />
<!-- Font preloads -->
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />

View File

@@ -0,0 +1,53 @@
---
import "../styles/global.css"
const today = new Date();
---
<footer>
<div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400">
<span class="hover:text-primary-500 transition-colors duration-200">
<a rel="noreferrer" href="https://nestri.io/terms" >Terms of Service</a></span>
<span class="text-gray-400 dark:text-gray-600">•</span>
<span class="hover:text-primary-500 transition-colors duration-200">
<a href="https://nestri.io/privacy">Privacy Policy</a>
</span>
</div>
<div class="mt-6 w-full justify-center flex items-center space-x-4">
<a href="https://discord.gg/6um5K6jrYj" target="_blank">
<span class="sr-only">Join our Discord Server</span>
<svg width="59" height="44" viewBox="0 0 59 44" aria-hidden="true" astro-icon="social/discord" style="height:28px">
<path d="M37.1937 0C36.6265 1.0071 36.1172 2.04893 35.6541 3.11392C31.2553 2.45409 26.7754 2.45409 22.365 3.11392C21.9136 2.04893 21.3926 1.0071 20.8254 0C16.6928 0.70613 12.6644 1.94475 8.84436 3.69271C1.27372 14.9098 -0.775214 25.8374 0.243466 36.6146C4.67704 39.8906 9.6431 42.391 14.9333 43.9884C16.1256 42.391 17.179 40.6893 18.0819 38.9182C16.3687 38.2815 14.7133 37.4828 13.1274 36.5567C13.5442 36.2557 13.9493 35.9432 14.3429 35.6422C23.6384 40.0179 34.4039 40.0179 43.711 35.6422C44.1046 35.9663 44.5097 36.2789 44.9264 36.5567C43.3405 37.4943 41.6852 38.2815 39.9604 38.9298C40.8633 40.7009 41.9167 42.4025 43.109 44C48.3992 42.4025 53.3653 39.9137 57.7988 36.6377C59.0027 24.1358 55.7383 13.3007 49.1748 3.70429C45.3663 1.95633 41.3379 0.717706 37.2053 0.0231518L37.1937 0ZM19.3784 29.9816C16.5192 29.9816 14.1461 27.3886 14.1461 24.1821C14.1461 20.9755 16.4266 18.371 19.3669 18.371C22.3071 18.371 24.6455 20.9871 24.5992 24.1821C24.5529 27.377 22.2956 29.9816 19.3784 29.9816ZM38.6639 29.9816C35.7931 29.9816 33.4431 27.3886 33.4431 24.1821C33.4431 20.9755 35.7236 18.371 38.6639 18.371C41.6042 18.371 43.9309 20.9871 43.8846 24.1821C43.8383 27.377 41.581 29.9816 38.6639 29.9816Z" fill="white"/>
</svg>
</a>
<a href="https://github.com/nestrilabs/nestri/" target="_blank">
<span class="sr-only">Go to Nestri's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
</a>
</div>
</footer>
<style>
footer {
padding: 2em 1em 6em 1em;
background: linear-gradient(var(--gray-gradient)) no-repeat;
color: rgb(var(--gray));
text-align: center;
}
.social-links {
display: flex;
justify-content: center;
gap: 1em;
margin-top: 1em;
}
.social-links a {
text-decoration: none;
color: rgb(var(--gray));
}
.social-links a:hover {
color: rgb(var(--gray-dark));
}
</style>

View File

@@ -0,0 +1,17 @@
---
interface Props {
date: Date;
}
const { date } = Astro.props;
---
<time datetime={date.toISOString()}>
{
date.toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
</time>

View File

@@ -0,0 +1,57 @@
---
import HeaderLink from './HeaderLink.astro';
import { SITE_TITLE } from '../consts';
import "../styles/global.css";
---
<header>
<nav>
<h2><a href="/">{SITE_TITLE}</a></h2>
<div class="internal-links">
<HeaderLink href="https://nestri.io/">Nestri Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="https://nestri.io/about">About us</HeaderLink>
</div>
</nav>
</header>
<style>
header {
margin: 0;
padding: 0 1em;
border-bottom: solid;
box-: 0 2px 8px rgba(var(--black), 5%);
}
h2 {
margin: 0;
font-size: 1em;
}
h2 a,
h2 a.active {
text-decoration: none;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
}
nav a {
padding: 1em 0.5em;
color: var(--black);
border-bottom: 4px solid transparent;
text-decoration: none;
}
nav a.active {
text-decoration: none;
border-bottom-color: var(--accent);
}
.social-links,
.social-links a {
display: flex;
}
@media (max-width: 720px) {
.social-links {
display: none;
}
}
</style>

View File

@@ -0,0 +1,24 @@
---
import type { HTMLAttributes } from 'astro/types';
type Props = HTMLAttributes<'a'>;
const { href, class: className, ...props } = Astro.props;
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '');
const subpath = pathname.match(/[^\/]+/g);
const isActive = href === pathname || href === '/' + (subpath?.[0] || '');
---
<a href={href} class:list={[className, { active: isActive }]} {...props}>
<slot />
</a>
<style>
a {
display: inline-block;
text-decoration: none;
}
a.active {
font-weight: bolder;
text-decoration: underline;
}
</style>

5
apps/blog/src/consts.ts Normal file
View File

@@ -0,0 +1,5 @@
// Place any global data in this file.
// You can import this data from anywhere in your site by using the `import` keyword.
export const SITE_TITLE = 'Nestri Blog';
export const SITE_DESCRIPTION = 'Welcome to Nestri\'s Blog - This Blog is about the current status of and about intresting facts about Nestri';

View File

@@ -0,0 +1,18 @@
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
// Load Markdown and MDX files in the `src/content/blog/` directory.
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
// Type-check frontmatter using a schema
schema: z.object({
title: z.string(),
description: z.string(),
// Transform string to Date object
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
}),
});
export const collections = { blog };

View File

@@ -0,0 +1,16 @@
---
title: 'First post'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Jul 08 2022'
heroImage: '/blog-placeholder-3.jpg'
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.

View File

@@ -0,0 +1,65 @@
---
title: 'Technical Deep Dive into Latency'
description: "Why It's High and How to Reduce It"
pubDate: 'May 18 2025'
heroImage: '/pexels-brett-sayles-2881224.jpg'
---
### Why It's High and How to Reduce It
First, let's start with the basics of the Internet.
The Internet connects clients and servers. Webpages primarily use the Application Layer protocol HTTP(S) to communicate with servers. HTTP is widely adopted for various applications, including mobile apps and other services requiring server communication.
There are also other client protocols like WebRTC (Web Real-Time Communication), which mainly powers streaming services needing a back channel. Nestri utilizes WebRTC, and we'll delve deeper into that later.
Imagine using a client protocol like WebRTC to send messages. Common formats for these messages include XML, HTML, or JSON.
While HTML contains significant duplicate symbols (e.g., `<a href="example.com">Some Link</a> <a href="example.com/subpage">Some nested Link</a>`), the modern web employs techniques to reduce its size. For instance, using modern zipping algorithms like gzip, this data can be compressed, resulting in a smaller size for transmission over the HTTP protocol.
In computer science, the more dense the information in a message (achieved through compression, for example), the higher its message entropy. Therefore, sending messages with high entropy is beneficial as it allows for the transfer of more information in a smaller package. Pure HTTP has relatively low entropy, similar to XML. JSON offers higher entropy, which can be further increased by removing whitespace and shortening attribute names. However, in modern client-server applications, JSON is often compressed.
So, we compress JSON traffic for efficiency. Have you ever compressed a large file? Modern systems make this process incredibly fast! But this requires computing power on both the client and server sides, which directly influences latency.
"Well, if I have a fiber connection, I don't need to worry about that..."
While a fiber connection offers significant bandwidth, this statement is somewhat misleading.
Latency also depends on your local network. A modern and stable Wi-Fi connection might seem sufficient, but the physical layer of the internet also contributes to latency. Wireless protocols, in particular, operate on a shared medium the air. This medium is utilized by various devices, commonly on frequencies around 2.4 or 5 GHz. This spectrum is divided among all these devices. Mechanisms like scheduling and signal modulation are used to manage this shared resource. In essence, to avoid a deeper dive into wireless communication, a wired connection is generally superior to a wireless connection due to the absence of a shared physical medium.
Okay, but what about Ethernet or fiber cables? Aren't we sharing those as well, with multiple applications or other internet users?
Yes, this also impacts latency. If many users in your local area are utilizing the same uplinks to a backbone (a high-speed part of the internet), you'll have to share that bandwidth. While fiber optic cables have substantial capacity due to advanced modulation techniques, consider the journey these data packets undertake across the internet.
Sometimes, if a data center is located nearby, your connection will involve fewer routers (fewer hops) between you and the server. Fewer hops generally translate to lower latency. Each router needs to queue your messages and determine the next destination. Modern routing protocols facilitate this process. However, even routers have to process messages in their queues. Thus, higher message entropy means fewer or smaller packets need to be sent.
What happens when your messages are too large for transmission? They are split into multiple parts and sent using protocols like TCP. TCP ensures reliable packet exchange by retransmitting any packets that are likely lost during internet transit. Packet loss can occur if a router's queue overflows, forcing it to drop packets, potentially prioritizing other traffic. This retransmission significantly increases latency as a packet might need to be sent multiple times.
UDP offers a different approach: it sends all packets without the overhead of retransmission. In this case, the application protocol is responsible for handling any lost packets. Fortunately, there's an application protocol that manages this quite effectively: WebRTC.
WebRTC is an open-source project providing APIs for real-time communication of audio, video, and generic data between peers via a browser. It leverages protocols like ICE, STUN, and TURN to handle NAT traversal and establish peer-to-peer connections, enabling low-latency media streaming and data exchange directly within web applications.
Sending raw video streams over WebRTC is inefficient; they require compression using modern codecs. A GPU is the optimal choice for this task because it has dedicated hardware (hardware encoder) to accelerate video encoding, significantly speeding up the process compared to software encoding on a CPU. Therefore, your GPU also plays a crucial role in reducing latency during video encoding and decoding.
So, why is all this relevant to Nestri?
We aim to deliver a cutting-edge, low-latency cloud gaming experience. Here's what we've implemented to combat bad latency:
**1. Reducing Mouse and Keyboard Latency**
1. Reduce package size by using the Protobuf protocol instead of JSON.
2. Avoid wasting compute power by not compressing these already optimized messages.
3. Minimize message flooding by bundling multiple mouse events into fewer messages through aggregation.
4. Implement all of this within WebRTC for a super lightweight communication over UDP.
**2. Reducing Video Latency**
1. Utilize cutting-edge encoder-decoders on a GPU instead of a CPU.
**3. Reducing Network Latency in the Backbone**
1. Bring servers closer to users to reduce the hop count.
Here's a glimpse of the results of these improvements, comparing the experience before and after implementation:
![[nestri footage video](/nestri-footage-latency.png)](https://fs.dathorse.com/w/ad2bee7e322b942491044fcffcccc899)
**Latency Test and comparison to the old Nestri**
Did you enjoy this blog post? Join our Discord and share your thoughts!

View File

@@ -0,0 +1,214 @@
---
title: 'Markdown Style Guide'
description: 'Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.'
pubDate: 'Jun 19 2024'
heroImage: '/blog-placeholder-1.jpg'
---
Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.
## Headings
The following HTML `<h1>``<h6>` elements represent six levels of section headings. `<h1>` is the highest section level while `<h6>` is the lowest.
# H1
## H2
### H3
#### H4
##### H5
###### H6
## Paragraph
Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat.
Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat.
## Images
### Syntax
```markdown
![Alt text](./full/or/relative/path/of/image)
```
### Output
![blog placeholder](/blog-placeholder-about.jpg)
## Blockquotes
The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations.
### Blockquote without attribution
#### Syntax
```markdown
> Tiam, ad mint andaepu dandae nostion secatur sequo quae.
> **Note** that you can use _Markdown syntax_ within a blockquote.
```
#### Output
> Tiam, ad mint andaepu dandae nostion secatur sequo quae.
> **Note** that you can use _Markdown syntax_ within a blockquote.
### Blockquote with attribution
#### Syntax
```markdown
> Don't communicate by sharing memory, share memory by communicating.<br>
> — <cite>Rob Pike[^1]</cite>
```
#### Output
> Don't communicate by sharing memory, share memory by communicating.<br>
> — <cite>Rob Pike[^1]</cite>
[^1]: The above quote is excerpted from Rob Pike's [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015.
## Tables
### Syntax
```markdown
| Italics | Bold | Code |
| --------- | -------- | ------ |
| _italics_ | **bold** | `code` |
```
### Output
| Italics | Bold | Code |
| --------- | -------- | ------ |
| _italics_ | **bold** | `code` |
## Code Blocks
### Syntax
we can use 3 backticks ``` in new line and write snippet and close with 3 backticks on new line and to highlight language specific syntax, write one word of language name after first 3 backticks, for eg. html, javascript, css, markdown, typescript, txt, bash
````markdown
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Example HTML5 Document</title>
</head>
<body>
<p>Test</p>
</body>
</html>
```
````
### Output
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Example HTML5 Document</title>
</head>
<body>
<p>Test</p>
</body>
</html>
```
## List Types
### Ordered List
#### Syntax
```markdown
1. First item
2. Second item
3. Third item
```
#### Output
1. First item
2. Second item
3. Third item
### Unordered List
#### Syntax
```markdown
- List item
- Another item
- And another item
```
#### Output
- List item
- Another item
- And another item
### Nested list
#### Syntax
```markdown
- Fruit
- Apple
- Orange
- Banana
- Dairy
- Milk
- Cheese
```
#### Output
- Fruit
- Apple
- Orange
- Banana
- Dairy
- Milk
- Cheese
## Other Elements — abbr, sub, sup, kbd, mark
### Syntax
```markdown
<abbr title="Graphics Interchange Format">GIF</abbr> is a bitmap image format.
H<sub>2</sub>O
X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>
Press <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>Delete</kbd> to end the session.
Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.
```
### Output
<abbr title="Graphics Interchange Format">GIF</abbr> is a bitmap image format.
H<sub>2</sub>O
X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>
Press <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>Delete</kbd> to end the session.
Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.

View File

@@ -0,0 +1,16 @@
---
title: 'Second post'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Jul 15 2022'
heroImage: '/blog-placeholder-4.jpg'
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.

View File

@@ -0,0 +1,16 @@
---
title: 'Third post'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Jul 22 2022'
heroImage: '/blog-placeholder-2.jpg'
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.

View File

@@ -0,0 +1,31 @@
---
title: 'Using MDX'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Jun 01 2024'
heroImage: '/blog-placeholder-5.jpg'
---
This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.
## Why MDX?
MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/markdown-content/#mdx-features) for things like interactive charts or alerts.
If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.
## Example
Here is how you import and use a UI component inside of MDX.
When you open this page in the browser, you should see the clickable button below.
import HeaderLink from '../../components/HeaderLink.astro';
<HeaderLink href="#" onclick="alert('clicked!')">
Embedded component in MDX
</HeaderLink>
## More Links
- [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx)
- [Astro Usage Documentation](https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages)
- **Note:** [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.

View File

@@ -0,0 +1,92 @@
---
import type { CollectionEntry } from 'astro:content';
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import FormattedDate from '../components/FormattedDate.astro';
import "../styles/global.css"
type Props = CollectionEntry<'blog'>['data'];
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
---
<html lang="en">
<head>
<BaseHead title={title} description={description} />
<style>
main {
width: calc(100% - 2em);
max-width: 100%;
margin: 0;
}
.hero-image {
width: 100%;
}
.hero-image img {
display: block;
margin: 0 auto;
border-radius: 12px;
}
.prose {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 1em;
color: rgb(var(--gray-dark));
}
.title {
margin-bottom: 1em;
padding: 1em 0;
text-align: center;
line-height: 1;
}
.title h1 {
margin: 0 0 0.5em 0;
}
.date {
margin-bottom: 0.5em;
color: rgb(var(--gray));
}
.last-updated-on {
font-style: italic;
}
</style>
</head>
<body>
<Header />
<main>
<article>
<div class="grid gap-8 items-start justify-center">
<div class="relative group">
<div class="absolute -inset-0.5 bg-radial-gradient opacity-40 group-hover:opacity-80 transition duration-1000 group-hover:duration-200 animate-tilt" />
<div class="relative bg-black rounded-lg leading-none flex items-center divide-x divide-gray-600">
{heroImage && <img width={1020} height={510} src={heroImage} alt="" />}
</div>
</div>
</div>
</div>
<div class="prose">
<div class="title">
<div class="date">
<FormattedDate date={pubDate} />
{
updatedDate && (
<div class="last-updated-on">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<h1>{title}</h1>
<hr />
</div>
<slot />
</div>
</article>
</main>
<Footer />
</body>
</html>

View File

@@ -0,0 +1,62 @@
---
import Layout from '../layouts/BlogPost.astro';
---
<Layout
title="About Me"
description="Lorem ipsum dolor sit amet"
pubDate={new Date('August 08 2021')}
heroImage="/blog-placeholder-about.jpg"
>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo
viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam
adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus
et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus
vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque
sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
</p>
<p>
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non
tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non
blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna
porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis
massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc.
Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis
bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra
massa massa ultricies mi.
</p>
<p>
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl
suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet
nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae
turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem
dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat
semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus
vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum
facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam
vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla
urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
</p>
<p>
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper
viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc
scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur
gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus
pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim
blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id
cursus metus aliquam eleifend mi.
</p>
<p>
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta
nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam
tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci
ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar
proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
</p>
</Layout>

View File

@@ -0,0 +1,21 @@
---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
import { render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
type Props = CollectionEntry<'blog'>;
const post = Astro.props;
const { Content } = await render(post);
---
<BlogPost {...post.data}>
<Content />
</BlogPost>

View File

@@ -0,0 +1,120 @@
---
import BaseHead from '../../components/BaseHead.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
import { getCollection } from 'astro:content';
import FormattedDate from '../../components/FormattedDate.astro';
import "../../styles/global.css"
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
<style>
main {
width: 960px;
}
ul {
display: flex;
flex-wrap: wrap;
gap: 2rem;
list-style-type: none;
margin: 0;
padding: 0;
}
ul li {
width: calc(50% - 1rem);
}
ul li * {
text-decoration: none;
transition: 0.2s ease;
}
ul li:first-child {
width: 100%;
margin-bottom: 1rem;
text-align: center;
}
ul li:first-child img {
width: 100%;
}
ul li:first-child .title {
font-size: 2.369rem;
}
ul li img {
}
ul li a {
display: block;
}
.title {
margin: 0;
color: #d9d9d9;
line-height: 1;
}
.date {
margin: 0;
color: #c0c0c0;
}
ul li a:hover h4,
ul li a:hover .date {
color: #f2f2f2;
}
ul a:hover img {
box-shadow: var(--box-shadow);
}
@media (max-width: 720px) {
ul {
gap: 0.5em;
}
ul li {
width: 100%;
text-align: center;
}
ul li:first-child {
margin-bottom: 0;
}
ul li:first-child .title {
font-size: 1.563em;
}
}
</style>
</head>
<body>
<Header />
<main>
<section>
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>
<div class="grid gap-8 items-start justify-center">
<div class="relative group">
<div class="absolute -inset-0.5 bg-radial-gradient opacity-0 group-hover:opacity-80 transition duration-1000 group-hover:duration-200 animate-tilt" />
<div class="relative bg-black rounded-lg leading-none flex items-center divide-x divide-gray-600">
<img width={720} height={360} src={post.data.heroImage} alt="" />
</div>
</div>
</div>
</div>
<h4 class="title py-4">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
</a>
</li>
))
}
</ul>
</section>
</main>
<Footer />
</body>
</html>

View File

@@ -0,0 +1,49 @@
---
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main>
<h1>🧑‍🚀 Hello, Astronaut!</h1>
<p>
Welcome to the official <a href="https://astro.build/">Astro</a> blog starter template. This
template serves as a lightweight, minimally-styled starting point for anyone looking to build
a personal website, blog, or portfolio with Astro.
</p>
<p>
This template comes with a few integrations already configured in your
<code>astro.config.mjs</code> file. You can customize your setup with
<a href="https://astro.build/integrations">Astro Integrations</a> to add tools like Tailwind,
React, or Vue to your project.
</p>
<p>Here are a few ideas on how to get started with the template:</p>
<ul>
<li>Edit this page in <code>src/pages/index.astro</code></li>
<li>Edit the site header items in <code>src/components/Header.astro</code></li>
<li>Add your name to the footer in <code>src/components/Footer.astro</code></li>
<li>Check out the included blog posts in <code>src/content/blog/</code></li>
<li>Customize the blog post page layout in <code>src/layouts/BlogPost.astro</code></li>
</ul>
<p>
Have fun! If you get stuck, remember to
<a href="https://docs.astro.build/">read the docs</a>
or <a href="https://astro.build/chat">join us on Discord</a> to ask questions.
</p>
<p>
Looking for a blog template with a bit more personality? Check out
<a href="https://github.com/Charca/astro-blog-template">astro-blog-template</a>
by <a href="https://twitter.com/Charca">Maxi Ferreira</a>.
</p>
</main>
<Footer />
</body>
</html>

View File

@@ -0,0 +1,16 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: SITE_TITLE,
description: SITE_DESCRIPTION,
site: context.site,
items: posts.map((post) => ({
...post.data,
link: `/blog/${post.id}/`,
})),
});
}

View File

@@ -0,0 +1,178 @@
/*
The CSS in this style tag is based off of Bear Blog's default CSS.
https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
*/
@import "tailwindcss";
:root {
/*--accent: rgb(255, 79, 1);*/
/*--accent-dark: #fafafa;*/
/*--black: 15, 18, 25;*/
/*--gray: 96, 1, 159;*/
/*--gray-light: 82, 82, 82;*/
--gray-dark: 250, 250, 250;
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
--box-shadow:
0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%),
0 16px 32px rgba(var(--gray), 33%);
}
@font-face {
font-family: 'Atkinson';
src: url('/fonts/atkinson-regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Atkinson';
src: url('/fonts/atkinson-bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
body {
font-family: 'Atkinson', sans-serif;
margin: 0;
padding: 0;
text-align: left;
background: linear-gradient(var(--gray-gradient)) no-repeat;
background-color: #171717;
background-size: 100% 600px;
word-wrap: break-word;
overflow-wrap: break-word;
color: rgb(var(--gray-dark));
font-size: 20px;
line-height: 1.7;
}
main {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 3em 1em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 0.5rem 0;
color: rgb(var(--black));
line-height: 1.2;
}
h1 {
font-size: 3.052em;
}
h2 {
font-size: 2.441em;
}
h3 {
font-size: 1.953em;
}
h4 {
font-size: 1.563em;
}
h5 {
font-size: 1.25em;
}
strong,
b {
font-weight: 700;
}
a {
color: var(--accent);
}
a:hover {
color: var(--accent);
}
p {
margin-bottom: 1em;
}
.prose p {
margin-bottom: 2em;
}
textarea {
width: 100%;
font-size: 16px;
}
input {
font-size: 16px;
}
table {
width: 100%;
}
img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
code {
padding: 2px 5px;
background-color: rgb(var(--gray-light));
border-radius: 2px;
}
pre {
padding: 1.5em;
border-radius: 8px;
}
pre > code {
all: unset;
}
blockquote {
border-left: 4px solid var(--accent);
padding: 0 0 0 20px;
margin: 0px;
font-size: 1.333em;
}
hr {
border: none;
border-top: 1px solid rgb(var(--gray-light));
}
@media (max-width: 720px) {
body {
font-size: 18px;
}
main {
padding: 1em;
}
}
.sr-only {
border: 0;
padding: 0;
margin: 0;
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px 1px 1px 1px);
/* maybe deprecated but we need to support legacy browsers */
clip: rect(1px, 1px, 1px, 1px);
/* modern browsers, clip-path works inwards from each corner */
clip-path: inset(50%);
/* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */
white-space: nowrap;
}
.bg-radial-gradient {
filter: blur(32px);
background-image: linear-gradient(
90deg,
rgb(239, 118, 70),
rgb(251, 91, 88),
rgb(255, 61, 116),
rgb(249, 33, 149),
rgb(227, 34, 188),
rgb(181, 94, 230),
rgb(118, 128, 252),
rgb(0, 150, 255),
rgb(0, 183, 255),
rgb(0, 208, 242),
rgb(0, 227, 184),
rgb(70, 239, 111)
);
}

15
apps/blog/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"compilerOptions": {
"strictNullChecks": true,
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
# Container build arguments #
ARG BASE_IMAGE=docker.io/cachyos/cachyos:latest
#*******************************************#
# Base Stage - Simple with light essentials #
#*******************************************#
FROM ${BASE_IMAGE} AS bases
# Only lightweight stuff needed by both builder and runtime
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm \
libssh2 curl wget libevdev libc++abi \
gstreamer gst-plugins-base

View File

@@ -0,0 +1,218 @@
# Container build arguments #
ARG RUNNER_BASE_IMAGE=runner-base:latest
#**************#
# builder base #
#**************#
FROM ${RUNNER_BASE_IMAGE} AS base-builder
ENV ARTIFACTS=/artifacts
RUN mkdir -p "${ARTIFACTS}"
# Environment setup for Rust and Cargo
ENV CARGO_HOME=/usr/local/cargo \
PATH="${CARGO_HOME}/bin:${PATH}"
# Install build essentials and caching tools
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm rustup git base-devel mold \
meson pkgconf cmake git gcc make
# Override various linker with symlink so mold is forcefully used (ld, ld.lld, lld)
RUN ln -sf /usr/bin/mold /usr/bin/ld && \
ln -sf /usr/bin/mold /usr/bin/ld.lld && \
ln -sf /usr/bin/mold /usr/bin/lld
# Install latest Rust using rustup
RUN rustup default stable
# Install cargo-chef with proper caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo install -j $(nproc) cargo-chef --locked
#*******************************#
# vimputti manager build stages #
#*******************************#
FROM base-builder AS vimputti-manager-deps
WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm lib32-gcc-libs
# Clone repository
RUN git clone --depth 1 --rev "9e8bfd0217eeab011c5afc368d3ea67a4c239e81" https://github.com/DatCaptainHorse/vimputti.git
#--------------------------------------------------------------------
FROM vimputti-manager-deps AS vimputti-manager-planner
WORKDIR /builder/vimputti
# Prepare recipe for dependency caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef prepare --recipe-path recipe.json
#--------------------------------------------------------------------
FROM vimputti-manager-deps AS vimputti-manager-cached-builder
WORKDIR /builder/vimputti
COPY --from=vimputti-manager-planner /builder/vimputti/recipe.json .
# Cache dependencies using cargo-chef
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef cook --release --recipe-path recipe.json
ENV CARGO_TARGET_DIR=/builder/target
COPY --from=vimputti-manager-planner /builder/vimputti/ .
# Build and install directly to artifacts
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
--mount=type=cache,target=/builder/target \
cargo build --release --package vimputti-manager && \
cargo build --release --package vimputti-shim && \
rustup target add i686-unknown-linux-gnu && \
cargo build --release --package vimputti-shim --target i686-unknown-linux-gnu && \
cp "${CARGO_TARGET_DIR}/release/vimputti-manager" "${ARTIFACTS}" && \
cp "${CARGO_TARGET_DIR}/release/libvimputti_shim.so" "${ARTIFACTS}/libvimputti_shim_64.so" && \
cp "${CARGO_TARGET_DIR}/i686-unknown-linux-gnu/release/libvimputti_shim.so" "${ARTIFACTS}/libvimputti_shim_32.so"
#****************************#
# nestri-server build stages #
#****************************#
FROM base-builder AS nestri-server-deps
WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm gst-plugins-good gst-plugin-rswebrtc
#--------------------------------------------------------------------
FROM nestri-server-deps AS nestri-server-planner
WORKDIR /builder/nestri
COPY packages/server/Cargo.toml packages/server/Cargo.lock ./
# Prepare recipe for dependency caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef prepare --recipe-path recipe.json
#--------------------------------------------------------------------
FROM nestri-server-deps AS nestri-server-cached-builder
WORKDIR /builder/nestri
COPY --from=nestri-server-planner /builder/nestri/recipe.json .
# Cache dependencies using cargo-chef
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef cook --release --recipe-path recipe.json
ENV CARGO_TARGET_DIR=/builder/target
COPY packages/server/ ./
# Build and install directly to artifacts
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
--mount=type=cache,target=/builder/target \
cargo build --release && \
cp "${CARGO_TARGET_DIR}/release/nestri-server" "${ARTIFACTS}"
#**********************************#
# gst-wayland-display build stages #
#**********************************#
FROM base-builder AS gst-wayland-deps
WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm libxkbcommon wayland \
gst-plugins-good gst-plugins-bad libinput
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo install cargo-c
# Grab cudart from NVIDIA..
RUN wget https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-13.0.96-archive.tar.xz -O cuda_cudart.tar.xz && \
mkdir cuda_cudart && tar -xf cuda_cudart.tar.xz -C cuda_cudart --strip-components=1 && \
cp cuda_cudart/lib/libcudart.so cuda_cudart/lib/libcudart.so.* /usr/lib/ && \
rm -r cuda_cudart && \
rm cuda_cudart.tar.xz
# Grab cuda lib from NVIDIA (it's in driver package of all things..)
RUN wget https://developer.download.nvidia.com/compute/cuda/redist/nvidia_driver/linux-x86_64/nvidia_driver-linux-x86_64-580.95.05-archive.tar.xz -O nvidia_driver.tar.xz && \
mkdir nvidia_driver && tar -xf nvidia_driver.tar.xz -C nvidia_driver --strip-components=1 && \
cp nvidia_driver/lib/libcuda.so.* /usr/lib/libcuda.so && \
ln -s /usr/lib/libcuda.so /usr/lib/libcuda.so.1 && \
rm -r nvidia_driver && \
rm nvidia_driver.tar.xz
# Clone repository
RUN git clone --depth 1 --rev "afa853fa03e8403c83bbb3bc0cf39147ad46c266" https://github.com/games-on-whales/gst-wayland-display.git
#--------------------------------------------------------------------
FROM gst-wayland-deps AS gst-wayland-planner
WORKDIR /builder/gst-wayland-display
# Prepare recipe for dependency caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef prepare --recipe-path recipe.json
#--------------------------------------------------------------------
FROM gst-wayland-deps AS gst-wayland-cached-builder
WORKDIR /builder/gst-wayland-display
COPY --from=gst-wayland-planner /builder/gst-wayland-display/recipe.json .
# Cache dependencies using cargo-chef
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef cook --release --recipe-path recipe.json
ENV CARGO_TARGET_DIR=/builder/target
COPY --from=gst-wayland-planner /builder/gst-wayland-display/ .
# Build and install directly to artifacts
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
--mount=type=cache,target=/builder/target \
cargo cinstall --prefix=${ARTIFACTS} --release
#*********************************#
# Patched bubblewrap build stages #
#*********************************#
FROM base-builder AS bubblewrap-deps
WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm libtool libcap libselinux
# Copy patch file from host
COPY packages/patches/bubblewrap/ /builder/patches/
# Clone repository
RUN git clone --depth 1 --rev "9ca3b05ec787acfb4b17bed37db5719fa777834f" https://github.com/containers/bubblewrap.git && \
cd bubblewrap && \
# Apply patch to fix user namespace issue
git apply ../patches/bubbleunheck.patch
#--------------------------------------------------------------------
FROM bubblewrap-deps AS bubblewrap-builder
WORKDIR /builder/bubblewrap
# Build and install directly to artifacts
RUN meson setup build --prefix=${ARTIFACTS} && \
meson compile -C build && \
meson install -C build
#*********************************************#
# Final Export Stage - Collects all artifacts #
#*********************************************#
FROM scratch AS artifacts
COPY --from=nestri-server-cached-builder /artifacts/nestri-server /artifacts/bin/
COPY --from=gst-wayland-cached-builder /artifacts/lib/ /artifacts/lib/
COPY --from=gst-wayland-cached-builder /artifacts/include/ /artifacts/include/
COPY --from=vimputti-manager-cached-builder /artifacts/vimputti-manager /artifacts/bin/
COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_64.so /artifacts/lib64/libvimputti_shim.so
COPY --from=vimputti-manager-cached-builder /artifacts/libvimputti_shim_32.so /artifacts/lib32/libvimputti_shim.so
COPY --from=gst-wayland-deps /usr/lib/libcuda.so /usr/lib/libcuda.so.* /artifacts/lib/
COPY --from=bubblewrap-builder /artifacts/bin/bwrap /artifacts/bin/

View File

@@ -1,125 +1,13 @@
# Container build arguments # # Container build arguments #
ARG BASE_IMAGE=docker.io/cachyos/cachyos:latest ARG RUNNER_BASE_IMAGE=runner-base:latest
ARG RUNNER_BUILDER_IMAGE=runner-builder:latest
#****************************************************************************** #*********************#
# Base Stage - Updates system packages # Final Runtime Stage #
#****************************************************************************** #*********************#
FROM ${BASE_IMAGE} AS base FROM ${RUNNER_BASE_IMAGE} AS runtime
FROM ${RUNNER_BUILDER_IMAGE} AS builder
RUN --mount=type=cache,target=/var/cache/pacman/pkg \ FROM runtime
pacman --noconfirm -Syu
#******************************************************************************
# Base Builder Stage - Prepares core build environment
#******************************************************************************
FROM base AS base-builder
# Environment setup for Rust and Cargo
ENV CARGO_HOME=/usr/local/cargo \
ARTIFACTS=/artifacts \
PATH="${CARGO_HOME}/bin:${PATH}" \
RUSTFLAGS="-C link-arg=-fuse-ld=mold"
# Install build essentials and caching tools
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm mold rustup && \
mkdir -p "${ARTIFACTS}"
# Install latest Rust using rustup
RUN rustup default stable
# Install cargo-chef with proper caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo install -j $(nproc) cargo-chef cargo-c --locked
#******************************************************************************
# Nestri Server Build Stages
#******************************************************************************
FROM base-builder AS nestri-server-deps
WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm meson pkgconf cmake git gcc make \
gstreamer gst-plugins-base gst-plugins-good gst-plugin-rswebrtc
#--------------------------------------------------------------------
FROM nestri-server-deps AS nestri-server-planner
WORKDIR /builder/nestri
COPY packages/server/Cargo.toml packages/server/Cargo.lock ./
# Prepare recipe for dependency caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef prepare --recipe-path recipe.json
#--------------------------------------------------------------------
FROM nestri-server-deps AS nestri-server-cached-builder
WORKDIR /builder/nestri
COPY --from=nestri-server-planner /builder/nestri/recipe.json .
# Cache dependencies using cargo-chef
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef cook --release --recipe-path recipe.json
ENV CARGO_TARGET_DIR=/builder/target
COPY packages/server/ ./
# Build and install directly to artifacts
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
--mount=type=cache,target=/builder/target \
cargo build --release && \
cp "${CARGO_TARGET_DIR}/release/nestri-server" "${ARTIFACTS}"
#******************************************************************************
# GST-Wayland Plugin Build Stages
#******************************************************************************
FROM base-builder AS gst-wayland-deps
WORKDIR /builder
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm meson pkgconf cmake git gcc make \
libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput
# Clone repository
RUN git clone --depth 1 --rev "dfeebb19b48f32207469e166a3955f5d65b5e6c6" https://github.com/games-on-whales/gst-wayland-display.git
#--------------------------------------------------------------------
FROM gst-wayland-deps AS gst-wayland-planner
WORKDIR /builder/gst-wayland-display
# Prepare recipe for dependency caching
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef prepare --recipe-path recipe.json
#--------------------------------------------------------------------
FROM gst-wayland-deps AS gst-wayland-cached-builder
WORKDIR /builder/gst-wayland-display
COPY --from=gst-wayland-planner /builder/gst-wayland-display/recipe.json .
# Cache dependencies using cargo-chef
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
cargo chef cook --release --recipe-path recipe.json
ENV CARGO_TARGET_DIR=/builder/target
COPY --from=gst-wayland-planner /builder/gst-wayland-display/ .
# Build and install directly to artifacts
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
--mount=type=cache,target=/builder/target \
cargo cinstall --prefix=${ARTIFACTS} --release
#******************************************************************************
# Final Runtime Stage
#******************************************************************************
FROM base AS runtime
### Package Installation ### ### Package Installation ###
# Core system components # Core system components
@@ -127,20 +15,17 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --needed --noconfirm \ pacman -Sy --needed --noconfirm \
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \ vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
vulkan-radeon lib32-vulkan-radeon \ vulkan-radeon lib32-vulkan-radeon \
mesa steam-native-runtime proton-cachyos lib32-mesa \ mesa lib32-mesa \
steam gtk3 lib32-gtk3 \ steam gtk3 lib32-gtk3 \
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \ sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
libssh2 curl wget \
pipewire pipewire-pulse pipewire-alsa wireplumber \ pipewire pipewire-pulse pipewire-alsa wireplumber \
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib \ noto-fonts-cjk supervisor jq pacman-contrib \
hwdata openssh \ hwdata openssh \
# GStreamer stack # GStreamer stack
gstreamer gst-plugins-base gst-plugins-good \ gst-plugins-good \
gst-plugins-bad gst-plugin-pipewire \ gst-plugins-bad gst-plugin-pipewire \
gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \ gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \
gst-plugin-va gst-plugin-qsv \ gst-plugin-va gst-plugin-qsv && \
# lib32 GStreamer stack to fix some games with videos
lib32-gstreamer lib32-gst-plugins-base lib32-gst-plugins-good && \
# Cleanup # Cleanup
paccache -rk1 && \ paccache -rk1 && \
rm -rf /usr/share/{info,man,doc}/* rm -rf /usr/share/{info,man,doc}/*
@@ -153,6 +38,7 @@ ENV NESTRI_USER="nestri" \
NESTRI_LANG=en_US.UTF-8 \ NESTRI_LANG=en_US.UTF-8 \
NESTRI_XDG_RUNTIME_DIR=/run/user/1000 \ NESTRI_XDG_RUNTIME_DIR=/run/user/1000 \
NESTRI_HOME=/home/nestri \ NESTRI_HOME=/home/nestri \
NESTRI_VIMPUTTI_PATH=/tmp/vimputti-1000 \
NVIDIA_DRIVER_CAPABILITIES=all NVIDIA_DRIVER_CAPABILITIES=all
RUN mkdir -p "/home/${NESTRI_USER}" && \ RUN mkdir -p "/home/${NESTRI_USER}" && \
@@ -174,29 +60,33 @@ RUN mkdir -p /run/dbus && \
-e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \ -e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \
/usr/share/wireplumber/wireplumber.conf /usr/share/wireplumber/wireplumber.conf
### Audio Systems Configs - Latency optimizations + Loopback ### ## Audio Systems Configs - Latency optimizations + Loopback ##
RUN mkdir -p /etc/pipewire/pipewire.conf.d && \ RUN mkdir -p /etc/pipewire/pipewire.conf.d && \
mkdir -p /etc/wireplumber/wireplumber.conf.d mkdir -p /etc/wireplumber/wireplumber.conf.d
COPY packages/configs/wireplumber.conf.d/* /etc/wireplumber/wireplumber.conf.d/ COPY packages/configs/wireplumber.conf.d/* /etc/wireplumber/wireplumber.conf.d/
COPY packages/configs/pipewire.conf.d/* /etc/pipewire/pipewire.conf.d/ COPY packages/configs/pipewire.conf.d/* /etc/pipewire/pipewire.conf.d/
## Steam Configs - Proton (CachyOS flavor) ## ## Steam Configs - Proton (Experimental flavor) ##
RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config" RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config"
COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/" COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/"
### Artifacts and Verification ### ### Artifacts from Builder ###
COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/ COPY --from=builder /artifacts/bin/nestri-server /usr/bin/
COPY --from=gst-wayland-cached-builder /artifacts/lib/ /usr/lib/ COPY --from=builder /artifacts/bin/bwrap /usr/bin/
COPY --from=gst-wayland-cached-builder /artifacts/include/ /usr/include/ COPY --from=builder /artifacts/lib/ /usr/lib/
RUN which nestri-server && ls -la /usr/lib/ | grep 'gstwaylanddisplay' COPY --from=builder /artifacts/lib32/ /usr/lib32/
COPY --from=builder /artifacts/lib64/ /usr/lib64/
COPY --from=builder /artifacts/bin/vimputti-manager /usr/bin/
### Scripts and Final Configuration ### ### Scripts and Final Configuration ###
COPY packages/scripts/ /etc/nestri/ COPY packages/scripts/ /etc/nestri/
RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \ RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \
chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" && \ chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" && \
sed -i 's/^#\(en_US\.UTF-8\)/\1/' /etc/locale.gen && \ sed -i 's/^#\(en_US\.UTF-8\)/\1/' /etc/locale.gen && \
setcap cap_net_admin+ep /usr/bin/vimputti-manager && \
dbus-uuidgen > /etc/machine-id && \
LANG=en_US.UTF-8 locale-gen LANG=en_US.UTF-8 locale-gen
# Root for most container engines, nestri-user compatible for apptainer without fakeroot # Root for most container engines, nestri-user compatible for apptainer without fakeroot

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,509 @@
import { controllerButtonToLinuxEventCode } from "./codes";
import { WebRTCStream } from "./webrtc-stream";
import {
ProtoMessageBase,
ProtoMessageInput,
ProtoMessageInputSchema,
} from "./proto/messages_pb";
import {
ProtoInputSchema,
ProtoControllerAttachSchema,
ProtoControllerDetachSchema,
ProtoControllerButtonSchema,
ProtoControllerTriggerSchema,
ProtoControllerAxisSchema,
ProtoControllerStickSchema,
ProtoControllerRumble,
} from "./proto/types_pb";
import { create, toBinary, fromBinary } from "@bufbuild/protobuf";
interface Props {
webrtc: WebRTCStream;
e: GamepadEvent;
}
interface GamepadState {
buttonState: Map<number, boolean>;
leftTrigger: number;
rightTrigger: number;
leftX: number;
leftY: number;
rightX: number;
rightY: number;
dpadX: number;
dpadY: number;
}
export class Controller {
protected wrtc: WebRTCStream;
protected slot: number;
protected connected: boolean = false;
protected gamepad: Gamepad | null = null;
protected lastState: GamepadState = {
buttonState: new Map<number, boolean>(),
leftTrigger: 0,
rightTrigger: 0,
leftX: 0,
leftY: 0,
rightX: 0,
rightY: 0,
dpadX: 0,
dpadY: 0,
};
// TODO: As user configurable, set quite low now for decent controllers (not Nintendo ones :P)
protected stickDeadzone: number = 2048; // 2048 / 32768 = ~0.06 (6% of stick range)
private updateInterval = 10.0; // 100 updates per second
private _dcRumbleHandler: ((data: ArrayBuffer) => void) | null = null;
constructor({ webrtc, e }: Props) {
this.wrtc = webrtc;
this.slot = e.gamepad.index;
this.updateInterval = 1000 / webrtc.currentFrameRate;
// Gamepad connected
this.gamepad = e.gamepad;
// Get vendor of gamepad from id string (i.e. "... Vendor: 054c Product: 09cc")
const vendorMatch = e.gamepad.id.match(/Vendor:\s?([0-9a-fA-F]{4})/);
const vendorId = vendorMatch ? vendorMatch[1].toLowerCase() : "unknown";
// Get product id of gamepad from id string
const productMatch = e.gamepad.id.match(/Product:\s?([0-9a-fA-F]{4})/);
const productId = productMatch ? productMatch[1].toLowerCase() : "unknown";
const attachMsg = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerAttach",
value: create(ProtoControllerAttachSchema, {
type: "ControllerAttach",
id: this.vendor_id_to_controller(vendorId, productId),
slot: this.slot,
}),
},
});
const message: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: attachMsg,
};
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
// Listen to feedback rumble events from server
this._dcRumbleHandler = (data: any) => this.rumbleCallback(data as ArrayBuffer);
this.wrtc.addDataChannelCallback(this._dcRumbleHandler);
this.run();
}
// Maps vendor id and product id to supported controller type
// Currently supported: Sony (ps4, ps5), Microsoft (xbox360, xboxone), Nintendo (switchpro)
// Default fallback to xbox360
private vendor_id_to_controller(vendorId: string, productId: string): string {
switch (vendorId) {
case "054c": // Sony
switch (productId) {
case "0ce6":
return "ps5";
case "05c4":
case "09cc":
return "ps4";
default:
return "ps4"; // default to ps4
}
case "045e": // Microsoft
switch (productId) {
case "02d1":
case "02dd":
return "xboxone";
case "028e":
return "xbox360";
default:
return "xbox360"; // default to xbox360
}
case "057e": // Nintendo
switch (productId) {
case "2009":
case "200e":
return "switchpro";
default:
return "switchpro"; // default to switchpro
}
default: {
return "xbox360";
}
}
}
private remapFromTo(
value: number,
fromMin: number,
fromMax: number,
toMin: number,
toMax: number,
) {
return ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin) + toMin;
}
private pollGamepad() {
const gamepads = navigator.getGamepads();
if (this.slot < gamepads.length) {
const gamepad = gamepads[this.slot];
if (gamepad) {
/* Button handling */
gamepad.buttons.forEach((button, index) => {
// Ignore d-pad buttons (12-15) as we handle those as axis
if (index >= 12 && index <= 15) return;
// ignore trigger buttons (6-7) as we handle those as axis
if (index === 6 || index === 7) return;
// If state differs, send
if (button.pressed !== this.lastState.buttonState.get(index)) {
const linuxCode = this.controllerButtonToVirtualKeyCode(index);
if (linuxCode === undefined) {
// Skip unmapped button index
this.lastState.buttonState.set(index, button.pressed);
return;
}
const buttonProto = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerButton",
value: create(ProtoControllerButtonSchema, {
type: "ControllerButton",
slot: this.slot,
button: linuxCode,
pressed: button.pressed,
}),
},
});
const buttonMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: buttonProto,
};
this.wrtc.sendBinary(
toBinary(ProtoMessageInputSchema, buttonMessage),
);
// Store button state
this.lastState.buttonState.set(index, button.pressed);
}
});
/* Trigger handling */
// map trigger value from 0.0 to 1.0 to -32768 to 32767
const leftTrigger = Math.round(
this.remapFromTo(gamepad.buttons[6]?.value ?? 0, 0, 1, -32768, 32767),
);
// If state differs, send
if (leftTrigger !== this.lastState.leftTrigger) {
const triggerProto = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerTrigger",
value: create(ProtoControllerTriggerSchema, {
type: "ControllerTrigger",
slot: this.slot,
trigger: 0, // 0 = left, 1 = right
value: leftTrigger,
}),
},
});
const triggerMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: triggerProto,
};
this.lastState.leftTrigger = leftTrigger;
this.wrtc.sendBinary(
toBinary(ProtoMessageInputSchema, triggerMessage),
);
}
const rightTrigger = Math.round(
this.remapFromTo(gamepad.buttons[7]?.value ?? 0, 0, 1, -32768, 32767),
);
// If state differs, send
if (rightTrigger !== this.lastState.rightTrigger) {
const triggerProto = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerTrigger",
value: create(ProtoControllerTriggerSchema, {
type: "ControllerTrigger",
slot: this.slot,
trigger: 1, // 0 = left, 1 = right
value: rightTrigger,
}),
},
});
const triggerMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: triggerProto,
};
this.lastState.rightTrigger = rightTrigger;
this.wrtc.sendBinary(
toBinary(ProtoMessageInputSchema, triggerMessage),
);
}
/* DPad handling */
// We send dpad buttons as axis values -1 to 1 for left/up, right/down
const dpadLeft = gamepad.buttons[14]?.pressed ? 1 : 0;
const dpadRight = gamepad.buttons[15]?.pressed ? 1 : 0;
const dpadX = dpadLeft ? -1 : dpadRight ? 1 : 0;
if (dpadX !== this.lastState.dpadX) {
const dpadProto = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerAxis",
value: create(ProtoControllerAxisSchema, {
type: "ControllerAxis",
slot: this.slot,
axis: 0, // 0 = dpadX, 1 = dpadY
value: dpadX,
}),
},
});
const dpadMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: dpadProto,
};
this.lastState.dpadX = dpadX;
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, dpadMessage));
}
const dpadUp = gamepad.buttons[12]?.pressed ? 1 : 0;
const dpadDown = gamepad.buttons[13]?.pressed ? 1 : 0;
const dpadY = dpadUp ? -1 : dpadDown ? 1 : 0;
if (dpadY !== this.lastState.dpadY) {
const dpadProto = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerAxis",
value: create(ProtoControllerAxisSchema, {
type: "ControllerAxis",
slot: this.slot,
axis: 1, // 0 = dpadX, 1 = dpadY
value: dpadY,
}),
},
});
const dpadMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: dpadProto,
};
this.lastState.dpadY = dpadY;
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, dpadMessage));
}
/* Stick handling */
// stick values need to be mapped from -1.0 to 1.0 to -32768 to 32767
const leftX = this.remapFromTo(gamepad.axes[0] ?? 0, -1, 1, -32768, 32767);
const leftY = this.remapFromTo(gamepad.axes[1] ?? 0, -1, 1, -32768, 32767);
// Apply deadzone
const sendLeftX =
Math.abs(leftX) > this.stickDeadzone ? Math.round(leftX) : 0;
const sendLeftY =
Math.abs(leftY) > this.stickDeadzone ? Math.round(leftY) : 0;
// if outside deadzone, send normally if changed
// if moves inside deadzone, zero it if not inside deadzone last time
if (
sendLeftX !== this.lastState.leftX ||
sendLeftY !== this.lastState.leftY
) {
// console.log("Sticks: ", sendLeftX, sendLeftY, sendRightX, sendRightY);
const stickProto = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerStick",
value: create(ProtoControllerStickSchema, {
type: "ControllerStick",
slot: this.slot,
stick: 0, // 0 = left, 1 = right
x: sendLeftX,
y: sendLeftY,
}),
},
});
const stickMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: stickProto,
};
this.lastState.leftX = sendLeftX;
this.lastState.leftY = sendLeftY;
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, stickMessage));
}
const rightX = this.remapFromTo(gamepad.axes[2] ?? 0, -1, 1, -32768, 32767);
const rightY = this.remapFromTo(gamepad.axes[3] ?? 0, -1, 1, -32768, 32767);
// Apply deadzone
const sendRightX =
Math.abs(rightX) > this.stickDeadzone ? Math.round(rightX) : 0;
const sendRightY =
Math.abs(rightY) > this.stickDeadzone ? Math.round(rightY) : 0;
if (
sendRightX !== this.lastState.rightX ||
sendRightY !== this.lastState.rightY
) {
const stickProto = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerStick",
value: create(ProtoControllerStickSchema, {
type: "ControllerStick",
slot: this.slot,
stick: 1, // 0 = left, 1 = right
x: sendRightX,
y: sendRightY,
}),
},
});
const stickMessage: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: stickProto,
};
this.lastState.rightX = sendRightX;
this.lastState.rightY = sendRightY;
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, stickMessage));
}
}
}
}
private loopInterval: any = null;
public run() {
if (this.connected)
this.stop();
this.connected = true;
// Poll gamepads in setInterval loop
this.loopInterval = setInterval(() => {
if (this.connected) this.pollGamepad();
}, this.updateInterval);
}
public stop() {
if (this.loopInterval) {
clearInterval(this.loopInterval);
this.loopInterval = null;
}
this.connected = false;
}
public getSlot() {
return this.slot;
}
public dispose() {
this.stop();
// Remove callback
if (this._dcRumbleHandler !== null) {
this.wrtc.removeDataChannelCallback(this._dcRumbleHandler);
this._dcRumbleHandler = null;
}
// Gamepad disconnected
const detachMsg = create(ProtoInputSchema, {
$typeName: "proto.ProtoInput",
inputType: {
case: "controllerDetach",
value: create(ProtoControllerDetachSchema, {
type: "ControllerDetach",
slot: this.slot,
}),
},
});
const message: ProtoMessageInput = {
$typeName: "proto.ProtoMessageInput",
messageBase: {
$typeName: "proto.ProtoMessageBase",
payloadType: "controllerInput",
} as ProtoMessageBase,
data: detachMsg,
};
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
}
private controllerButtonToVirtualKeyCode(code: number) {
return controllerButtonToLinuxEventCode[code] || undefined;
}
private rumbleCallback(data: ArrayBuffer) {
// If not connected, ignore
if (!this.connected) return;
try {
// First decode the wrapper message
const uint8Data = new Uint8Array(data);
const messageWrapper = fromBinary(ProtoMessageInputSchema, uint8Data);
// Check if it contains controller rumble data
if (messageWrapper.data?.inputType?.case === "controllerRumble") {
const rumbleMsg = messageWrapper.data.inputType.value as ProtoControllerRumble;
// Check if aimed at this controller slot
if (rumbleMsg.slot !== this.slot) return;
// Trigger actual rumble
// Need to remap from 0-65535 to 0.0-1.0 ranges
const clampedLowFreq = Math.max(0, Math.min(65535, rumbleMsg.lowFrequency));
const rumbleLowFreq = this.remapFromTo(
clampedLowFreq,
0,
65535,
0.0,
1.0,
);
const clampedHighFreq = Math.max(0, Math.min(65535, rumbleMsg.highFrequency));
const rumbleHighFreq = this.remapFromTo(
clampedHighFreq,
0,
65535,
0.0,
1.0,
);
// Cap to valid range (max 5000)
const rumbleDuration = Math.max(0, Math.min(5000, rumbleMsg.duration));
if (this.gamepad.vibrationActuator) {
this.gamepad.vibrationActuator.playEffect("dual-rumble", {
startDelay: 0,
duration: rumbleDuration,
weakMagnitude: rumbleLowFreq,
strongMagnitude: rumbleHighFreq,
}).catch(console.error);
}
}
} catch (error) {
console.error("Failed to decode rumble message:", error);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts" // @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
// @generated from file types.proto (package proto, syntax proto3) // @generated from file types.proto (package proto, syntax proto3)
/* eslint-disable */ /* eslint-disable */
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv1"; import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv1"; import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
import type { Message } from "@bufbuild/protobuf"; import type { Message } from "@bufbuild/protobuf";
/** /**
* Describes the file types.proto. * Describes the file types.proto.
*/ */
export const file_types: GenFile = /*@__PURE__*/ export const file_types: GenFile = /*@__PURE__*/
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSLcAgoKUHJvdG9JbnB1dBIrCgptb3VzZV9tb3ZlGAEgASgLMhUucHJvdG8uUHJvdG9Nb3VzZU1vdmVIABIyCg5tb3VzZV9tb3ZlX2FicxgCIAEoCzIYLnByb3RvLlByb3RvTW91c2VNb3ZlQWJzSAASLQoLbW91c2Vfd2hlZWwYAyABKAsyFi5wcm90by5Qcm90b01vdXNlV2hlZWxIABIyCg5tb3VzZV9rZXlfZG93bhgEIAEoCzIYLnByb3RvLlByb3RvTW91c2VLZXlEb3duSAASLgoMbW91c2Vfa2V5X3VwGAUgASgLMhYucHJvdG8uUHJvdG9Nb3VzZUtleVVwSAASJwoIa2V5X2Rvd24YBiABKAsyEy5wcm90by5Qcm90b0tleURvd25IABIjCgZrZXlfdXAYByABKAsyES5wcm90by5Qcm90b0tleVVwSABCDAoKaW5wdXRfdHlwZUIWWhRyZWxheS9pbnRlcm5hbC9wcm90b2IGcHJvdG8z"); fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSI/ChVQcm90b0NvbnRyb2xsZXJBdHRhY2gSDAoEdHlwZRgBIAEoCRIKCgJpZBgCIAEoCRIMCgRzbG90GAMgASgFIjMKFVByb3RvQ29udHJvbGxlckRldGFjaBIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUiVAoVUHJvdG9Db250cm9sbGVyQnV0dG9uEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIOCgZidXR0b24YAyABKAUSDwoHcHJlc3NlZBgEIAEoCCJUChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIPCgd0cmlnZ2VyGAMgASgFEg0KBXZhbHVlGAQgASgFIlcKFFByb3RvQ29udHJvbGxlclN0aWNrEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRINCgVzdGljaxgDIAEoBRIJCgF4GAQgASgFEgkKAXkYBSABKAUiTgoTUHJvdG9Db250cm9sbGVyQXhpcxIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUSDAoEYXhpcxgDIAEoBRINCgV2YWx1ZRgEIAEoBSJ0ChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEdHlwZRgBIAEoCRIMCgRzbG90GAIgASgFEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi9QUKClByb3RvSW5wdXQSKwoKbW91c2VfbW92ZRgBIAEoCzIVLnByb3RvLlByb3RvTW91c2VNb3ZlSAASMgoObW91c2VfbW92ZV9hYnMYAiABKAsyGC5wcm90by5Qcm90b01vdXNlTW92ZUFic0gAEi0KC21vdXNlX3doZWVsGAMgASgLMhYucHJvdG8uUHJvdG9Nb3VzZVdoZWVsSAASMgoObW91c2Vfa2V5X2Rvd24YBCABKAsyGC5wcm90by5Qcm90b01vdXNlS2V5RG93bkgAEi4KDG1vdXNlX2tleV91cBgFIAEoCzIWLnByb3RvLlByb3RvTW91c2VLZXlVcEgAEicKCGtleV9kb3duGAYgASgLMhMucHJvdG8uUHJvdG9LZXlEb3duSAASIwoGa2V5X3VwGAcgASgLMhEucHJvdG8uUHJvdG9LZXlVcEgAEjkKEWNvbnRyb2xsZXJfYXR0YWNoGAggASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyQXR0YWNoSAASOQoRY29udHJvbGxlcl9kZXRhY2gYCSABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJEZXRhY2hIABI5ChFjb250cm9sbGVyX2J1dHRvbhgKIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckJ1dHRvbkgAEjsKEmNvbnRyb2xsZXJfdHJpZ2dlchgLIAEoCzIdLnByb3RvLlByb3RvQ29udHJvbGxlclRyaWdnZXJIABI3ChBjb250cm9sbGVyX3N0aWNrGAwgASgLMhsucHJvdG8uUHJvdG9Db250cm9sbGVyU3RpY2tIABI1Cg9jb250cm9sbGVyX2F4aXMYDSABKAsyGi5wcm90by5Qcm90b0NvbnRyb2xsZXJBeGlzSAASOQoRY29udHJvbGxlcl9ydW1ibGUYDiABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIAEIMCgppbnB1dF90eXBlQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM");
/** /**
* MouseMove message * MouseMove message
@@ -209,6 +209,293 @@ export type ProtoKeyUp = Message<"proto.ProtoKeyUp"> & {
export const ProtoKeyUpSchema: GenMessage<ProtoKeyUp> = /*@__PURE__*/ export const ProtoKeyUpSchema: GenMessage<ProtoKeyUp> = /*@__PURE__*/
messageDesc(file_types, 6); messageDesc(file_types, 6);
/**
* ControllerAttach message
*
* @generated from message proto.ProtoControllerAttach
*/
export type ProtoControllerAttach = Message<"proto.ProtoControllerAttach"> & {
/**
* Fixed value "ControllerAttach"
*
* @generated from field: string type = 1;
*/
type: string;
/**
* One of the following enums: "ps", "xbox" or "switch"
*
* @generated from field: string id = 2;
*/
id: string;
/**
* Slot number (0-3)
*
* @generated from field: int32 slot = 3;
*/
slot: number;
};
/**
* Describes the message proto.ProtoControllerAttach.
* Use `create(ProtoControllerAttachSchema)` to create a new message.
*/
export const ProtoControllerAttachSchema: GenMessage<ProtoControllerAttach> = /*@__PURE__*/
messageDesc(file_types, 7);
/**
* ControllerDetach message
*
* @generated from message proto.ProtoControllerDetach
*/
export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & {
/**
* Fixed value "ControllerDetach"
*
* @generated from field: string type = 1;
*/
type: string;
/**
* Slot number (0-3)
*
* @generated from field: int32 slot = 2;
*/
slot: number;
};
/**
* Describes the message proto.ProtoControllerDetach.
* Use `create(ProtoControllerDetachSchema)` to create a new message.
*/
export const ProtoControllerDetachSchema: GenMessage<ProtoControllerDetach> = /*@__PURE__*/
messageDesc(file_types, 8);
/**
* ControllerButton message
*
* @generated from message proto.ProtoControllerButton
*/
export type ProtoControllerButton = Message<"proto.ProtoControllerButton"> & {
/**
* Fixed value "ControllerButtons"
*
* @generated from field: string type = 1;
*/
type: string;
/**
* Slot number (0-3)
*
* @generated from field: int32 slot = 2;
*/
slot: number;
/**
* Button code (linux input event code)
*
* @generated from field: int32 button = 3;
*/
button: number;
/**
* true if pressed, false if released
*
* @generated from field: bool pressed = 4;
*/
pressed: boolean;
};
/**
* Describes the message proto.ProtoControllerButton.
* Use `create(ProtoControllerButtonSchema)` to create a new message.
*/
export const ProtoControllerButtonSchema: GenMessage<ProtoControllerButton> = /*@__PURE__*/
messageDesc(file_types, 9);
/**
* ControllerTriggers message
*
* @generated from message proto.ProtoControllerTrigger
*/
export type ProtoControllerTrigger = Message<"proto.ProtoControllerTrigger"> & {
/**
* Fixed value "ControllerTriggers"
*
* @generated from field: string type = 1;
*/
type: string;
/**
* Slot number (0-3)
*
* @generated from field: int32 slot = 2;
*/
slot: number;
/**
* Trigger number (0 for left, 1 for right)
*
* @generated from field: int32 trigger = 3;
*/
trigger: number;
/**
* trigger value (-32768 to 32767)
*
* @generated from field: int32 value = 4;
*/
value: number;
};
/**
* Describes the message proto.ProtoControllerTrigger.
* Use `create(ProtoControllerTriggerSchema)` to create a new message.
*/
export const ProtoControllerTriggerSchema: GenMessage<ProtoControllerTrigger> = /*@__PURE__*/
messageDesc(file_types, 10);
/**
* ControllerSticks message
*
* @generated from message proto.ProtoControllerStick
*/
export type ProtoControllerStick = Message<"proto.ProtoControllerStick"> & {
/**
* Fixed value "ControllerStick"
*
* @generated from field: string type = 1;
*/
type: string;
/**
* Slot number (0-3)
*
* @generated from field: int32 slot = 2;
*/
slot: number;
/**
* Stick number (0 for left, 1 for right)
*
* @generated from field: int32 stick = 3;
*/
stick: number;
/**
* X axis value (-32768 to 32767)
*
* @generated from field: int32 x = 4;
*/
x: number;
/**
* Y axis value (-32768 to 32767)
*
* @generated from field: int32 y = 5;
*/
y: number;
};
/**
* Describes the message proto.ProtoControllerStick.
* Use `create(ProtoControllerStickSchema)` to create a new message.
*/
export const ProtoControllerStickSchema: GenMessage<ProtoControllerStick> = /*@__PURE__*/
messageDesc(file_types, 11);
/**
* ControllerAxis message
*
* @generated from message proto.ProtoControllerAxis
*/
export type ProtoControllerAxis = Message<"proto.ProtoControllerAxis"> & {
/**
* Fixed value "ControllerAxis"
*
* @generated from field: string type = 1;
*/
type: string;
/**
* Slot number (0-3)
*
* @generated from field: int32 slot = 2;
*/
slot: number;
/**
* Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
*
* @generated from field: int32 axis = 3;
*/
axis: number;
/**
* axis value (-1 to 1)
*
* @generated from field: int32 value = 4;
*/
value: number;
};
/**
* Describes the message proto.ProtoControllerAxis.
* Use `create(ProtoControllerAxisSchema)` to create a new message.
*/
export const ProtoControllerAxisSchema: GenMessage<ProtoControllerAxis> = /*@__PURE__*/
messageDesc(file_types, 12);
/**
* ControllerRumble message
*
* @generated from message proto.ProtoControllerRumble
*/
export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & {
/**
* Fixed value "ControllerRumble"
*
* @generated from field: string type = 1;
*/
type: string;
/**
* Slot number (0-3)
*
* @generated from field: int32 slot = 2;
*/
slot: number;
/**
* Low frequency rumble (0-65535)
*
* @generated from field: int32 low_frequency = 3;
*/
lowFrequency: number;
/**
* High frequency rumble (0-65535)
*
* @generated from field: int32 high_frequency = 4;
*/
highFrequency: number;
/**
* Duration in milliseconds
*
* @generated from field: int32 duration = 5;
*/
duration: number;
};
/**
* Describes the message proto.ProtoControllerRumble.
* Use `create(ProtoControllerRumbleSchema)` to create a new message.
*/
export const ProtoControllerRumbleSchema: GenMessage<ProtoControllerRumble> = /*@__PURE__*/
messageDesc(file_types, 13);
/** /**
* Union of all Input types * Union of all Input types
* *
@@ -260,6 +547,48 @@ export type ProtoInput = Message<"proto.ProtoInput"> & {
*/ */
value: ProtoKeyUp; value: ProtoKeyUp;
case: "keyUp"; case: "keyUp";
} | {
/**
* @generated from field: proto.ProtoControllerAttach controller_attach = 8;
*/
value: ProtoControllerAttach;
case: "controllerAttach";
} | {
/**
* @generated from field: proto.ProtoControllerDetach controller_detach = 9;
*/
value: ProtoControllerDetach;
case: "controllerDetach";
} | {
/**
* @generated from field: proto.ProtoControllerButton controller_button = 10;
*/
value: ProtoControllerButton;
case: "controllerButton";
} | {
/**
* @generated from field: proto.ProtoControllerTrigger controller_trigger = 11;
*/
value: ProtoControllerTrigger;
case: "controllerTrigger";
} | {
/**
* @generated from field: proto.ProtoControllerStick controller_stick = 12;
*/
value: ProtoControllerStick;
case: "controllerStick";
} | {
/**
* @generated from field: proto.ProtoControllerAxis controller_axis = 13;
*/
value: ProtoControllerAxis;
case: "controllerAxis";
} | {
/**
* @generated from field: proto.ProtoControllerRumble controller_rumble = 14;
*/
value: ProtoControllerRumble;
case: "controllerRumble";
} | { case: undefined; value?: undefined }; } | { case: undefined; value?: undefined };
}; };
@@ -268,5 +597,5 @@ export type ProtoInput = Message<"proto.ProtoInput"> & {
* Use `create(ProtoInputSchema)` to create a new message. * Use `create(ProtoInputSchema)` to create a new message.
*/ */
export const ProtoInputSchema: GenMessage<ProtoInput> = /*@__PURE__*/ export const ProtoInputSchema: GenMessage<ProtoInput> = /*@__PURE__*/
messageDesc(file_types, 7); messageDesc(file_types, 14);

View File

@@ -5,6 +5,7 @@ import {
SafeStream, SafeStream,
} from "./messages"; } from "./messages";
import { webSockets } from "@libp2p/websockets"; import { webSockets } from "@libp2p/websockets";
import { webTransport } from "@libp2p/webtransport";
import { createLibp2p, Libp2p } from "libp2p"; import { createLibp2p, Libp2p } from "libp2p";
import { noise } from "@chainsafe/libp2p-noise"; import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from "@chainsafe/libp2p-yamux"; import { yamux } from "@chainsafe/libp2p-yamux";
@@ -13,9 +14,6 @@ import { multiaddr } from "@multiformats/multiaddr";
import { Connection } from "@libp2p/interface"; import { Connection } from "@libp2p/interface";
import { ping } from "@libp2p/ping"; import { ping } from "@libp2p/ping";
//FIXME: Sometimes the room will wait to say offline, then appear to be online after retrying :D
// This works for me, with my trashy internet, does it work for you as well?
const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0"; const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0";
export class WebRTCStream { export class WebRTCStream {
@@ -30,8 +28,9 @@ export class WebRTCStream {
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined; private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined;
private _serverURL: string | undefined = undefined; private _serverURL: string | undefined = undefined;
private _roomName: string | undefined = undefined; private _roomName: string | undefined = undefined;
private _isConnected: boolean = false; // Add flag to track connection state private _isConnected: boolean = false;
currentFrameRate: number = 60; private _dataChannelCallbacks: Array<(data: any) => void> = [];
currentFrameRate: number = 100;
constructor( constructor(
serverURL: string, serverURL: string,
@@ -59,7 +58,7 @@ export class WebRTCStream {
console.log("Setting up libp2p"); console.log("Setting up libp2p");
this._p2p = await createLibp2p({ this._p2p = await createLibp2p({
transports: [webSockets()], transports: [webSockets(), webTransport()],
connectionEncrypters: [noise()], connectionEncrypters: [noise()],
streamMuxers: [yamux()], streamMuxers: [yamux()],
connectionGater: { connectionGater: {
@@ -219,7 +218,8 @@ export class WebRTCStream {
} }
private _checkConnectionState() { private _checkConnectionState() {
if (!this._pc) return; if (!this._pc || !this._p2p || !this._p2pConn)
return;
console.debug("Checking connection state:", { console.debug("Checking connection state:", {
connectionState: this._pc.connectionState, connectionState: this._pc.connectionState,
@@ -267,9 +267,9 @@ export class WebRTCStream {
this._pc.connectionState === "closed" || this._pc.connectionState === "closed" ||
this._pc.iceConnectionState === "failed" this._pc.iceConnectionState === "failed"
) { ) {
console.log("Connection failed or closed, attempting reconnect"); console.log("PeerConnection failed or closed");
this._isConnected = false; // Reset connected state //this._isConnected = false; // Reset connected state
this._handleConnectionFailure(); //this._handleConnectionFailure();
} }
} }
@@ -318,6 +318,7 @@ export class WebRTCStream {
console.error("Error closing data channel:", err); console.error("Error closing data channel:", err);
} }
this._dataChannel = undefined; this._dataChannel = undefined;
this._dataChannelCallbacks = [];
} }
this._isConnected = false; // Reset connected state during cleanup this._isConnected = false; // Reset connected state during cleanup
} }
@@ -329,15 +330,31 @@ export class WebRTCStream {
} }
} }
public addDataChannelCallback(callback: (data: any) => void) {
this._dataChannelCallbacks.push(callback);
}
public removeDataChannelCallback(callback: (data: any) => void) {
this._dataChannelCallbacks = this._dataChannelCallbacks.filter(cb => cb !== callback);
}
private _setupDataChannelEvents() { private _setupDataChannelEvents() {
if (!this._dataChannel) return; if (!this._dataChannel) return;
this._dataChannel.onclose = () => console.log("sendChannel has closed"); this._dataChannel.onclose = () => console.log("sendChannel has closed");
this._dataChannel.onopen = () => console.log("sendChannel has opened"); this._dataChannel.onopen = () => console.log("sendChannel has opened");
this._dataChannel.onmessage = (e) => this._dataChannel.onmessage = (event => {
console.log( // Parse as ProtoBuf message
`Message from DataChannel '${this._dataChannel?.label}' payload '${e.data}'`, const data = event.data;
); // Call registered callback if exists
this._dataChannelCallbacks.forEach((callback) => {
try {
callback(data);
} catch (err) {
console.error("Error in data channel callback:", err);
}
});
});
} }
private _gatherFrameRate() { private _gatherFrameRate() {

View File

@@ -0,0 +1,18 @@
diff --git a/bubblewrap.c b/bubblewrap.c
index f8728c7..42cfe2e 100644
--- a/bubblewrap.c
+++ b/bubblewrap.c
@@ -876,13 +876,6 @@ acquire_privs (void)
/* Keep only the required capabilities for setup */
set_required_caps ();
}
- else if (real_uid != 0 && has_caps ())
- {
- /* We have some capabilities in the non-setuid case, which should not happen.
- Probably caused by the binary being setcap instead of setuid which we
- don't support anymore */
- die ("Unexpected capabilities but not setuid, old file caps config?");
- }
else if (real_uid == 0)
{
/* If our uid is 0, default to inheriting all caps; the caller

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -19,17 +18,8 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -39,13 +29,7 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
@@ -57,16 +41,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -77,17 +52,12 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
@@ -103,8 +73,6 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg=
github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
@@ -112,15 +80,14 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y=
github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= github.com/koron/go-ssdp v0.1.0/go.mod h1:GltaDBjtK1kemZOusWYLGotV0kBeEf59Bp0wtSB0uyU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -130,39 +97,41 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784=
github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo=
github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE= github.com/libp2p/go-libp2p v0.44.0 h1:5Gtt8OrF8yiXmH+Mx4+/iBeFRMK1TY3a8OrEBDEqAvs=
github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI= github.com/libp2p/go-libp2p v0.44.0/go.mod h1:NovCojezAt4dnDd4fH048K7PKEqH0UFYYqJRjIIu8zc=
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8=
github.com/libp2p/go-libp2p-pubsub v0.13.1 h1:tV3ttzzZSCk0EtEXnxVmWIXgjVxXx+20Jwjbs/Ctzjo= github.com/libp2p/go-libp2p-pubsub v0.15.0 h1:cG7Cng2BT82WttmPFMi50gDNV+58K626m/wR00vGL1o=
github.com/libp2p/go-libp2p-pubsub v0.13.1/go.mod h1:MKPU5vMI8RRFyTP0HfdsF9cLmL1nHAeJm44AxJGJx44= github.com/libp2p/go-libp2p-pubsub v0.15.0/go.mod h1:lr4oE8bFgQaifRcoc2uWhWWiK6tPdOEKpUuR408GFN4=
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc=
github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA=
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po= github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE=
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o=
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q= github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marcopolo/simnet v0.0.1 h1:rSMslhPz6q9IvJeFWDoMGxMIrlsbXau3NkuIXHGJxfg=
github.com/marcopolo/simnet v0.0.1/go.mod h1:WDaQkgLAjqDUEBAOXz22+1j6wXKfGlC5sD5XWt3ddOs=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
@@ -183,36 +152,29 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo= github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw=
github.com/multiformats/go-multiaddr v0.15.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M=
github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc=
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc=
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI=
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ=
github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
@@ -222,29 +184,29 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.38 h1:Mgt3XIIq47uR5vcLLahfRucE6tFPjxHak+z5ZZFEzLU= github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw=
github.com/pion/interceptor v0.1.38/go.mod h1:HS9X+Ue5LDE6q2C2tuvOuO83XkBdJFgn6MBDtfoJX4Q= github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.8.15 h1:MuhuGn1cxpVCPLNY1lI7F1tQ8Spntpgf12ob+pOYT8s= github.com/pion/rtp v1.8.24 h1:+ICyZXUQDv95EsHN70RrA4XKJf5MGWyC6QQc1u6/ynI=
github.com/pion/rtp v1.8.15/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= github.com/pion/rtp v1.8.24/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo=
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
@@ -253,43 +215,36 @@ github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1A
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
github.com/pion/webrtc/v4 v4.1.1 h1:PMFPtLg1kpD2pVtun+LGUzA3k54JdFl87WO0Z1+HKug= github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw=
github.com/pion/webrtc/v4 v4.1.1/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4= github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA= github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao=
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
@@ -311,10 +266,8 @@ github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
@@ -323,15 +276,13 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
@@ -341,20 +292,20 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -369,22 +320,20 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -406,8 +355,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -423,17 +372,14 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -444,13 +390,14 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef h1:5xFtU4tmJMJSxSeDlr1dgBff2tDXrq0laLdS1EA3LYw=
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -467,24 +414,24 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -505,10 +452,9 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=

View File

@@ -30,29 +30,12 @@ func InitWebRTCAPI() error {
return fmt.Errorf("failed to register extensions: %w", err) return fmt.Errorf("failed to register extensions: %w", err)
} }
// Default codecs cover most of our needs // Default codecs cover our needs
err = mediaEngine.RegisterDefaultCodecs() err = mediaEngine.RegisterDefaultCodecs()
if err != nil { if err != nil {
return err return err
} }
// Add H.265 for special cases
videoRTCPFeedback := []webrtc.RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
for _, codec := range []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH265, ClockRate: 90000, RTCPFeedback: videoRTCPFeedback},
PayloadType: 48,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeRTX, ClockRate: 90000, SDPFmtpLine: "apt=48"},
PayloadType: 49,
},
} {
if err = mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
// Interceptor registry // Interceptor registry
interceptorRegistry := &interceptor.Registry{} interceptorRegistry := &interceptor.Registry{}
@@ -98,7 +81,8 @@ func InitWebRTCAPI() error {
slog.Info("Using WebRTC UDP Port Range", "start", flags.WebRTCUDPStart, "end", flags.WebRTCUDPEnd) slog.Info("Using WebRTC UDP Port Range", "start", flags.WebRTCUDPStart, "end", flags.WebRTCUDPEnd)
} }
settingEngine.SetIncludeLoopbackCandidate(true) // Just in case // Improves speed when sending offers to browsers (https://github.com/pion/webrtc/issues/3174)
settingEngine.SetIncludeLoopbackCandidate(true)
// Create a new API object with our customized settings // Create a new API object with our customized settings
globalWebRTCAPI = webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithSettingEngine(settingEngine), webrtc.WithInterceptorRegistry(interceptorRegistry)) globalWebRTCAPI = webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithSettingEngine(settingEngine), webrtc.WithInterceptorRegistry(interceptorRegistry))

View File

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

View File

@@ -4,24 +4,31 @@ import (
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"net/http"
"os" "os"
"relay/internal/common" "relay/internal/common"
"relay/internal/shared" "relay/internal/shared"
"time"
"github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p"
pubsub "github.com/libp2p/go-libp2p-pubsub" pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
"github.com/libp2p/go-libp2p/p2p/protocol/ping" "github.com/libp2p/go-libp2p/p2p/protocol/ping"
"github.com/libp2p/go-libp2p/p2p/security/noise" "github.com/libp2p/go-libp2p/p2p/security/noise"
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
"github.com/libp2p/go-libp2p/p2p/transport/tcp" "github.com/libp2p/go-libp2p/p2p/transport/tcp"
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket" ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
webtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
"github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multiaddr"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
) )
// -- Variables -- // -- Variables --
@@ -30,17 +37,9 @@ var globalRelay *Relay
// -- Structs -- // -- Structs --
// RelayInfo contains light information of Relay, in mesh-friendly format
type RelayInfo struct {
ID peer.ID
MeshAddrs []string // Addresses of this relay
MeshRooms *common.SafeMap[string, shared.RoomInfo] // Rooms hosted by this relay
MeshLatencies *common.SafeMap[string, time.Duration] // Latencies to other peers from this relay
}
// Relay structure enhanced with metrics and state // Relay structure enhanced with metrics and state
type Relay struct { type Relay struct {
RelayInfo *PeerInfo
Host host.Host // libp2p host for peer-to-peer networking Host host.Host // libp2p host for peer-to-peer networking
PubSub *pubsub.PubSub // PubSub for state synchronization PubSub *pubsub.PubSub // PubSub for state synchronization
@@ -48,7 +47,6 @@ type Relay struct {
// Local // Local
LocalRooms *common.SafeMap[ulid.ULID, *shared.Room] // room ID -> local Room struct (hosted by this relay) LocalRooms *common.SafeMap[ulid.ULID, *shared.Room] // room ID -> local Room struct (hosted by this relay)
LocalMeshPeers *common.SafeMap[peer.ID, *RelayInfo] // peer ID -> mesh peer relay info (connected to this relay)
LocalMeshConnections *common.SafeMap[peer.ID, *webrtc.PeerConnection] // peer ID -> PeerConnection (connected to this relay) LocalMeshConnections *common.SafeMap[peer.ID, *webrtc.PeerConnection] // peer ID -> PeerConnection (connected to this relay)
// Protocols // Protocols
@@ -60,11 +58,43 @@ type Relay struct {
} }
func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay, error) { func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay, error) {
// If metrics are enabled, start the metrics server first
metricsOpts := make([]libp2p.Option, 0)
var rmgr network.ResourceManager
if common.GetFlags().Metrics {
go func() {
slog.Info("Starting prometheus metrics server at '/debug/metrics/prometheus'", "port", common.GetFlags().MetricsPort)
http.Handle("/debug/metrics/prometheus", promhttp.Handler())
if err := http.ListenAndServe(fmt.Sprintf(":%d", common.GetFlags().MetricsPort), nil); err != nil {
slog.Error("Failed to start metrics server", "err", err)
}
}()
rcmgr.MustRegisterWith(prometheus.DefaultRegisterer)
str, err := rcmgr.NewStatsTraceReporter()
if err != nil {
log.Fatal(err)
}
rmgr, err = rcmgr.NewResourceManager(rcmgr.NewFixedLimiter(rcmgr.DefaultLimits.AutoScale()), rcmgr.WithTraceReporter(str))
if err != nil {
log.Fatal(err)
}
metricsOpts = append(metricsOpts, libp2p.ResourceManager(rmgr))
metricsOpts = append(metricsOpts, libp2p.PrometheusRegisterer(prometheus.DefaultRegisterer))
} else {
rmgr = nil
}
listenAddrs := []string{ listenAddrs := []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4 - Raw TCP fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4 - Raw TCP
fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6 - Raw TCP fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6 - Raw TCP
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/ws", port), // IPv4 - TCP WebSocket fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/ws", port), // IPv4 - TCP WebSocket
fmt.Sprintf("/ip6/::/tcp/%d/ws", port), // IPv6 - TCP WebSocket fmt.Sprintf("/ip6/::/tcp/%d/ws", port), // IPv6 - TCP WebSocket
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1/webtransport", port), // IPv4 - UDP QUIC WebTransport
fmt.Sprintf("/ip6/::/udp/%d/quic-v1/webtransport", port), // IPv6 - UDP QUIC WebTransport
} }
var muAddrs []multiaddr.Multiaddr var muAddrs []multiaddr.Multiaddr
@@ -78,11 +108,12 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
// Initialize libp2p host // Initialize libp2p host
p2pHost, err := libp2p.New( p2pHost, err := libp2p.New(
// TODO: Currently static identity libp2p.ChainOptions(metricsOpts...),
libp2p.Identity(identityKey), libp2p.Identity(identityKey),
// Enable required transports // Enable required transports
libp2p.Transport(tcp.NewTCPTransport), libp2p.Transport(tcp.NewTCPTransport),
libp2p.Transport(ws.New), libp2p.Transport(ws.New),
libp2p.Transport(webtransport.New),
// Other options // Other options
libp2p.ListenAddrs(muAddrs...), libp2p.ListenAddrs(muAddrs...),
libp2p.Security(noise.ID, noise.New), libp2p.Security(noise.ID, noise.New),
@@ -91,6 +122,7 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
libp2p.EnableNATService(), libp2p.EnableNATService(),
libp2p.EnableAutoNATv2(), libp2p.EnableAutoNATv2(),
libp2p.ShareTCPListener(), libp2p.ShareTCPListener(),
libp2p.QUICReuse(quicreuse.NewConnManager),
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create libp2p host for relay: %w", err) return nil, fmt.Errorf("failed to create libp2p host for relay: %w", err)
@@ -105,23 +137,13 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
// Initialize Ping Service // Initialize Ping Service
pingSvc := ping.NewPingService(p2pHost) pingSvc := ping.NewPingService(p2pHost)
var addresses []string
for _, addr := range p2pHost.Addrs() {
addresses = append(addresses, addr.String())
}
r := &Relay{ r := &Relay{
RelayInfo: RelayInfo{ PeerInfo: NewPeerInfo(p2pHost.ID(), p2pHost.Addrs()),
ID: p2pHost.ID(), Host: p2pHost,
MeshAddrs: addresses, PubSub: p2pPubsub,
MeshRooms: common.NewSafeMap[string, shared.RoomInfo](), PingService: pingSvc,
MeshLatencies: common.NewSafeMap[string, time.Duration](), LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](),
}, LocalMeshConnections: common.NewSafeMap[peer.ID, *webrtc.PeerConnection](),
Host: p2pHost,
PubSub: p2pPubsub,
PingService: pingSvc,
LocalRooms: common.NewSafeMap[ulid.ULID, *shared.Room](),
LocalMeshPeers: common.NewSafeMap[peer.ID, *RelayInfo](),
} }
// Add network notifier after relay is initialized // Add network notifier after relay is initialized
@@ -152,7 +174,7 @@ func NewRelay(ctx context.Context, port int, identityKey crypto.PrivKey) (*Relay
return r, nil return r, nil
} }
func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error { func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) (*Relay, error) {
var err error var err error
persistentDir := common.GetFlags().PersistDir persistentDir := common.GetFlags().PersistDir
@@ -164,7 +186,7 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
if hasIdentity { if hasIdentity {
_, err = os.Stat(persistentDir + "/identity.key") _, err = os.Stat(persistentDir + "/identity.key")
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to check identity key file: %w", err) return nil, fmt.Errorf("failed to check identity key file: %w", err)
} else if os.IsNotExist(err) { } else if os.IsNotExist(err) {
hasIdentity = false hasIdentity = false
} }
@@ -172,17 +194,17 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
if !hasIdentity { if !hasIdentity {
// Make sure the persistent directory exists // Make sure the persistent directory exists
if err = os.MkdirAll(persistentDir, 0700); err != nil { if err = os.MkdirAll(persistentDir, 0700); err != nil {
return fmt.Errorf("failed to create persistent data directory: %w", err) return nil, fmt.Errorf("failed to create persistent data directory: %w", err)
} }
// Generate // Generate
slog.Info("Generating new identity for relay") slog.Info("Generating new identity for relay")
privKey, err = common.GenerateED25519Key() privKey, err = common.GenerateED25519Key()
if err != nil { if err != nil {
return fmt.Errorf("failed to generate new identity: %w", err) return nil, fmt.Errorf("failed to generate new identity: %w", err)
} }
// Save the key // Save the key
if err = common.SaveED25519Key(privKey, persistentDir+"/identity.key"); err != nil { if err = common.SaveED25519Key(privKey, persistentDir+"/identity.key"); err != nil {
return fmt.Errorf("failed to save identity key: %w", err) return nil, fmt.Errorf("failed to save identity key: %w", err)
} }
slog.Info("New identity generated and saved", "path", persistentDir+"/identity.key") slog.Info("New identity generated and saved", "path", persistentDir+"/identity.key")
} else { } else {
@@ -190,25 +212,45 @@ func InitRelay(ctx context.Context, ctxCancel context.CancelFunc) error {
// Load the key // Load the key
privKey, err = common.LoadED25519Key(persistentDir + "/identity.key") privKey, err = common.LoadED25519Key(persistentDir + "/identity.key")
if err != nil { if err != nil {
return fmt.Errorf("failed to load identity key: %w", err) return nil, fmt.Errorf("failed to load identity key: %w", err)
} }
} }
// Convert to libp2p crypto.PrivKey // Convert to libp2p crypto.PrivKey
identityKey, err = crypto.UnmarshalEd25519PrivateKey(privKey) identityKey, err = crypto.UnmarshalEd25519PrivateKey(privKey)
if err != nil { if err != nil {
return fmt.Errorf("failed to unmarshal ED25519 private key: %w", err) return nil, fmt.Errorf("failed to unmarshal ED25519 private key: %w", err)
} }
globalRelay, err = NewRelay(ctx, common.GetFlags().EndpointPort, identityKey) globalRelay, err = NewRelay(ctx, common.GetFlags().EndpointPort, identityKey)
if err != nil { if err != nil {
return fmt.Errorf("failed to create relay: %w", err) return nil, fmt.Errorf("failed to create relay: %w", err)
} }
if err = common.InitWebRTCAPI(); err != nil { if err = common.InitWebRTCAPI(); err != nil {
return err return nil, err
} }
slog.Info("Relay initialized", "id", globalRelay.ID) slog.Info("Relay initialized", "id", globalRelay.ID)
return nil
// Load previous peers on startup
defaultFile := common.GetFlags().PersistDir + "/peerstore.json"
if err = globalRelay.LoadFromFile(defaultFile); err != nil {
slog.Warn("Failed to load previous peer store", "error", err)
} else {
globalRelay.Peers.Range(func(id peer.ID, pi *PeerInfo) bool {
if len(pi.Addrs) <= 0 {
slog.Warn("Peer from peer store has no addresses", "peer", id)
return true
}
// Connect to first address only
if err = globalRelay.ConnectToPeer(context.Background(), pi.Addrs[0]); err != nil {
slog.Error("Failed to connect to peer from peer store", "peer", id, "error", err)
}
return true
})
}
return globalRelay, nil
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
package core
import (
"errors"
"log/slog"
"os"
"relay/internal/common"
"relay/internal/shared"
"time"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
)
// PeerInfo contains information of a peer, in light transmit-friendly format
type PeerInfo struct {
ID peer.ID
Addrs []multiaddr.Multiaddr // Addresses of this peer
Peers *common.SafeMap[peer.ID, *PeerInfo] // Peers connected to this peer
Latencies *common.SafeMap[peer.ID, time.Duration] // Latencies to other peers from this peer
Rooms *common.SafeMap[string, shared.RoomInfo] // Rooms this peer is part of or owner of
}
func NewPeerInfo(id peer.ID, addrs []multiaddr.Multiaddr) *PeerInfo {
return &PeerInfo{
ID: id,
Addrs: addrs,
Peers: common.NewSafeMap[peer.ID, *PeerInfo](),
Latencies: common.NewSafeMap[peer.ID, time.Duration](),
Rooms: common.NewSafeMap[string, shared.RoomInfo](),
}
}
// SaveToFile saves the peer store to a JSON file in persistent path
func (pi *PeerInfo) SaveToFile(filePath string) error {
if len(filePath) <= 0 {
return errors.New("filepath is not set")
}
// Marshal the peer store to JSON array (we don't need to store IDs..)
data, err := pi.Peers.MarshalJSON()
if err != nil {
return errors.New("failed to marshal peer store data: " + err.Error())
}
// Save the data to a file
if err = os.WriteFile(filePath, data, 0644); err != nil {
return errors.New("failed to save peer store to file: " + err.Error())
}
slog.Info("PeerStore saved to file", "path", filePath)
return nil
}
// LoadFromFile loads the peer store from a JSON file in persistent path
func (pi *PeerInfo) LoadFromFile(filePath string) error {
if len(filePath) <= 0 {
return errors.New("filepath is not set")
}
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
slog.Info("PeerStore file does not exist, starting with empty store")
return nil // No peers to load
}
return errors.New("failed to read peer store file: " + err.Error())
}
// Unmarshal the JSON data into the peer store
if err = pi.Peers.UnmarshalJSON(data); err != nil {
return errors.New("failed to unmarshal peer store data: " + err.Error())
}
slog.Info("PeerStore loaded from file", "path", filePath)
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.6 // protoc-gen-go v1.36.10
// protoc (unknown) // protoc (unknown)
// source: types.proto // source: types.proto
@@ -416,6 +416,481 @@ func (x *ProtoKeyUp) GetKey() int32 {
return 0 return 0
} }
// ControllerAttach message
type ProtoControllerAttach struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerAttach"
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` // One of the following enums: "ps", "xbox" or "switch"
Slot int32 `protobuf:"varint,3,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProtoControllerAttach) Reset() {
*x = ProtoControllerAttach{}
mi := &file_types_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProtoControllerAttach) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProtoControllerAttach) ProtoMessage() {}
func (x *ProtoControllerAttach) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProtoControllerAttach.ProtoReflect.Descriptor instead.
func (*ProtoControllerAttach) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{7}
}
func (x *ProtoControllerAttach) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *ProtoControllerAttach) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *ProtoControllerAttach) GetSlot() int32 {
if x != nil {
return x.Slot
}
return 0
}
// ControllerDetach message
type ProtoControllerDetach struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerDetach"
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProtoControllerDetach) Reset() {
*x = ProtoControllerDetach{}
mi := &file_types_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProtoControllerDetach) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProtoControllerDetach) ProtoMessage() {}
func (x *ProtoControllerDetach) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProtoControllerDetach.ProtoReflect.Descriptor instead.
func (*ProtoControllerDetach) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{8}
}
func (x *ProtoControllerDetach) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *ProtoControllerDetach) GetSlot() int32 {
if x != nil {
return x.Slot
}
return 0
}
// ControllerButton message
type ProtoControllerButton struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerButtons"
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
Button int32 `protobuf:"varint,3,opt,name=button,proto3" json:"button,omitempty"` // Button code (linux input event code)
Pressed bool `protobuf:"varint,4,opt,name=pressed,proto3" json:"pressed,omitempty"` // true if pressed, false if released
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProtoControllerButton) Reset() {
*x = ProtoControllerButton{}
mi := &file_types_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProtoControllerButton) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProtoControllerButton) ProtoMessage() {}
func (x *ProtoControllerButton) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProtoControllerButton.ProtoReflect.Descriptor instead.
func (*ProtoControllerButton) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{9}
}
func (x *ProtoControllerButton) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *ProtoControllerButton) GetSlot() int32 {
if x != nil {
return x.Slot
}
return 0
}
func (x *ProtoControllerButton) GetButton() int32 {
if x != nil {
return x.Button
}
return 0
}
func (x *ProtoControllerButton) GetPressed() bool {
if x != nil {
return x.Pressed
}
return false
}
// ControllerTriggers message
type ProtoControllerTrigger struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerTriggers"
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
Trigger int32 `protobuf:"varint,3,opt,name=trigger,proto3" json:"trigger,omitempty"` // Trigger number (0 for left, 1 for right)
Value int32 `protobuf:"varint,4,opt,name=value,proto3" json:"value,omitempty"` // trigger value (-32768 to 32767)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProtoControllerTrigger) Reset() {
*x = ProtoControllerTrigger{}
mi := &file_types_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProtoControllerTrigger) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProtoControllerTrigger) ProtoMessage() {}
func (x *ProtoControllerTrigger) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProtoControllerTrigger.ProtoReflect.Descriptor instead.
func (*ProtoControllerTrigger) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{10}
}
func (x *ProtoControllerTrigger) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *ProtoControllerTrigger) GetSlot() int32 {
if x != nil {
return x.Slot
}
return 0
}
func (x *ProtoControllerTrigger) GetTrigger() int32 {
if x != nil {
return x.Trigger
}
return 0
}
func (x *ProtoControllerTrigger) GetValue() int32 {
if x != nil {
return x.Value
}
return 0
}
// ControllerSticks message
type ProtoControllerStick struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerStick"
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
Stick int32 `protobuf:"varint,3,opt,name=stick,proto3" json:"stick,omitempty"` // Stick number (0 for left, 1 for right)
X int32 `protobuf:"varint,4,opt,name=x,proto3" json:"x,omitempty"` // X axis value (-32768 to 32767)
Y int32 `protobuf:"varint,5,opt,name=y,proto3" json:"y,omitempty"` // Y axis value (-32768 to 32767)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProtoControllerStick) Reset() {
*x = ProtoControllerStick{}
mi := &file_types_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProtoControllerStick) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProtoControllerStick) ProtoMessage() {}
func (x *ProtoControllerStick) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProtoControllerStick.ProtoReflect.Descriptor instead.
func (*ProtoControllerStick) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{11}
}
func (x *ProtoControllerStick) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *ProtoControllerStick) GetSlot() int32 {
if x != nil {
return x.Slot
}
return 0
}
func (x *ProtoControllerStick) GetStick() int32 {
if x != nil {
return x.Stick
}
return 0
}
func (x *ProtoControllerStick) GetX() int32 {
if x != nil {
return x.X
}
return 0
}
func (x *ProtoControllerStick) GetY() int32 {
if x != nil {
return x.Y
}
return 0
}
// ControllerAxis message
type ProtoControllerAxis struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerAxis"
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
Axis int32 `protobuf:"varint,3,opt,name=axis,proto3" json:"axis,omitempty"` // Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
Value int32 `protobuf:"varint,4,opt,name=value,proto3" json:"value,omitempty"` // axis value (-1 to 1)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProtoControllerAxis) Reset() {
*x = ProtoControllerAxis{}
mi := &file_types_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProtoControllerAxis) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProtoControllerAxis) ProtoMessage() {}
func (x *ProtoControllerAxis) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProtoControllerAxis.ProtoReflect.Descriptor instead.
func (*ProtoControllerAxis) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{12}
}
func (x *ProtoControllerAxis) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *ProtoControllerAxis) GetSlot() int32 {
if x != nil {
return x.Slot
}
return 0
}
func (x *ProtoControllerAxis) GetAxis() int32 {
if x != nil {
return x.Axis
}
return 0
}
func (x *ProtoControllerAxis) GetValue() int32 {
if x != nil {
return x.Value
}
return 0
}
// ControllerRumble message
type ProtoControllerRumble struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // Fixed value "ControllerRumble"
Slot int32 `protobuf:"varint,2,opt,name=slot,proto3" json:"slot,omitempty"` // Slot number (0-3)
LowFrequency int32 `protobuf:"varint,3,opt,name=low_frequency,json=lowFrequency,proto3" json:"low_frequency,omitempty"` // Low frequency rumble (0-65535)
HighFrequency int32 `protobuf:"varint,4,opt,name=high_frequency,json=highFrequency,proto3" json:"high_frequency,omitempty"` // High frequency rumble (0-65535)
Duration int32 `protobuf:"varint,5,opt,name=duration,proto3" json:"duration,omitempty"` // Duration in milliseconds
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProtoControllerRumble) Reset() {
*x = ProtoControllerRumble{}
mi := &file_types_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProtoControllerRumble) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProtoControllerRumble) ProtoMessage() {}
func (x *ProtoControllerRumble) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProtoControllerRumble.ProtoReflect.Descriptor instead.
func (*ProtoControllerRumble) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{13}
}
func (x *ProtoControllerRumble) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *ProtoControllerRumble) GetSlot() int32 {
if x != nil {
return x.Slot
}
return 0
}
func (x *ProtoControllerRumble) GetLowFrequency() int32 {
if x != nil {
return x.LowFrequency
}
return 0
}
func (x *ProtoControllerRumble) GetHighFrequency() int32 {
if x != nil {
return x.HighFrequency
}
return 0
}
func (x *ProtoControllerRumble) GetDuration() int32 {
if x != nil {
return x.Duration
}
return 0
}
// Union of all Input types // Union of all Input types
type ProtoInput struct { type ProtoInput struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@@ -428,6 +903,13 @@ type ProtoInput struct {
// *ProtoInput_MouseKeyUp // *ProtoInput_MouseKeyUp
// *ProtoInput_KeyDown // *ProtoInput_KeyDown
// *ProtoInput_KeyUp // *ProtoInput_KeyUp
// *ProtoInput_ControllerAttach
// *ProtoInput_ControllerDetach
// *ProtoInput_ControllerButton
// *ProtoInput_ControllerTrigger
// *ProtoInput_ControllerStick
// *ProtoInput_ControllerAxis
// *ProtoInput_ControllerRumble
InputType isProtoInput_InputType `protobuf_oneof:"input_type"` InputType isProtoInput_InputType `protobuf_oneof:"input_type"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -435,7 +917,7 @@ type ProtoInput struct {
func (x *ProtoInput) Reset() { func (x *ProtoInput) Reset() {
*x = ProtoInput{} *x = ProtoInput{}
mi := &file_types_proto_msgTypes[7] mi := &file_types_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -447,7 +929,7 @@ func (x *ProtoInput) String() string {
func (*ProtoInput) ProtoMessage() {} func (*ProtoInput) ProtoMessage() {}
func (x *ProtoInput) ProtoReflect() protoreflect.Message { func (x *ProtoInput) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[7] mi := &file_types_proto_msgTypes[14]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -460,7 +942,7 @@ func (x *ProtoInput) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProtoInput.ProtoReflect.Descriptor instead. // Deprecated: Use ProtoInput.ProtoReflect.Descriptor instead.
func (*ProtoInput) Descriptor() ([]byte, []int) { func (*ProtoInput) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{7} return file_types_proto_rawDescGZIP(), []int{14}
} }
func (x *ProtoInput) GetInputType() isProtoInput_InputType { func (x *ProtoInput) GetInputType() isProtoInput_InputType {
@@ -533,6 +1015,69 @@ func (x *ProtoInput) GetKeyUp() *ProtoKeyUp {
return nil return nil
} }
func (x *ProtoInput) GetControllerAttach() *ProtoControllerAttach {
if x != nil {
if x, ok := x.InputType.(*ProtoInput_ControllerAttach); ok {
return x.ControllerAttach
}
}
return nil
}
func (x *ProtoInput) GetControllerDetach() *ProtoControllerDetach {
if x != nil {
if x, ok := x.InputType.(*ProtoInput_ControllerDetach); ok {
return x.ControllerDetach
}
}
return nil
}
func (x *ProtoInput) GetControllerButton() *ProtoControllerButton {
if x != nil {
if x, ok := x.InputType.(*ProtoInput_ControllerButton); ok {
return x.ControllerButton
}
}
return nil
}
func (x *ProtoInput) GetControllerTrigger() *ProtoControllerTrigger {
if x != nil {
if x, ok := x.InputType.(*ProtoInput_ControllerTrigger); ok {
return x.ControllerTrigger
}
}
return nil
}
func (x *ProtoInput) GetControllerStick() *ProtoControllerStick {
if x != nil {
if x, ok := x.InputType.(*ProtoInput_ControllerStick); ok {
return x.ControllerStick
}
}
return nil
}
func (x *ProtoInput) GetControllerAxis() *ProtoControllerAxis {
if x != nil {
if x, ok := x.InputType.(*ProtoInput_ControllerAxis); ok {
return x.ControllerAxis
}
}
return nil
}
func (x *ProtoInput) GetControllerRumble() *ProtoControllerRumble {
if x != nil {
if x, ok := x.InputType.(*ProtoInput_ControllerRumble); ok {
return x.ControllerRumble
}
}
return nil
}
type isProtoInput_InputType interface { type isProtoInput_InputType interface {
isProtoInput_InputType() isProtoInput_InputType()
} }
@@ -565,6 +1110,34 @@ type ProtoInput_KeyUp struct {
KeyUp *ProtoKeyUp `protobuf:"bytes,7,opt,name=key_up,json=keyUp,proto3,oneof"` KeyUp *ProtoKeyUp `protobuf:"bytes,7,opt,name=key_up,json=keyUp,proto3,oneof"`
} }
type ProtoInput_ControllerAttach struct {
ControllerAttach *ProtoControllerAttach `protobuf:"bytes,8,opt,name=controller_attach,json=controllerAttach,proto3,oneof"`
}
type ProtoInput_ControllerDetach struct {
ControllerDetach *ProtoControllerDetach `protobuf:"bytes,9,opt,name=controller_detach,json=controllerDetach,proto3,oneof"`
}
type ProtoInput_ControllerButton struct {
ControllerButton *ProtoControllerButton `protobuf:"bytes,10,opt,name=controller_button,json=controllerButton,proto3,oneof"`
}
type ProtoInput_ControllerTrigger struct {
ControllerTrigger *ProtoControllerTrigger `protobuf:"bytes,11,opt,name=controller_trigger,json=controllerTrigger,proto3,oneof"`
}
type ProtoInput_ControllerStick struct {
ControllerStick *ProtoControllerStick `protobuf:"bytes,12,opt,name=controller_stick,json=controllerStick,proto3,oneof"`
}
type ProtoInput_ControllerAxis struct {
ControllerAxis *ProtoControllerAxis `protobuf:"bytes,13,opt,name=controller_axis,json=controllerAxis,proto3,oneof"`
}
type ProtoInput_ControllerRumble struct {
ControllerRumble *ProtoControllerRumble `protobuf:"bytes,14,opt,name=controller_rumble,json=controllerRumble,proto3,oneof"`
}
func (*ProtoInput_MouseMove) isProtoInput_InputType() {} func (*ProtoInput_MouseMove) isProtoInput_InputType() {}
func (*ProtoInput_MouseMoveAbs) isProtoInput_InputType() {} func (*ProtoInput_MouseMoveAbs) isProtoInput_InputType() {}
@@ -579,6 +1152,20 @@ func (*ProtoInput_KeyDown) isProtoInput_InputType() {}
func (*ProtoInput_KeyUp) isProtoInput_InputType() {} func (*ProtoInput_KeyUp) isProtoInput_InputType() {}
func (*ProtoInput_ControllerAttach) isProtoInput_InputType() {}
func (*ProtoInput_ControllerDetach) isProtoInput_InputType() {}
func (*ProtoInput_ControllerButton) isProtoInput_InputType() {}
func (*ProtoInput_ControllerTrigger) isProtoInput_InputType() {}
func (*ProtoInput_ControllerStick) isProtoInput_InputType() {}
func (*ProtoInput_ControllerAxis) isProtoInput_InputType() {}
func (*ProtoInput_ControllerRumble) isProtoInput_InputType() {}
var File_types_proto protoreflect.FileDescriptor var File_types_proto protoreflect.FileDescriptor
const file_types_proto_rawDesc = "" + const file_types_proto_rawDesc = "" +
@@ -608,7 +1195,41 @@ const file_types_proto_rawDesc = "" +
"\n" + "\n" +
"ProtoKeyUp\x12\x12\n" + "ProtoKeyUp\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" +
"\x03key\x18\x02 \x01(\x05R\x03key\"\xab\x03\n" + "\x03key\x18\x02 \x01(\x05R\x03key\"O\n" +
"\x15ProtoControllerAttach\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x0e\n" +
"\x02id\x18\x02 \x01(\tR\x02id\x12\x12\n" +
"\x04slot\x18\x03 \x01(\x05R\x04slot\"?\n" +
"\x15ProtoControllerDetach\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04slot\x18\x02 \x01(\x05R\x04slot\"q\n" +
"\x15ProtoControllerButton\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x16\n" +
"\x06button\x18\x03 \x01(\x05R\x06button\x12\x18\n" +
"\apressed\x18\x04 \x01(\bR\apressed\"p\n" +
"\x16ProtoControllerTrigger\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x18\n" +
"\atrigger\x18\x03 \x01(\x05R\atrigger\x12\x14\n" +
"\x05value\x18\x04 \x01(\x05R\x05value\"p\n" +
"\x14ProtoControllerStick\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x14\n" +
"\x05stick\x18\x03 \x01(\x05R\x05stick\x12\f\n" +
"\x01x\x18\x04 \x01(\x05R\x01x\x12\f\n" +
"\x01y\x18\x05 \x01(\x05R\x01y\"g\n" +
"\x13ProtoControllerAxis\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12\x12\n" +
"\x04axis\x18\x03 \x01(\x05R\x04axis\x12\x14\n" +
"\x05value\x18\x04 \x01(\x05R\x05value\"\xa7\x01\n" +
"\x15ProtoControllerRumble\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04slot\x18\x02 \x01(\x05R\x04slot\x12#\n" +
"\rlow_frequency\x18\x03 \x01(\x05R\flowFrequency\x12%\n" +
"\x0ehigh_frequency\x18\x04 \x01(\x05R\rhighFrequency\x12\x1a\n" +
"\bduration\x18\x05 \x01(\x05R\bduration\"\xc0\a\n" +
"\n" + "\n" +
"ProtoInput\x126\n" + "ProtoInput\x126\n" +
"\n" + "\n" +
@@ -620,7 +1241,15 @@ const file_types_proto_rawDesc = "" +
"\fmouse_key_up\x18\x05 \x01(\v2\x16.proto.ProtoMouseKeyUpH\x00R\n" + "\fmouse_key_up\x18\x05 \x01(\v2\x16.proto.ProtoMouseKeyUpH\x00R\n" +
"mouseKeyUp\x120\n" + "mouseKeyUp\x120\n" +
"\bkey_down\x18\x06 \x01(\v2\x13.proto.ProtoKeyDownH\x00R\akeyDown\x12*\n" + "\bkey_down\x18\x06 \x01(\v2\x13.proto.ProtoKeyDownH\x00R\akeyDown\x12*\n" +
"\x06key_up\x18\a \x01(\v2\x11.proto.ProtoKeyUpH\x00R\x05keyUpB\f\n" + "\x06key_up\x18\a \x01(\v2\x11.proto.ProtoKeyUpH\x00R\x05keyUp\x12K\n" +
"\x11controller_attach\x18\b \x01(\v2\x1c.proto.ProtoControllerAttachH\x00R\x10controllerAttach\x12K\n" +
"\x11controller_detach\x18\t \x01(\v2\x1c.proto.ProtoControllerDetachH\x00R\x10controllerDetach\x12K\n" +
"\x11controller_button\x18\n" +
" \x01(\v2\x1c.proto.ProtoControllerButtonH\x00R\x10controllerButton\x12N\n" +
"\x12controller_trigger\x18\v \x01(\v2\x1d.proto.ProtoControllerTriggerH\x00R\x11controllerTrigger\x12H\n" +
"\x10controller_stick\x18\f \x01(\v2\x1b.proto.ProtoControllerStickH\x00R\x0fcontrollerStick\x12E\n" +
"\x0fcontroller_axis\x18\r \x01(\v2\x1a.proto.ProtoControllerAxisH\x00R\x0econtrollerAxis\x12K\n" +
"\x11controller_rumble\x18\x0e \x01(\v2\x1c.proto.ProtoControllerRumbleH\x00R\x10controllerRumbleB\f\n" +
"\n" + "\n" +
"input_typeB\x16Z\x14relay/internal/protob\x06proto3" "input_typeB\x16Z\x14relay/internal/protob\x06proto3"
@@ -636,30 +1265,44 @@ func file_types_proto_rawDescGZIP() []byte {
return file_types_proto_rawDescData return file_types_proto_rawDescData
} }
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
var file_types_proto_goTypes = []any{ var file_types_proto_goTypes = []any{
(*ProtoMouseMove)(nil), // 0: proto.ProtoMouseMove (*ProtoMouseMove)(nil), // 0: proto.ProtoMouseMove
(*ProtoMouseMoveAbs)(nil), // 1: proto.ProtoMouseMoveAbs (*ProtoMouseMoveAbs)(nil), // 1: proto.ProtoMouseMoveAbs
(*ProtoMouseWheel)(nil), // 2: proto.ProtoMouseWheel (*ProtoMouseWheel)(nil), // 2: proto.ProtoMouseWheel
(*ProtoMouseKeyDown)(nil), // 3: proto.ProtoMouseKeyDown (*ProtoMouseKeyDown)(nil), // 3: proto.ProtoMouseKeyDown
(*ProtoMouseKeyUp)(nil), // 4: proto.ProtoMouseKeyUp (*ProtoMouseKeyUp)(nil), // 4: proto.ProtoMouseKeyUp
(*ProtoKeyDown)(nil), // 5: proto.ProtoKeyDown (*ProtoKeyDown)(nil), // 5: proto.ProtoKeyDown
(*ProtoKeyUp)(nil), // 6: proto.ProtoKeyUp (*ProtoKeyUp)(nil), // 6: proto.ProtoKeyUp
(*ProtoInput)(nil), // 7: proto.ProtoInput (*ProtoControllerAttach)(nil), // 7: proto.ProtoControllerAttach
(*ProtoControllerDetach)(nil), // 8: proto.ProtoControllerDetach
(*ProtoControllerButton)(nil), // 9: proto.ProtoControllerButton
(*ProtoControllerTrigger)(nil), // 10: proto.ProtoControllerTrigger
(*ProtoControllerStick)(nil), // 11: proto.ProtoControllerStick
(*ProtoControllerAxis)(nil), // 12: proto.ProtoControllerAxis
(*ProtoControllerRumble)(nil), // 13: proto.ProtoControllerRumble
(*ProtoInput)(nil), // 14: proto.ProtoInput
} }
var file_types_proto_depIdxs = []int32{ var file_types_proto_depIdxs = []int32{
0, // 0: proto.ProtoInput.mouse_move:type_name -> proto.ProtoMouseMove 0, // 0: proto.ProtoInput.mouse_move:type_name -> proto.ProtoMouseMove
1, // 1: proto.ProtoInput.mouse_move_abs:type_name -> proto.ProtoMouseMoveAbs 1, // 1: proto.ProtoInput.mouse_move_abs:type_name -> proto.ProtoMouseMoveAbs
2, // 2: proto.ProtoInput.mouse_wheel:type_name -> proto.ProtoMouseWheel 2, // 2: proto.ProtoInput.mouse_wheel:type_name -> proto.ProtoMouseWheel
3, // 3: proto.ProtoInput.mouse_key_down:type_name -> proto.ProtoMouseKeyDown 3, // 3: proto.ProtoInput.mouse_key_down:type_name -> proto.ProtoMouseKeyDown
4, // 4: proto.ProtoInput.mouse_key_up:type_name -> proto.ProtoMouseKeyUp 4, // 4: proto.ProtoInput.mouse_key_up:type_name -> proto.ProtoMouseKeyUp
5, // 5: proto.ProtoInput.key_down:type_name -> proto.ProtoKeyDown 5, // 5: proto.ProtoInput.key_down:type_name -> proto.ProtoKeyDown
6, // 6: proto.ProtoInput.key_up:type_name -> proto.ProtoKeyUp 6, // 6: proto.ProtoInput.key_up:type_name -> proto.ProtoKeyUp
7, // [7:7] is the sub-list for method output_type 7, // 7: proto.ProtoInput.controller_attach:type_name -> proto.ProtoControllerAttach
7, // [7:7] is the sub-list for method input_type 8, // 8: proto.ProtoInput.controller_detach:type_name -> proto.ProtoControllerDetach
7, // [7:7] is the sub-list for extension type_name 9, // 9: proto.ProtoInput.controller_button:type_name -> proto.ProtoControllerButton
7, // [7:7] is the sub-list for extension extendee 10, // 10: proto.ProtoInput.controller_trigger:type_name -> proto.ProtoControllerTrigger
0, // [0:7] is the sub-list for field type_name 11, // 11: proto.ProtoInput.controller_stick:type_name -> proto.ProtoControllerStick
12, // 12: proto.ProtoInput.controller_axis:type_name -> proto.ProtoControllerAxis
13, // 13: proto.ProtoInput.controller_rumble:type_name -> proto.ProtoControllerRumble
14, // [14:14] is the sub-list for method output_type
14, // [14:14] is the sub-list for method input_type
14, // [14:14] is the sub-list for extension type_name
14, // [14:14] is the sub-list for extension extendee
0, // [0:14] is the sub-list for field type_name
} }
func init() { file_types_proto_init() } func init() { file_types_proto_init() }
@@ -667,7 +1310,7 @@ func file_types_proto_init() {
if File_types_proto != nil { if File_types_proto != nil {
return return
} }
file_types_proto_msgTypes[7].OneofWrappers = []any{ file_types_proto_msgTypes[14].OneofWrappers = []any{
(*ProtoInput_MouseMove)(nil), (*ProtoInput_MouseMove)(nil),
(*ProtoInput_MouseMoveAbs)(nil), (*ProtoInput_MouseMoveAbs)(nil),
(*ProtoInput_MouseWheel)(nil), (*ProtoInput_MouseWheel)(nil),
@@ -675,6 +1318,13 @@ func file_types_proto_init() {
(*ProtoInput_MouseKeyUp)(nil), (*ProtoInput_MouseKeyUp)(nil),
(*ProtoInput_KeyDown)(nil), (*ProtoInput_KeyDown)(nil),
(*ProtoInput_KeyUp)(nil), (*ProtoInput_KeyUp)(nil),
(*ProtoInput_ControllerAttach)(nil),
(*ProtoInput_ControllerDetach)(nil),
(*ProtoInput_ControllerButton)(nil),
(*ProtoInput_ControllerTrigger)(nil),
(*ProtoInput_ControllerStick)(nil),
(*ProtoInput_ControllerAxis)(nil),
(*ProtoInput_ControllerRumble)(nil),
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@@ -682,7 +1332,7 @@ func file_types_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 8, NumMessages: 15,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@@ -49,25 +49,12 @@ func (r *Room) removeParticipantByID(pID ulid.ULID) {
} }
} }
// Removes all participants from a Room
/*func (r *Room) removeAllParticipants() {
for id, participant := range r.Participants.Copy() {
if err := r.signalParticipantOffline(participant); err != nil {
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
}
r.Participants.Delete(id)
slog.Debug("Removed participant from room", "participant", id, "room", r.Name)
}
}*/
// IsOnline checks if the room is online (has both audio and video tracks) // IsOnline checks if the room is online (has both audio and video tracks)
func (r *Room) IsOnline() bool { func (r *Room) IsOnline() bool {
return r.AudioTrack != nil && r.VideoTrack != nil return r.AudioTrack != nil && r.VideoTrack != nil
} }
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) { func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
//oldOnline := r.IsOnline()
switch trackType { switch trackType {
case webrtc.RTPCodecTypeAudio: case webrtc.RTPCodecTypeAudio:
r.AudioTrack = track r.AudioTrack = track
@@ -76,69 +63,4 @@ func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalS
default: default:
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType) slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
} }
/*newOnline := r.IsOnline()
if oldOnline != newOnline {
if newOnline {
slog.Debug("Room online, participants will be signaled", "room", r.Name)
r.signalParticipantsWithTracks()
} else {
slog.Debug("Room offline, signaling participants", "room", r.Name)
r.signalParticipantsOffline()
}
// TODO: Publish updated state to mesh
go func() {
if err := r.Relay.publishRoomStates(context.Background()); err != nil {
slog.Error("Failed to publish room states on change", "room", r.Name, "err", err)
}
}()
}*/
} }
/* TODO: libp2p'ify
func (r *Room) signalParticipantsWithTracks() {
for _, participant := range r.Participants.Copy() {
if err := r.signalParticipantWithTracks(participant); err != nil {
slog.Error("Failed to signal participant with tracks", "participant", participant.ID, "room", r.Name, "err", err)
}
}
}
func (r *Room) signalParticipantWithTracks(participant *Participant) error {
if r.AudioTrack != nil {
if err := participant.addTrack(r.AudioTrack); err != nil {
return fmt.Errorf("failed to add audio track: %w", err)
}
}
if r.VideoTrack != nil {
if err := participant.addTrack(r.VideoTrack); err != nil {
return fmt.Errorf("failed to add video track: %w", err)
}
}
if err := participant.signalOffer(); err != nil {
return fmt.Errorf("failed to signal offer: %w", err)
}
return nil
}
func (r *Room) signalParticipantsOffline() {
for _, participant := range r.Participants.Copy() {
if err := r.signalParticipantOffline(participant); err != nil {
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
}
}
}
// signalParticipantOffline signals a single participant offline
func (r *Room) signalParticipantOffline(participant *Participant) error {
// Skip if websocket is nil or closed
if participant.WebSocket == nil || participant.WebSocket.IsClosed() {
return nil
}
if err := participant.WebSocket.SendAnswerMessageWS(connections.AnswerOffline); err != nil {
return err
}
return nil
}
*/

View File

@@ -32,7 +32,7 @@ func main() {
slog.SetDefault(logger) slog.SetDefault(logger)
// Start relay // Start relay
err := core.InitRelay(mainCtx, mainStopper) relay, err := core.InitRelay(mainCtx, mainStopper)
if err != nil { if err != nil {
slog.Error("Failed to initialize relay", "err", err) slog.Error("Failed to initialize relay", "err", err)
mainStopper() mainStopper()
@@ -41,5 +41,10 @@ func main() {
// Wait for exit signal // Wait for exit signal
<-mainCtx.Done() <-mainCtx.Done()
slog.Info("Shutting down gracefully by signal...") slog.Info("Shutting down gracefully by signal..")
defaultFile := common.GetFlags().PersistDir + "/peerstore.json"
if err = relay.SaveToFile(defaultFile); err != nil {
slog.Error("Failed to save peer store", "err", err)
}
} }

View File

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

View File

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

View File

@@ -31,6 +31,9 @@ parse_resolution() {
MAX_RETRIES=3 MAX_RETRIES=3
RETRY_COUNT=0 RETRY_COUNT=0
WAYLAND_READY_DELAY=3 WAYLAND_READY_DELAY=3
ENTCMD_PREFIX=""
PRELOAD_SHIM_64=/usr/lib64/libvimputti_shim.so
PRELOAD_SHIM_32=/usr/lib32/libvimputti_shim.so
# Kills process if running # Kills process if running
kill_if_running() { kill_if_running() {
@@ -43,83 +46,49 @@ kill_if_running() {
fi fi
} }
# Starts up Steam namespace-less live-patcher
start_steam_namespaceless_patcher() {
kill_if_running "${PATCHER_PID:-}" "steam-patcher"
local entrypoints=(
"${HOME}/.local/share/Steam/steamrt64/steam-runtime-steamrt/_v2-entry-point"
"${HOME}/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/_v2-entry-point"
"${HOME}/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point"
# < Add more entrypoints here if needed >
)
local custom_entrypoint="/etc/nestri/_v2-entry-point"
local temp_entrypoint="/tmp/_v2-entry-point.padded"
if [[ ! -f "$custom_entrypoint" ]]; then
log "Error: Custom _v2-entry-point not found at $custom_entrypoint"
exit 1
fi
log "Starting Steam _v2-entry-point patcher..."
(
while true; do
for i in "${!entrypoints[@]}"; do
local steam_entrypoint="${entrypoints[$i]}"
if [[ -f "$steam_entrypoint" ]]; then
# Get original file size
local original_size
original_size=$(stat -c %s "$steam_entrypoint" 2>/dev/null)
if [[ -z "$original_size" ]] || [[ "$original_size" -eq 0 ]]; then
log "Warning: Could not determine size of $steam_entrypoint, retrying..."
continue
fi
# Copy custom entrypoint to temp location
cp "$custom_entrypoint" "$temp_entrypoint" 2>/dev/null || {
log "Warning: Failed to copy custom entrypoint to $temp_entrypoint"
continue
}
# Pad the temporary file to match original size
if (( $(stat -c %s "$temp_entrypoint") < original_size )); then
truncate -s "$original_size" "$temp_entrypoint" 2>/dev/null || {
log "Warning: Failed to pad $temp_entrypoint to $original_size bytes"
continue
}
fi
# Copy padded file to Steam's entrypoint, if contents differ
if ! cmp -s "$temp_entrypoint" "$steam_entrypoint"; then
cp "$temp_entrypoint" "$steam_entrypoint" 2>/dev/null || {
log "Warning: Failed to patch $steam_entrypoint"
}
fi
fi
done
# Sleep for 1s
sleep 1
done
) &
PATCHER_PID=$!
log "Steam _v2-entry-point patcher started (PID: $PATCHER_PID)"
}
# Starts nestri-server # Starts nestri-server
start_nestri_server() { start_nestri_server() {
kill_if_running "${NESTRI_PID:-}" "nestri-server" kill_if_running "${NESTRI_PID:-}" "nestri-server"
log "Starting nestri-server..."
nestri-server $NESTRI_PARAMS &
NESTRI_PID=$!
log "Waiting for Wayland display 'wayland-1'..."
WAYLAND_SOCKET="${XDG_RUNTIME_DIR}/wayland-1" WAYLAND_SOCKET="${XDG_RUNTIME_DIR}/wayland-1"
# Make sure to remove old socket if exists (along with .lock file)
if [[ -e "$WAYLAND_SOCKET" ]]; then
log "Removing stale Wayland socket $WAYLAND_SOCKET"
rm -f "$WAYLAND_SOCKET" 2>/dev/null || {
log "Error: Failed to remove stale Wayland socket $WAYLAND_SOCKET"
exit 1
}
# Ignore error if .lock file doesn't exist
rm -f "${WAYLAND_SOCKET}.lock" 2>/dev/null || true
fi
# Also if gstreamer cache exists, remove it to avoid previous errors from persisting
local gst_cache="${NESTRI_HOME}/.cache/gstreamer-1.0/"
if [[ -d "$gst_cache" ]]; then
log "Removing gstreamer cache at $gst_cache"
rm -rf "${gst_cache}" 2>/dev/null || {
log "Warning: Failed to remove gstreamer cache at $gst_cache"
}
fi
# Start nestri-server
log "Starting nestri-server.."
# Try with realtime scheduling first (chrt -f 80), if fails, launch normally
if $ENTCMD_PREFIX chrt -f 80 true 2>/dev/null; then
$ENTCMD_PREFIX chrt -f 80 nestri-server $NESTRI_PARAMS &
NESTRI_PID=$!
log "Started nestri-server with realtime scheduling"
else
$ENTCMD_PREFIX nestri-server $NESTRI_PARAMS &
NESTRI_PID=$!
log "Started nestri-server"
fi
log "Waiting for Wayland display $WAYLAND_SOCKET.."
for ((i=1; i<=15; i++)); do for ((i=1; i<=15; i++)); do
if [[ -e "$WAYLAND_SOCKET" ]]; then if [[ -e "$WAYLAND_SOCKET" ]]; then
log "Wayland display 'wayland-1' ready" log "Wayland display $WAYLAND_SOCKET ready"
sleep "${WAYLAND_READY_DELAY:-3}" sleep "${WAYLAND_READY_DELAY:-3}"
start_compositor start_compositor
return return
@@ -127,12 +96,7 @@ start_nestri_server() {
sleep 1 sleep 1
done done
log "Error: Wayland display 'wayland-1' not available" log "Error: Wayland display $WAYLAND_SOCKET not available"
# Workaround for gstreamer being bit slow at times
log "Clearing gstreamer cache.."
rm -rf "${HOME}/.cache/gstreamer-1.0" 2>/dev/null || true
increment_retry "nestri-server" increment_retry "nestri-server"
restart_chain restart_chain
} }
@@ -144,15 +108,17 @@ start_compositor() {
# Set default values only if variables are unset (not empty) # Set default values only if variables are unset (not empty)
if [[ -z "${NESTRI_LAUNCH_CMD+x}" ]]; then if [[ -z "${NESTRI_LAUNCH_CMD+x}" ]]; then
NESTRI_LAUNCH_CMD="steam-native -tenfoot -cef-force-gpu" NESTRI_LAUNCH_CMD="dbus-launch steam -tenfoot -cef-force-gpu"
fi fi
if [[ -z "${NESTRI_LAUNCH_COMPOSITOR+x}" ]]; then if [[ -z "${NESTRI_LAUNCH_COMPOSITOR+x}" ]]; then
NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland --force-grab-cursor -g -f --rt --mangoapp -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}" NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland --force-grab-cursor -g -f --rt --mangoapp -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}"
fi fi
# Start Steam patcher only if Steam command is present and if needed for container runtime # If PRELOAD_SHIM_arch's are set and exist, set LD_PRELOAD for 32/64-bit apps
if [[ -n "${NESTRI_LAUNCH_CMD}" ]] && [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]] && [[ "${container_runtime:-}" != "podman" ]]; then local do_ld_preload=false
start_steam_namespaceless_patcher if [[ -f "$PRELOAD_SHIM_64" ]] || [[ -f "$PRELOAD_SHIM_32" ]]; then
do_ld_preload=true
log "Using LD_PRELOAD shim(s)"
fi fi
# Launch compositor if configured # Launch compositor if configured
@@ -163,32 +129,51 @@ start_compositor() {
# Check if this is a gamescope command # Check if this is a gamescope command
if [[ "$compositor_cmd" == *"gamescope"* ]]; then if [[ "$compositor_cmd" == *"gamescope"* ]]; then
is_gamescope=true is_gamescope=true
# Append application command for gamescope if needed
if [[ -n "$NESTRI_LAUNCH_CMD" ]] && [[ "$compositor_cmd" != *" -- "* ]]; then if [[ -n "$NESTRI_LAUNCH_CMD" ]] && [[ "$compositor_cmd" != *" -- "* ]]; then
# If steam in launch command, enable gamescope integration via -e # If steam in launch command, enable gamescope integration via -e
if [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then if [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then
compositor_cmd+=" -e" compositor_cmd+=" -e"
fi fi
compositor_cmd+=" -- $NESTRI_LAUNCH_CMD" # If ld_preload is true, add env with LD_PRELOAD
if $do_ld_preload; then
compositor_cmd+=" -- env LD_PRELOAD='/usr/\$LIB/libvimputti_shim.so' bash -c $(printf %q "$NESTRI_LAUNCH_CMD")"
else
compositor_cmd+=" -- bash -c $(printf %q "$NESTRI_LAUNCH_CMD")"
fi
fi fi
fi fi
# Get appropriate socket based on compositor type
if $is_gamescope; then
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/gamescope-0"
else
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/wayland-0"
fi
# Clean up old socket if exists
if [[ -e "$COMPOSITOR_SOCKET" ]]; then
log "Removing stale compositor socket $COMPOSITOR_SOCKET"
rm -f "$COMPOSITOR_SOCKET" 2>/dev/null || {
log "Error: Failed to remove stale compositor socket $COMPOSITOR_SOCKET"
exit 1
}
# Ignore error if .lock file doesn't exist
rm -f "${COMPOSITOR_SOCKET}.lock" 2>/dev/null || true
fi
log "Starting compositor: $compositor_cmd" log "Starting compositor: $compositor_cmd"
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$compositor_cmd" & WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$compositor_cmd" &
COMPOSITOR_PID=$! COMPOSITOR_PID=$!
# Wait for appropriate socket based on compositor type # If ld_preload is true, export LD_PRELOAD
if $is_gamescope; then if $do_ld_preload; then
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/gamescope-0" export LD_PRELOAD='/usr/$LIB/libvimputti_shim.so'
log "Waiting for gamescope socket..."
else
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/wayland-0"
log "Waiting for wayland-0 socket..."
fi fi
log "Waiting for compositor socket $COMPOSITOR_SOCKET.."
for ((i=1; i<=15; i++)); do for ((i=1; i<=15; i++)); do
if [[ -e "$COMPOSITOR_SOCKET" ]]; then if [[ -e "$COMPOSITOR_SOCKET" ]]; then
log "Compositor socket ready ($COMPOSITOR_SOCKET)." log "Compositor socket ready $COMPOSITOR_SOCKET"
# Patch resolution with wlr-randr for non-gamescope compositors # Patch resolution with wlr-randr for non-gamescope compositors
if ! $is_gamescope; then if ! $is_gamescope; then
local OUTPUT_NAME local OUTPUT_NAME
@@ -205,6 +190,8 @@ start_compositor() {
WAYLAND_DISPLAY=wayland-0 /bin/bash -c "$NESTRI_LAUNCH_CMD" & WAYLAND_DISPLAY=wayland-0 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
APP_PID=$! APP_PID=$!
fi fi
else
log "Gamescope detected, skipping wlr-randr resolution patch"
fi fi
return return
fi fi
@@ -246,8 +233,6 @@ cleanup() {
kill_if_running "${NESTRI_PID:-}" "nestri-server" kill_if_running "${NESTRI_PID:-}" "nestri-server"
kill_if_running "${COMPOSITOR_PID:-}" "compositor" kill_if_running "${COMPOSITOR_PID:-}" "compositor"
kill_if_running "${APP_PID:-}" "application" kill_if_running "${APP_PID:-}" "application"
kill_if_running "${PATCHER_PID:-}" "steam-patcher"
rm -f "/tmp/_v2-entry-point.padded" 2>/dev/null
exit $exit_code exit $exit_code
} }
@@ -272,11 +257,6 @@ main_loop() {
log "application died" log "application died"
increment_retry "application" increment_retry "application"
start_compositor start_compositor
# Check patcher
elif [[ -n "${PATCHER_PID:-}" ]] && ! kill -0 "${PATCHER_PID}" 2>/dev/null; then
log "steam-patcher died"
increment_retry "steam-patcher"
start_steam_namespaceless_patcher
fi fi
done done
} }
@@ -287,9 +267,8 @@ main() {
log "Warning: Failed to detect container information." log "Warning: Failed to detect container information."
} }
# Ensure DBus session env exists if [[ "$container_runtime" != "apptainer" ]]; then
if command -v dbus-launch >/dev/null 2>&1 && [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then ENTCMD_PREFIX="sudo -E -u ${NESTRI_USER}"
eval "$(dbus-launch)"
fi fi
restart_chain restart_chain

View File

@@ -41,6 +41,15 @@ priority=5
nice=-10 nice=-10
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
[program:vimputti-manager]
user=nestri
command=vimputti-manager
autorestart=true
autostart=true
startretries=3
priority=6
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s,VIMPUTTI_PATH=%(ENV_NESTRI_VIMPUTTI_PATH)s
[program:entrypoint] [program:entrypoint]
command=/etc/nestri/entrypoint.sh command=/etc/nestri/entrypoint.sh
autorestart=false autorestart=false

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -58,6 +58,14 @@ impl Args {
.env("NESTRI_ROOM") .env("NESTRI_ROOM")
.help("Nestri room name/identifier"), .help("Nestri room name/identifier"),
) )
.arg(
Arg::new("vimputti-path")
.long("vimputti-path")
.env("VIMPUTTI_PATH")
.help("Path to vimputti socket")
.value_parser(NonEmptyStringValueParser::new())
.default_value("/tmp/vimputti-0"),
)
.arg( .arg(
Arg::new("gpu-vendor") Arg::new("gpu-vendor")
.short('g') .short('g')
@@ -204,10 +212,10 @@ impl Args {
.default_value("192"), .default_value("192"),
) )
.arg( .arg(
Arg::new("dma-buf") Arg::new("zero-copy")
.long("dma-buf") .long("zero-copy")
.env("DMA_BUF") .env("ZERO_COPY")
.help("Use DMA-BUF for pipeline") .help("Use zero-copy pipeline")
.value_parser(BoolishValueParser::new()) .value_parser(BoolishValueParser::new())
.default_value("false"), .default_value("false"),
) )

View File

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

View File

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

View File

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

View File

@@ -112,11 +112,25 @@ pub fn get_gpus() -> Result<Vec<GPUInfo>, Box<dyn Error>> {
let minor = &caps[1]; let minor = &caps[1];
// Read vendor and device ID // Read vendor and device ID
let vendor_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/vendor", minor))?; let vendor_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/vendor", minor));
let vendor_str = match vendor_str {
Ok(v) => v,
Err(e) => {
tracing::warn!("Failed to read vendor for card{}: {}", minor, e);
continue;
}
};
let vendor_str = vendor_str.trim_start_matches("0x").trim_end_matches('\n'); let vendor_str = vendor_str.trim_start_matches("0x").trim_end_matches('\n');
let vendor = u16::from_str_radix(vendor_str, 16)?; let vendor = u16::from_str_radix(vendor_str, 16)?;
let device_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/device", minor))?; let device_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/device", minor));
let device_str = match device_str {
Ok(d) => d,
Err(e) => {
tracing::warn!("Failed to read device for card{}: {}", minor, e);
continue;
}
};
let device_str = device_str.trim_start_matches("0x").trim_end_matches('\n'); let device_str = device_str.trim_start_matches("0x").trim_end_matches('\n');
// Look up in hwdata PCI database // Look up in hwdata PCI database
@@ -129,7 +143,15 @@ pub fn get_gpus() -> Result<Vec<GPUInfo>, Box<dyn Error>> {
}; };
// Read PCI bus ID // Read PCI bus ID
let pci_bus_id = fs::read_to_string(format!("/sys/class/drm/card{}/device/uevent", minor))?; let pci_bus_id = fs::read_to_string(format!("/sys/class/drm/card{}/device/uevent", minor));
let pci_bus_id = match pci_bus_id {
Ok(p) => p,
Err(e) => {
tracing::warn!("Failed to read PCI bus ID for card{}: {}", minor, e);
continue;
}
};
// Extract PCI_SLOT_NAME from uevent content
let pci_bus_id = pci_bus_id let pci_bus_id = pci_bus_id
.lines() .lines()
.find_map(|line| { .find_map(|line| {
@@ -191,7 +213,6 @@ fn parse_pci_ids(pci_data: &str, vendor_id: &str, device_id: &str) -> Option<Str
fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> { fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> {
let entries = fs::read_dir("/sys/bus/pci/devices").ok()?; let entries = fs::read_dir("/sys/bus/pci/devices").ok()?;
for entry in entries.flatten() { for entry in entries.flatten() {
if !entry.path().to_string_lossy().contains(&pci_addr) { if !entry.path().to_string_lossy().contains(&pci_addr) {
continue; continue;

View File

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

View File

@@ -0,0 +1,205 @@
use crate::proto::proto::proto_input::InputType::{
ControllerAttach, ControllerAxis, ControllerButton, ControllerDetach, ControllerRumble,
ControllerStick, ControllerTrigger,
};
use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::mpsc;
fn controller_string_to_type(controller_type: &str) -> Result<vimputti::DeviceConfig> {
match controller_type.to_lowercase().as_str() {
"ps4" => Ok(vimputti::ControllerTemplates::ps4()),
"ps5" => Ok(vimputti::ControllerTemplates::ps5()),
"xbox360" => Ok(vimputti::ControllerTemplates::xbox360()),
"xboxone" => Ok(vimputti::ControllerTemplates::xbox_one()),
"switchpro" => Ok(vimputti::ControllerTemplates::switch_pro()),
_ => Err(anyhow::anyhow!(
"Unsupported controller type: {}",
controller_type
)),
}
}
pub struct ControllerInput {
config: vimputti::DeviceConfig,
device: vimputti::client::VirtualController,
}
impl ControllerInput {
pub async fn new(
controller_type: String,
client: &vimputti::client::VimputtiClient,
) -> Result<Self> {
let config = controller_string_to_type(&controller_type)?;
Ok(Self {
config: config.clone(),
device: client.create_device(config).await?,
})
}
pub fn device_mut(&mut self) -> &mut vimputti::client::VirtualController {
&mut self.device
}
pub fn device(&self) -> &vimputti::client::VirtualController {
&self.device
}
}
pub struct ControllerManager {
vimputti_client: Arc<vimputti::client::VimputtiClient>,
cmd_tx: mpsc::Sender<crate::proto::proto::ProtoInput>,
rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>, // (slot, strong, weak, duration_ms)
}
impl ControllerManager {
pub fn new(
vimputti_client: Arc<vimputti::client::VimputtiClient>,
) -> Result<(Self, mpsc::Receiver<(u32, u16, u16, u16)>)> {
let (cmd_tx, cmd_rx) = mpsc::channel(100);
let (rumble_tx, rumble_rx) = mpsc::channel(100);
tokio::spawn(command_loop(
cmd_rx,
vimputti_client.clone(),
rumble_tx.clone(),
));
Ok((
Self {
vimputti_client,
cmd_tx,
rumble_tx,
},
rumble_rx,
))
}
pub async fn send_command(&self, input: crate::proto::proto::ProtoInput) -> Result<()> {
self.cmd_tx.send(input).await?;
Ok(())
}
}
async fn command_loop(
mut cmd_rx: mpsc::Receiver<crate::proto::proto::ProtoInput>,
vimputti_client: Arc<vimputti::client::VimputtiClient>,
rumble_tx: mpsc::Sender<(u32, u16, u16, u16)>,
) {
let mut controllers: HashMap<u32, ControllerInput> = HashMap::new();
while let Some(input) = cmd_rx.recv().await {
if let Some(input_type) = input.input_type {
match input_type {
ControllerAttach(data) => {
// Check if controller already exists in the slot, if so, ignore
if controllers.contains_key(&(data.slot as u32)) {
tracing::warn!(
"Controller slot {} already occupied, ignoring attach",
data.slot
);
} else {
if let Ok(mut controller) =
ControllerInput::new(data.id.clone(), &vimputti_client).await
{
let slot = data.slot as u32;
let rumble_tx = rumble_tx.clone();
controller
.device_mut()
.on_rumble(move |strong, weak, duration_ms| {
let _ = rumble_tx.try_send((slot, strong, weak, duration_ms));
})
.await
.map_err(|e| {
tracing::warn!(
"Failed to register rumble callback for slot {}: {}",
slot,
e
);
})
.ok();
controllers.insert(data.slot as u32, controller);
tracing::info!("Controller {} attached to slot {}", data.id, data.slot);
} else {
tracing::error!(
"Failed to create controller of type {} for slot {}",
data.id,
data.slot
);
}
}
}
ControllerDetach(data) => {
if controllers.remove(&(data.slot as u32)).is_some() {
tracing::info!("Controller detached from slot {}", data.slot);
} else {
tracing::warn!("No controller found in slot {} to detach", data.slot);
}
}
ControllerButton(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) {
if let Some(button) = vimputti::Button::from_ev_code(data.button as u16) {
let device = controller.device();
device.button(button, data.pressed);
device.sync();
}
} else {
tracing::warn!("Controller slot {} not found for button event", data.slot);
}
}
ControllerStick(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) {
let device = controller.device();
if data.stick == 0 {
// Left stick
device.axis(vimputti::Axis::LeftStickX, data.x);
device.sync();
device.axis(vimputti::Axis::LeftStickY, data.y);
} else if data.stick == 1 {
// Right stick
device.axis(vimputti::Axis::RightStickX, data.x);
device.sync();
device.axis(vimputti::Axis::RightStickY, data.y);
}
device.sync();
} else {
tracing::warn!("Controller slot {} not found for stick event", data.slot);
}
}
ControllerTrigger(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) {
let device = controller.device();
if data.trigger == 0 {
// Left trigger
device.axis(vimputti::Axis::LowerLeftTrigger, data.value);
} else if data.trigger == 1 {
// Right trigger
device.axis(vimputti::Axis::LowerRightTrigger, data.value);
}
device.sync();
} else {
tracing::warn!("Controller slot {} not found for trigger event", data.slot);
}
}
ControllerAxis(data) => {
if let Some(controller) = controllers.get(&(data.slot as u32)) {
let device = controller.device();
if data.axis == 0 {
// dpad x
device.axis(vimputti::Axis::DPadX, data.value);
} else if data.axis == 1 {
// dpad y
device.axis(vimputti::Axis::DPadY, data.value);
}
device.sync();
}
}
// Rumble will be outgoing event..
ControllerRumble(_) => {
//no-op
}
_ => {
//no-op
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,29 +3,31 @@
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoTimestampEntry { pub struct ProtoTimestampEntry {
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub stage: ::prost::alloc::string::String, pub stage: ::prost::alloc::string::String,
#[prost(message, optional, tag = "2")] #[prost(message, optional, tag="2")]
pub time: ::core::option::Option<::prost_types::Timestamp>, pub time: ::core::option::Option<::prost_types::Timestamp>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoLatencyTracker { pub struct ProtoLatencyTracker {
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub sequence_id: ::prost::alloc::string::String, pub sequence_id: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "2")] #[prost(message, repeated, tag="2")]
pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>, pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>,
} }
// Mouse messages
/// MouseMove message /// MouseMove message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseMove { pub struct ProtoMouseMove {
/// Fixed value "MouseMove" /// Fixed value "MouseMove"
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag = "2")] #[prost(int32, tag="2")]
pub x: i32, pub x: i32,
#[prost(int32, tag = "3")] #[prost(int32, tag="3")]
pub y: i32, pub y: i32,
} }
/// MouseMoveAbs message /// MouseMoveAbs message
@@ -33,11 +35,11 @@ pub struct ProtoMouseMove {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseMoveAbs { pub struct ProtoMouseMoveAbs {
/// Fixed value "MouseMoveAbs" /// Fixed value "MouseMoveAbs"
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag = "2")] #[prost(int32, tag="2")]
pub x: i32, pub x: i32,
#[prost(int32, tag = "3")] #[prost(int32, tag="3")]
pub y: i32, pub y: i32,
} }
/// MouseWheel message /// MouseWheel message
@@ -45,11 +47,11 @@ pub struct ProtoMouseMoveAbs {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseWheel { pub struct ProtoMouseWheel {
/// Fixed value "MouseWheel" /// Fixed value "MouseWheel"
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag = "2")] #[prost(int32, tag="2")]
pub x: i32, pub x: i32,
#[prost(int32, tag = "3")] #[prost(int32, tag="3")]
pub y: i32, pub y: i32,
} }
/// MouseKeyDown message /// MouseKeyDown message
@@ -57,9 +59,9 @@ pub struct ProtoMouseWheel {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyDown { pub struct ProtoMouseKeyDown {
/// Fixed value "MouseKeyDown" /// Fixed value "MouseKeyDown"
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag = "2")] #[prost(int32, tag="2")]
pub key: i32, pub key: i32,
} }
/// MouseKeyUp message /// MouseKeyUp message
@@ -67,19 +69,21 @@ pub struct ProtoMouseKeyDown {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyUp { pub struct ProtoMouseKeyUp {
/// Fixed value "MouseKeyUp" /// Fixed value "MouseKeyUp"
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag = "2")] #[prost(int32, tag="2")]
pub key: i32, pub key: i32,
} }
// Keyboard messages
/// KeyDown message /// KeyDown message
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoKeyDown { pub struct ProtoKeyDown {
/// Fixed value "KeyDown" /// Fixed value "KeyDown"
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag = "2")] #[prost(int32, tag="2")]
pub key: i32, pub key: i32,
} }
/// KeyUp message /// KeyUp message
@@ -87,53 +91,185 @@ pub struct ProtoKeyDown {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoKeyUp { pub struct ProtoKeyUp {
/// Fixed value "KeyUp" /// Fixed value "KeyUp"
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag = "2")] #[prost(int32, tag="2")]
pub key: i32, pub key: i32,
} }
// Controller messages
/// ControllerAttach message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerAttach {
/// Fixed value "ControllerAttach"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// One of the following enums: "ps", "xbox" or "switch"
#[prost(string, tag="2")]
pub id: ::prost::alloc::string::String,
/// Slot number (0-3)
#[prost(int32, tag="3")]
pub slot: i32,
}
/// ControllerDetach message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerDetach {
/// Fixed value "ControllerDetach"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3)
#[prost(int32, tag="2")]
pub slot: i32,
}
/// ControllerButton message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerButton {
/// Fixed value "ControllerButtons"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3)
#[prost(int32, tag="2")]
pub slot: i32,
/// Button code (linux input event code)
#[prost(int32, tag="3")]
pub button: i32,
/// true if pressed, false if released
#[prost(bool, tag="4")]
pub pressed: bool,
}
/// ControllerTriggers message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerTrigger {
/// Fixed value "ControllerTriggers"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3)
#[prost(int32, tag="2")]
pub slot: i32,
/// Trigger number (0 for left, 1 for right)
#[prost(int32, tag="3")]
pub trigger: i32,
/// trigger value (-32768 to 32767)
#[prost(int32, tag="4")]
pub value: i32,
}
/// ControllerSticks message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerStick {
/// Fixed value "ControllerStick"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3)
#[prost(int32, tag="2")]
pub slot: i32,
/// Stick number (0 for left, 1 for right)
#[prost(int32, tag="3")]
pub stick: i32,
/// X axis value (-32768 to 32767)
#[prost(int32, tag="4")]
pub x: i32,
/// Y axis value (-32768 to 32767)
#[prost(int32, tag="5")]
pub y: i32,
}
/// ControllerAxis message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerAxis {
/// Fixed value "ControllerAxis"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3)
#[prost(int32, tag="2")]
pub slot: i32,
/// Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
#[prost(int32, tag="3")]
pub axis: i32,
/// axis value (-1 to 1)
#[prost(int32, tag="4")]
pub value: i32,
}
/// ControllerRumble message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerRumble {
/// Fixed value "ControllerRumble"
#[prost(string, tag="1")]
pub r#type: ::prost::alloc::string::String,
/// Slot number (0-3)
#[prost(int32, tag="2")]
pub slot: i32,
/// Low frequency rumble (0-65535)
#[prost(int32, tag="3")]
pub low_frequency: i32,
/// High frequency rumble (0-65535)
#[prost(int32, tag="4")]
pub high_frequency: i32,
/// Duration in milliseconds
#[prost(int32, tag="5")]
pub duration: i32,
}
/// Union of all Input types /// Union of all Input types
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoInput { pub struct ProtoInput {
#[prost(oneof = "proto_input::InputType", tags = "1, 2, 3, 4, 5, 6, 7")] #[prost(oneof="proto_input::InputType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14")]
pub input_type: ::core::option::Option<proto_input::InputType>, pub input_type: ::core::option::Option<proto_input::InputType>,
} }
/// Nested message and enum types in `ProtoInput`. /// Nested message and enum types in `ProtoInput`.
pub mod proto_input { pub mod proto_input {
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)] #[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum InputType { pub enum InputType {
#[prost(message, tag = "1")] #[prost(message, tag="1")]
MouseMove(super::ProtoMouseMove), MouseMove(super::ProtoMouseMove),
#[prost(message, tag = "2")] #[prost(message, tag="2")]
MouseMoveAbs(super::ProtoMouseMoveAbs), MouseMoveAbs(super::ProtoMouseMoveAbs),
#[prost(message, tag = "3")] #[prost(message, tag="3")]
MouseWheel(super::ProtoMouseWheel), MouseWheel(super::ProtoMouseWheel),
#[prost(message, tag = "4")] #[prost(message, tag="4")]
MouseKeyDown(super::ProtoMouseKeyDown), MouseKeyDown(super::ProtoMouseKeyDown),
#[prost(message, tag = "5")] #[prost(message, tag="5")]
MouseKeyUp(super::ProtoMouseKeyUp), MouseKeyUp(super::ProtoMouseKeyUp),
#[prost(message, tag = "6")] #[prost(message, tag="6")]
KeyDown(super::ProtoKeyDown), KeyDown(super::ProtoKeyDown),
#[prost(message, tag = "7")] #[prost(message, tag="7")]
KeyUp(super::ProtoKeyUp), KeyUp(super::ProtoKeyUp),
#[prost(message, tag="8")]
ControllerAttach(super::ProtoControllerAttach),
#[prost(message, tag="9")]
ControllerDetach(super::ProtoControllerDetach),
#[prost(message, tag="10")]
ControllerButton(super::ProtoControllerButton),
#[prost(message, tag="11")]
ControllerTrigger(super::ProtoControllerTrigger),
#[prost(message, tag="12")]
ControllerStick(super::ProtoControllerStick),
#[prost(message, tag="13")]
ControllerAxis(super::ProtoControllerAxis),
#[prost(message, tag="14")]
ControllerRumble(super::ProtoControllerRumble),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessageBase { pub struct ProtoMessageBase {
#[prost(string, tag = "1")] #[prost(string, tag="1")]
pub payload_type: ::prost::alloc::string::String, pub payload_type: ::prost::alloc::string::String,
#[prost(message, optional, tag = "2")] #[prost(message, optional, tag="2")]
pub latency: ::core::option::Option<ProtoLatencyTracker>, pub latency: ::core::option::Option<ProtoLatencyTracker>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessageInput { pub struct ProtoMessageInput {
#[prost(message, optional, tag = "1")] #[prost(message, optional, tag="1")]
pub message_base: ::core::option::Option<ProtoMessageBase>, pub message_base: ::core::option::Option<ProtoMessageBase>,
#[prost(message, optional, tag = "2")] #[prost(message, optional, tag="2")]
pub data: ::core::option::Option<ProtoInput>, pub data: ::core::option::Option<ProtoInput>,
} }
// @@protoc_insertion_point(module) // @@protoc_insertion_point(module)

Some files were not shown because too many files have changed in this diff Show More