Compare commits
3 Commits
capacitor-
...
d3bc1d17e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3bc1d17e2 | ||
|
|
d501b66c11 | ||
|
|
dc1b552ac1 |
20
.github/workflows/docker-bake.hcl
vendored
@@ -3,14 +3,14 @@ variable "BASE_IMAGE" {
|
||||
}
|
||||
|
||||
group "default" {
|
||||
targets = ["runner-base", "runner-builder"]
|
||||
targets = ["runner"]
|
||||
}
|
||||
|
||||
target "runner-base" {
|
||||
dockerfile = "containerfiles/runner-base.Containerfile"
|
||||
context = "."
|
||||
args = {
|
||||
BASE_IMAGE = BASE_IMAGE
|
||||
BASE_IMAGE = "${BASE_IMAGE}"
|
||||
}
|
||||
cache-from = ["type=gha,scope=runner-base-pr"]
|
||||
cache-to = ["type=gha,scope=runner-base-pr,mode=max"]
|
||||
@@ -30,3 +30,19 @@ target "runner-builder" {
|
||||
runner-base = "target:runner-base"
|
||||
}
|
||||
}
|
||||
|
||||
target "runner" {
|
||||
dockerfile = "containerfiles/runner.Containerfile"
|
||||
context = "."
|
||||
args = {
|
||||
RUNNER_BASE_IMAGE = "runner-base:latest"
|
||||
RUNNER_BUILDER_IMAGE = "runner-builder:latest"
|
||||
}
|
||||
cache-from = ["type=gha,scope=runner-pr"]
|
||||
cache-to = ["type=gha,scope=runner-pr,mode=max"]
|
||||
tags = ["nestri-runner"]
|
||||
contexts = {
|
||||
runner-base = "target:runner-base"
|
||||
runner-builder = "target:runner-builder"
|
||||
}
|
||||
}
|
||||
|
||||
81
.github/workflows/play-standalone.yml
vendored
@@ -1,81 +0,0 @@
|
||||
name: Build Nestri standalone playsite
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "containerfiles/playsite.Containerfile"
|
||||
- ".github/workflows/play-standalone.yml"
|
||||
- "packages/play-standalone/**"
|
||||
push:
|
||||
branches: [ dev, production ]
|
||||
paths:
|
||||
- "containerfiles/playsite.Containerfile"
|
||||
- ".github/workflows/play-standalone.yml"
|
||||
- "packages/play-standalone/**"
|
||||
tags:
|
||||
- v*.*.*
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: nestrilabs/nestri
|
||||
BASE_TAG_PREFIX: playsite
|
||||
|
||||
jobs:
|
||||
build-docker-pr:
|
||||
name: Build image on PR
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: containerfiles/playsite.Containerfile
|
||||
context: ./
|
||||
push: false
|
||||
load: true
|
||||
tags: nestri:playsite
|
||||
|
||||
build-and-push-docker:
|
||||
name: Build and push image
|
||||
if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/production' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
- name: Extract Container metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.BASE_TAG_PREFIX }}
|
||||
#
|
||||
#tag on release, and a nightly build for 'dev'
|
||||
tags: |
|
||||
type=raw,value=nightly,enable={{is_default_branch}}
|
||||
type=raw,value={{branch}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'production') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: containerfiles/playsite.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
3
.github/workflows/relay.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: Build Nestri relay
|
||||
#Tabs not spaces, you moron :)
|
||||
|
||||
name: Build nestri:relay
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
|
||||
73
.github/workflows/runner-bases.yml
vendored
@@ -1,73 +0,0 @@
|
||||
name: Build Nestri runner base images
|
||||
|
||||
on: [ workflow_call ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: nestrilabs/nestri
|
||||
BASE_IMAGE: docker.io/cachyos/cachyos:latest
|
||||
|
||||
jobs:
|
||||
build-and-push-bases:
|
||||
name: Build and push images
|
||||
if: ${{ github.ref == 'refs/heads/production' || github.ref == 'refs/heads/dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- { suffix: "v2", base: "docker.io/cachyos/cachyos:latest" }
|
||||
- { suffix: "v3", base: "docker.io/cachyos/cachyos-v3:latest" }
|
||||
#- { suffix: "v4", base: "docker.io/cachyos/cachyos-v4:latest" } # Disabled until GHA has this
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 20
|
||||
- name: Build and push runner-base image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner-base.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest-${{ matrix.variant.suffix }}
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ matrix.variant.base }}
|
||||
cache-from: type=gha,scope=runner-base-${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner-base-${{ matrix.variant.suffix }},mode=max
|
||||
pull: true
|
||||
- name: Build and push runner-builder image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner-builder.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest-${{ matrix.variant.suffix }}
|
||||
build-args: |
|
||||
RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest-${{ matrix.variant.suffix }}
|
||||
cache-from: type=gha,scope=runner-builder-${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner-builder-${{ matrix.variant.suffix }},mode=max
|
||||
- name: Build and push runner-common image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner-common.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-common:latest-${{ matrix.variant.suffix }}
|
||||
build-args: |
|
||||
RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest-${{ matrix.variant.suffix }}
|
||||
RUNNER_BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest-${{ matrix.variant.suffix }}
|
||||
cache-from: type=gha,scope=runner-common-${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner-common-${{ matrix.variant.suffix }},mode=max
|
||||
86
.github/workflows/runner-variants.yml
vendored
@@ -1,86 +0,0 @@
|
||||
name: Build Nestri runner image variants
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 7 0 * * 1,3,6 # Nightlies
|
||||
push:
|
||||
branches: [ dev, production ]
|
||||
paths:
|
||||
- "containerfiles/*runner.Containerfile"
|
||||
- ".github/workflows/runner-variants.yml"
|
||||
- "packages/scripts/**"
|
||||
- "packages/configs/**"
|
||||
tags:
|
||||
- v*.*.*
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: nestrilabs/nestri
|
||||
|
||||
jobs:
|
||||
bases:
|
||||
uses: ./.github/workflows/runner-bases.yml
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
build-and-push-variants:
|
||||
needs: [ bases ]
|
||||
name: Build and push images
|
||||
if: ${{ github.ref == 'refs/heads/production' || github.ref == 'refs/heads/dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- { suffix: "v2", base: "docker.io/cachyos/cachyos:latest" }
|
||||
- { suffix: "v3", base: "docker.io/cachyos/cachyos-v3:latest" }
|
||||
#- { suffix: "v4", base: "docker.io/cachyos/cachyos-v4:latest" } # Disabled until GHA has this
|
||||
runner:
|
||||
- steam
|
||||
- heroic
|
||||
- minecraft
|
||||
# ADD MORE HERE AS NEEDED #
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
- name: Extract runner metadata
|
||||
id: meta-runner
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner
|
||||
tags: |
|
||||
type=raw,value=nightly-${{ matrix.runner }}-${{ matrix.variant.suffix }},enable={{is_default_branch}}
|
||||
type=raw,value={{branch}}-${{ matrix.runner }}-${{ matrix.variant.suffix }}
|
||||
type=raw,value=latest-${{ matrix.runner }}-${{ matrix.variant.suffix }},enable=${{ github.ref == format('refs/heads/{0}', 'production') }}
|
||||
type=semver,pattern={{version}}-${{ matrix.runner }}-${{ matrix.variant.suffix }}
|
||||
type=semver,pattern={{major}}.{{minor}}-${{ matrix.runner }}-${{ matrix.variant.suffix }}
|
||||
type=semver,pattern={{major}}-${{ matrix.runner }}-${{ matrix.variant.suffix }}
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 20
|
||||
- name: Build and push runner image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/${{ matrix.runner }}-runner.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ steps.meta-runner.outputs.tags }}
|
||||
labels: ${{ steps.meta-runner.outputs.labels }}
|
||||
build-args: |
|
||||
RUNNER_COMMON_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-common:latest-${{ matrix.variant.suffix }}
|
||||
cache-from: type=gha,scope=runner-${{ matrix.runner }}-${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner-${{ matrix.runner }}-${{ matrix.variant.suffix }},mode=max
|
||||
148
.github/workflows/runner.yml
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
#Tabs not spaces, you moron :)
|
||||
|
||||
name: Build nestri-runner
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "containerfiles/runner*.Containerfile"
|
||||
- "packages/scripts/**"
|
||||
- "packages/server/**"
|
||||
- ".github/workflows/runner.yml"
|
||||
schedule:
|
||||
- cron: 7 0 * * 1,3,6 # Regularly to keep that build cache warm
|
||||
push:
|
||||
branches: [dev, production]
|
||||
paths:
|
||||
- "containerfiles/runner*.Containerfile"
|
||||
- ".github/workflows/runner.yml"
|
||||
- "packages/scripts/**"
|
||||
- "packages/server/**"
|
||||
tags:
|
||||
- v*.*.*
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: nestrilabs/nestri
|
||||
BASE_IMAGE: docker.io/cachyos/cachyos:latest
|
||||
|
||||
# This makes our release ci quit prematurely
|
||||
# concurrency:
|
||||
# group: ci-${{ github.ref }}
|
||||
# cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-docker-pr:
|
||||
name: Build images on PR
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
-
|
||||
name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 20
|
||||
-
|
||||
name: Build images using bake
|
||||
uses: docker/bake-action@v6
|
||||
env:
|
||||
BASE_IMAGE: ${{ env.BASE_IMAGE }}
|
||||
with:
|
||||
files: |
|
||||
./.github/workflows/docker-bake.hcl
|
||||
targets: runner
|
||||
push: false
|
||||
load: true
|
||||
|
||||
build-and-push-docker:
|
||||
name: Build and push images
|
||||
if: ${{ github.ref == 'refs/heads/production' || github.ref == 'refs/heads/dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- { suffix: "", base: "docker.io/cachyos/cachyos:latest" }
|
||||
- { suffix: "-v3", base: "docker.io/cachyos/cachyos-v3:latest" }
|
||||
#- { suffix: "-v4", base: "docker.io/cachyos/cachyos-v4:latest" } # Disabled until GHA has this
|
||||
steps:
|
||||
-
|
||||
name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
-
|
||||
name: Extract runner metadata
|
||||
id: meta-runner
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner
|
||||
tags: |
|
||||
type=raw,value=nightly${{ matrix.variant.suffix }},enable={{is_default_branch}}
|
||||
type=raw,value={{branch}}${{ matrix.variant.suffix }}
|
||||
type=raw,value=latest${{ matrix.variant.suffix }},enable=${{ github.ref == format('refs/heads/{0}', 'production') }}
|
||||
type=semver,pattern={{version}}${{ matrix.variant.suffix }}
|
||||
type=semver,pattern={{major}}.{{minor}}${{ matrix.variant.suffix }}
|
||||
type=semver,pattern={{major}}${{ matrix.variant.suffix }}
|
||||
-
|
||||
name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 20
|
||||
-
|
||||
name: Build and push runner-base image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner-base.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }}
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ matrix.variant.base }}
|
||||
cache-from: type=gha,scope=runner-base${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner-base${{ matrix.variant.suffix }},mode=max
|
||||
pull: ${{ github.event_name == 'schedule' }}
|
||||
-
|
||||
name: Build and push runner-builder image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner-builder.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest${{ matrix.variant.suffix }}
|
||||
build-args: |
|
||||
RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }}
|
||||
cache-from: type=gha,scope=runner-builder${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner-builder${{ matrix.variant.suffix }},mode=max
|
||||
-
|
||||
name: Build and push runner image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: containerfiles/runner.Containerfile
|
||||
context: ./
|
||||
push: true
|
||||
tags: ${{ steps.meta-runner.outputs.tags }}
|
||||
labels: ${{ steps.meta-runner.outputs.labels }}
|
||||
build-args: |
|
||||
RUNNER_BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-base:latest${{ matrix.variant.suffix }}
|
||||
RUNNER_BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/runner-builder:latest${{ matrix.variant.suffix }}
|
||||
cache-from: type=gha,scope=runner${{ matrix.variant.suffix }},mode=max
|
||||
cache-to: type=gha,scope=runner${{ matrix.variant.suffix }},mode=max
|
||||
24
apps/blog/.gitignore
vendored
Normal 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
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
apps/blog/.vscode/launch.json
vendored
Normal 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
@@ -0,0 +1,68 @@
|
||||
# Astro Starter Kit: Blog
|
||||
|
||||
```sh
|
||||
bun create astro@latest -- --template blog
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/blog)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/blog)
|
||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/blog/devcontainer.json)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
18
apps/blog/astro.config.mjs
Normal 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
21
apps/blog/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
apps/blog/public/blog-placeholder-1.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
apps/blog/public/blog-placeholder-2.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
apps/blog/public/blog-placeholder-3.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/blog/public/blog-placeholder-4.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
apps/blog/public/blog-placeholder-5.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/blog/public/blog-placeholder-about.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
9
apps/blog/public/favicon.svg
Normal 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 |
BIN
apps/blog/public/fonts/atkinson-bold.woff
Normal file
BIN
apps/blog/public/fonts/atkinson-regular.woff
Normal file
BIN
apps/blog/public/nestri-footage-latency.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
apps/blog/public/pexels-brett-sayles-2881224.jpg
Normal file
|
After Width: | Height: | Size: 157 KiB |
55
apps/blog/src/components/BaseHead.astro
Normal 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)} />
|
||||
53
apps/blog/src/components/Footer.astro
Normal 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>
|
||||
17
apps/blog/src/components/FormattedDate.astro
Normal 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>
|
||||
57
apps/blog/src/components/Header.astro
Normal 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>
|
||||
24
apps/blog/src/components/HeaderLink.astro
Normal 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
@@ -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';
|
||||
18
apps/blog/src/content.config.ts
Normal 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 };
|
||||
16
apps/blog/src/content/blog/first-post.md
Normal 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.
|
||||
65
apps/blog/src/content/blog/latency-deep-dive.md
Normal 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:
|
||||
|
||||
](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!
|
||||
214
apps/blog/src/content/blog/markdown-style-guide.md
Normal 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
|
||||

|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
16
apps/blog/src/content/blog/second-post.md
Normal 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.
|
||||
16
apps/blog/src/content/blog/third-post.md
Normal 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.
|
||||
31
apps/blog/src/content/blog/using-mdx.mdx
Normal 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.
|
||||
92
apps/blog/src/layouts/BlogPost.astro
Normal 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>
|
||||
62
apps/blog/src/pages/about.astro
Normal 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>
|
||||
21
apps/blog/src/pages/blog/[...slug].astro
Normal 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>
|
||||
120
apps/blog/src/pages/blog/index.astro
Normal 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>
|
||||
49
apps/blog/src/pages/index.astro
Normal 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>
|
||||
16
apps/blog/src/pages/rss.xml.js
Normal 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}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
178
apps/blog/src/styles/global.css
Normal 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
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# Container build arguments #
|
||||
ARG RUNNER_COMMON_IMAGE=runner-common:latest
|
||||
|
||||
#*********************#
|
||||
# Final Runtime Stage #
|
||||
#*********************#
|
||||
FROM ${RUNNER_COMMON_IMAGE}
|
||||
|
||||
### FLAVOR/VARIANT CONFIGURATION ###
|
||||
## HEROIC LAUNCHER ##
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --noconfirm heroic-games-launcher-bin && \
|
||||
# Cleanup
|
||||
paccache -rk1 && \
|
||||
rm -rf /usr/share/{info,man,doc}/*
|
||||
|
||||
## FLAVOR/VARIANT LAUNCH COMMAND ##
|
||||
ENV NESTRI_LAUNCH_CMD="heroic"
|
||||
### END OF FLAVOR/VARIANT CONFIGURATION ###
|
||||
|
||||
### REQUIRED DEFAULT ENTRYPOINT FOR FLAVOR/VARIANT ###
|
||||
USER root
|
||||
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Container build arguments #
|
||||
ARG RUNNER_COMMON_IMAGE=runner-common:latest
|
||||
|
||||
#*********************#
|
||||
# Final Runtime Stage #
|
||||
#*********************#
|
||||
FROM ${RUNNER_COMMON_IMAGE}
|
||||
|
||||
### FLAVOR/VARIANT CONFIGURATION ###
|
||||
## MINECRAFT ##
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --noconfirm paru && \
|
||||
sudo -H -u ${NESTRI_USER} paru -S --noconfirm aur/minecraft-launcher && \
|
||||
# Cleanup
|
||||
paccache -rk1 && \
|
||||
rm -rf /usr/share/{info,man,doc}/*
|
||||
|
||||
## FLAVOR/VARIANT LAUNCH COMMAND ##
|
||||
ENV NESTRI_LAUNCH_CMD="minecraft-launcher"
|
||||
### END OF FLAVOR/VARIANT CONFIGURATION ###
|
||||
|
||||
### REQUIRED DEFAULT ENTRYPOINT FOR FLAVOR/VARIANT ###
|
||||
USER root
|
||||
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]
|
||||
@@ -15,7 +15,7 @@ ENV CARGO_HOME=/usr/local/cargo \
|
||||
|
||||
# Install build essentials and caching tools
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --noconfirm rustup git base-devel mold \
|
||||
pacman -Sy --noconfirm rustup git base-devel mold \
|
||||
meson pkgconf cmake git gcc make
|
||||
|
||||
# Override various linker with symlink so mold is forcefully used (ld, ld.lld, lld)
|
||||
@@ -28,7 +28,7 @@ RUN rustup default stable
|
||||
|
||||
# Install cargo-chef with proper caching
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo install cargo-chef --locked
|
||||
cargo install -j $(nproc) cargo-chef --locked
|
||||
|
||||
#*******************************#
|
||||
# vimputti manager build stages #
|
||||
@@ -38,10 +38,10 @@ WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --noconfirm lib32-gcc-libs
|
||||
pacman -Sy --noconfirm lib32-gcc-libs
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 --rev "2fde5376b6b9a38cdbd94ccc6a80c9d29a81a417" https://github.com/DatCaptainHorse/vimputti.git
|
||||
RUN git clone --depth 1 --rev "9e8bfd0217eeab011c5afc368d3ea67a4c239e81" https://github.com/DatCaptainHorse/vimputti.git
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM vimputti-manager-deps AS vimputti-manager-planner
|
||||
@@ -83,7 +83,7 @@ WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --noconfirm gst-plugins-good gst-plugin-rswebrtc
|
||||
pacman -Sy --noconfirm gst-plugins-good gst-plugin-rswebrtc
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM nestri-server-deps AS nestri-server-planner
|
||||
@@ -123,14 +123,29 @@ WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --noconfirm libxkbcommon wayland \
|
||||
pacman -Sy --noconfirm libxkbcommon wayland \
|
||||
gst-plugins-good gst-plugins-bad libinput
|
||||
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo install cargo-c
|
||||
|
||||
# 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 "e4c70b64dad3cd8bbf5eec011f419386adf737ee" https://github.com/games-on-whales/gst-wayland-display.git
|
||||
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
|
||||
@@ -148,7 +163,7 @@ COPY --from=gst-wayland-planner /builder/gst-wayland-display/recipe.json .
|
||||
|
||||
# Cache dependencies using cargo-chef
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
cargo chef cook --release --recipe-path recipe.json --features cuda
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
|
||||
ENV CARGO_TARGET_DIR=/builder/target
|
||||
@@ -158,7 +173,7 @@ COPY --from=gst-wayland-planner /builder/gst-wayland-display/ .
|
||||
# Build and install directly to artifacts
|
||||
RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
--mount=type=cache,target=/builder/target \
|
||||
cargo cinstall --prefix=${ARTIFACTS} --release --features cuda
|
||||
cargo cinstall --prefix=${ARTIFACTS} --release
|
||||
|
||||
#*********************************#
|
||||
# Patched bubblewrap build stages #
|
||||
@@ -168,7 +183,7 @@ WORKDIR /builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --noconfirm libtool libcap libselinux
|
||||
pacman -Sy --noconfirm libtool libcap libselinux
|
||||
|
||||
# Copy patch file from host
|
||||
COPY packages/patches/bubblewrap/ /builder/patches/
|
||||
@@ -199,4 +214,5 @@ 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/
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
ARG RUNNER_BASE_IMAGE=runner-base:latest
|
||||
ARG RUNNER_BUILDER_IMAGE=runner-builder:latest
|
||||
|
||||
#**********************#
|
||||
# Runtime Common Stage #
|
||||
#**********************#
|
||||
#*********************#
|
||||
# Final Runtime Stage #
|
||||
#*********************#
|
||||
FROM ${RUNNER_BASE_IMAGE} AS runtime
|
||||
FROM ${RUNNER_BUILDER_IMAGE} AS builder
|
||||
FROM runtime
|
||||
@@ -12,11 +12,11 @@ FROM runtime
|
||||
### Package Installation ###
|
||||
# Core system components
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --needed --noconfirm \
|
||||
pacman -Sy --needed --noconfirm \
|
||||
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
|
||||
vulkan-radeon lib32-vulkan-radeon \
|
||||
mesa lib32-mesa \
|
||||
gtk3 lib32-gtk3 \
|
||||
steam gtk3 lib32-gtk3 \
|
||||
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
|
||||
pipewire pipewire-pulse pipewire-alsa wireplumber \
|
||||
noto-fonts-cjk supervisor jq pacman-contrib \
|
||||
@@ -67,10 +67,10 @@ RUN mkdir -p /etc/pipewire/pipewire.conf.d && \
|
||||
COPY packages/configs/wireplumber.conf.d/* /etc/wireplumber/wireplumber.conf.d/
|
||||
COPY packages/configs/pipewire.conf.d/* /etc/pipewire/pipewire.conf.d/
|
||||
|
||||
## MangoHud Config ##
|
||||
RUN mkdir -p "${NESTRI_HOME}/.config/MangoHud"
|
||||
## Steam Configs - Proton (Experimental flavor) ##
|
||||
RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config"
|
||||
|
||||
COPY packages/configs/MangoHud/MangoHud.conf "${NESTRI_HOME}/.config/MangoHud/"
|
||||
COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/"
|
||||
|
||||
### Artifacts from Builder ###
|
||||
COPY --from=builder /artifacts/bin/nestri-server /usr/bin/
|
||||
@@ -88,3 +88,7 @@ RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \
|
||||
setcap cap_net_admin+ep /usr/bin/vimputti-manager && \
|
||||
dbus-uuidgen > /etc/machine-id && \
|
||||
LANG=en_US.UTF-8 locale-gen
|
||||
|
||||
# Root for most container engines, nestri-user compatible for apptainer without fakeroot
|
||||
USER root
|
||||
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Container build arguments #
|
||||
ARG RUNNER_COMMON_IMAGE=runner-common:latest
|
||||
|
||||
#*********************#
|
||||
# Final Runtime Stage #
|
||||
#*********************#
|
||||
FROM ${RUNNER_COMMON_IMAGE}
|
||||
|
||||
### FLAVOR/VARIANT CONFIGURATION ###
|
||||
## STEAM ##
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -S --noconfirm steam && \
|
||||
# Cleanup
|
||||
paccache -rk1 && \
|
||||
rm -rf /usr/share/{info,man,doc}/*
|
||||
|
||||
## Steam Configs - Proton (Experimental flavor) ##
|
||||
RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config"
|
||||
COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/"
|
||||
|
||||
## FLAVOR/VARIANT LAUNCH COMMAND ##
|
||||
ENV NESTRI_LAUNCH_CMD="steam -tenfoot -cef-force-gpu"
|
||||
### END OF FLAVOR/VARIANT CONFIGURATION ###
|
||||
|
||||
### REQUIRED DEFAULT ENTRYPOINT FOR FLAVOR/VARIANT ###
|
||||
USER root
|
||||
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]
|
||||
@@ -21,7 +21,6 @@
|
||||
"core-js-pure",
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"sharp",
|
||||
"workerd"
|
||||
],
|
||||
"workspaces": {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
legacy_layout=false
|
||||
|
||||
# common
|
||||
horizontal
|
||||
horizontal_stretch
|
||||
hud_no_margin
|
||||
no_small_font
|
||||
background_alpha=0.66
|
||||
round_corners=0
|
||||
background_color=000000
|
||||
font_size=24
|
||||
position=top-left
|
||||
engine_short_names
|
||||
|
||||
# colors
|
||||
text_color=DFDFDF
|
||||
gpu_color=FF4E00
|
||||
cpu_color=00AA00
|
||||
engine_color=00AA00
|
||||
vram_color=00AA00
|
||||
ram_color=00AA00
|
||||
frametime_color=FF4E00
|
||||
|
||||
# load colors
|
||||
cpu_load_color=DFDFDF,DF964D,DF3D3D
|
||||
gpu_load_color=DFDFDF,DF964D,DF3D3D
|
||||
|
||||
# GPU and VRAM
|
||||
gpu_text=NESTRI
|
||||
gpu_stats
|
||||
gpu_load_change
|
||||
gpu_load_value=70,90
|
||||
|
||||
vram
|
||||
|
||||
# CPU and RAM
|
||||
cpu_text=CPU
|
||||
cpu_stats
|
||||
cpu_load_change
|
||||
cpu_load_value=70,90
|
||||
|
||||
ram
|
||||
|
||||
# FPS and timing
|
||||
fps
|
||||
fps_metrics=0.01
|
||||
|
||||
frame_timing
|
||||
@@ -1,3 +0,0 @@
|
||||
.idea/
|
||||
dist/
|
||||
node_modules/
|
||||
@@ -7,22 +7,24 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/buf": "^1.59.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.10.0"
|
||||
"@bufbuild/buf": "^1.57.2",
|
||||
"@bufbuild/protoc-gen-es": "^2.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.10.0",
|
||||
"@chainsafe/libp2p-noise": "^17.0.0",
|
||||
"@bufbuild/protobuf": "^2.9.0",
|
||||
"@chainsafe/libp2p-noise": "^16.1.4",
|
||||
"@chainsafe/libp2p-quic": "^1.1.3",
|
||||
"@chainsafe/libp2p-yamux": "^8.0.1",
|
||||
"@libp2p/identify": "^4.0.5",
|
||||
"@libp2p/interface": "^3.0.2",
|
||||
"@libp2p/ping": "^3.0.5",
|
||||
"@libp2p/websockets": "^10.0.6",
|
||||
"@libp2p/webtransport": "^6.0.7",
|
||||
"@libp2p/utils": "^7.0.5",
|
||||
"@multiformats/multiaddr": "^13.0.1",
|
||||
"libp2p": "^3.0.6",
|
||||
"uint8arraylist": "^2.4.8"
|
||||
"@chainsafe/libp2p-yamux": "^7.0.4",
|
||||
"@libp2p/identify": "^3.0.39",
|
||||
"@libp2p/interface": "^2.11.0",
|
||||
"@libp2p/ping": "^2.0.37",
|
||||
"@libp2p/websockets": "^9.2.19",
|
||||
"@libp2p/webtransport": "^5.0.51",
|
||||
"@multiformats/multiaddr": "^12.5.1",
|
||||
"it-length-prefixed": "^10.0.1",
|
||||
"it-pipe": "^3.0.1",
|
||||
"libp2p": "^2.10.0",
|
||||
"uint8arraylist": "^2.4.8",
|
||||
"uint8arrays": "^5.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
import { controllerButtonToLinuxEventCode } from "./codes";
|
||||
import { WebRTCStream } from "./webrtc-stream";
|
||||
import {
|
||||
ProtoMessageBase,
|
||||
ProtoMessageInput,
|
||||
ProtoMessageInputSchema,
|
||||
} from "./proto/messages_pb";
|
||||
import {
|
||||
ProtoInputSchema,
|
||||
ProtoControllerAttachSchema,
|
||||
ProtoControllerDetachSchema,
|
||||
ProtoControllerStateBatchSchema,
|
||||
ProtoControllerStateBatch,
|
||||
ProtoControllerButtonSchema,
|
||||
ProtoControllerTriggerSchema,
|
||||
ProtoControllerAxisSchema,
|
||||
ProtoControllerStickSchema,
|
||||
ProtoControllerRumble,
|
||||
} from "./proto/types_pb";
|
||||
import { create, toBinary, fromBinary } from "@bufbuild/protobuf";
|
||||
import { createMessage } from "./utils";
|
||||
import { ProtoMessageSchema } from "./proto/messages_pb";
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCStream;
|
||||
@@ -17,7 +23,6 @@ interface Props {
|
||||
}
|
||||
|
||||
interface GamepadState {
|
||||
previousButtonState: Map<number, boolean>;
|
||||
buttonState: Map<number, boolean>;
|
||||
leftTrigger: number;
|
||||
rightTrigger: number;
|
||||
@@ -29,17 +34,12 @@ interface GamepadState {
|
||||
dpadY: number;
|
||||
}
|
||||
|
||||
enum PollState {
|
||||
IDLE,
|
||||
RUNNING,
|
||||
}
|
||||
|
||||
export class Controller {
|
||||
protected wrtc: WebRTCStream;
|
||||
protected slot: number;
|
||||
protected connected: boolean = false;
|
||||
protected gamepad: Gamepad | null = null;
|
||||
protected state: GamepadState = {
|
||||
previousButtonState: new Map<number, boolean>(),
|
||||
protected lastState: GamepadState = {
|
||||
buttonState: new Map<number, boolean>(),
|
||||
leftTrigger: 0,
|
||||
rightTrigger: 0,
|
||||
@@ -53,33 +53,17 @@ export class Controller {
|
||||
// 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)
|
||||
|
||||
// Polling configuration
|
||||
private readonly FULL_RATE_MS = 10; // 100 UPS
|
||||
private readonly IDLE_THRESHOLD = 100; // ms before considering idle/hands off controller
|
||||
private readonly FULL_INTERVAL = 250; // ms before sending full state occassionally, to verify inputs are synced
|
||||
|
||||
// Polling state
|
||||
private pollingState: PollState = PollState.IDLE;
|
||||
private lastInputTime: number = Date.now();
|
||||
private lastFullTime: number = Date.now();
|
||||
private pollInterval: any = null;
|
||||
|
||||
// Controller batch vars
|
||||
private sequence: number = 0;
|
||||
private readonly CHANGED_BUTTONS_STATE = 1 << 0;
|
||||
private readonly CHANGED_LEFT_STICK_X = 1 << 1;
|
||||
private readonly CHANGED_LEFT_STICK_Y = 1 << 2;
|
||||
private readonly CHANGED_RIGHT_STICK_X = 1 << 3;
|
||||
private readonly CHANGED_RIGHT_STICK_Y = 1 << 4;
|
||||
private readonly CHANGED_LEFT_TRIGGER = 1 << 5;
|
||||
private readonly CHANGED_RIGHT_TRIGGER = 1 << 6;
|
||||
private readonly CHANGED_DPAD_X = 1 << 7;
|
||||
private readonly CHANGED_DPAD_Y = 1 << 8;
|
||||
|
||||
private _dcHandler: ((data: ArrayBuffer) => void) | null = null;
|
||||
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})/);
|
||||
@@ -88,49 +72,34 @@ export class Controller {
|
||||
const productMatch = e.gamepad.id.match(/Product:\s?([0-9a-fA-F]{4})/);
|
||||
const productId = productMatch ? productMatch[1].toLowerCase() : "unknown";
|
||||
|
||||
// Listen to datachannel events from server
|
||||
this._dcHandler = (data: ArrayBuffer) => {
|
||||
if (!this.connected) return;
|
||||
try {
|
||||
// First decode the wrapper message
|
||||
const uint8Data = new Uint8Array(data);
|
||||
const messageWrapper = fromBinary(ProtoMessageSchema, uint8Data);
|
||||
|
||||
if (messageWrapper.payload.case === "controllerRumble") {
|
||||
this.rumbleCallback(messageWrapper.payload.value);
|
||||
} else if (messageWrapper.payload.case === "controllerAttach") {
|
||||
if (this.gamepad) return; // already attached
|
||||
const attachMsg = messageWrapper.payload.value;
|
||||
// Gamepad connected succesfully
|
||||
this.gamepad = e.gamepad;
|
||||
console.log(
|
||||
`Gamepad connected: ${e.gamepad.id}, local slot ${e.gamepad.index}, msg: ${attachMsg.sessionSlot}`,
|
||||
);
|
||||
this.run();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error decoding datachannel message:", err);
|
||||
}
|
||||
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.addDataChannelCallback(this._dcHandler);
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
|
||||
const attachMsg = createMessage(
|
||||
create(ProtoControllerAttachSchema, {
|
||||
id: this.vendor_id_to_controller(vendorId, productId),
|
||||
sessionSlot: e.gamepad.index,
|
||||
sessionId: this.wrtc.getSessionID(),
|
||||
}),
|
||||
"controllerInput",
|
||||
);
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, attachMsg));
|
||||
// Listen to feedback rumble events from server
|
||||
this._dcRumbleHandler = (data: any) => this.rumbleCallback(data as ArrayBuffer);
|
||||
this.wrtc.addDataChannelCallback(this._dcRumbleHandler);
|
||||
|
||||
this.run();
|
||||
}
|
||||
|
||||
public getSlot(): number {
|
||||
return this.gamepad.index;
|
||||
}
|
||||
|
||||
// Maps vendor id and product id to supported controller type
|
||||
// Currently supported: Sony (ps4, ps5), Microsoft (xbox360, xboxone), Nintendo (switchpro)
|
||||
// Default fallback to xbox360
|
||||
@@ -180,352 +149,361 @@ export class Controller {
|
||||
return ((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin) + toMin;
|
||||
}
|
||||
|
||||
private restartPolling() {
|
||||
// Clear existing interval
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
|
||||
// Restart with active polling
|
||||
this.pollingState = PollState.RUNNING;
|
||||
this.lastInputTime = Date.now();
|
||||
|
||||
// Start interval
|
||||
this.pollInterval = setInterval(
|
||||
() => this.pollGamepad(),
|
||||
this.FULL_RATE_MS,
|
||||
);
|
||||
}
|
||||
|
||||
private pollGamepad() {
|
||||
if (!this.connected || !this.gamepad) return;
|
||||
|
||||
const gamepads = navigator.getGamepads();
|
||||
if (!gamepads[this.gamepad.index]) return;
|
||||
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;
|
||||
}
|
||||
|
||||
this.gamepad = gamepads[this.gamepad.index];
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// Collect state changes
|
||||
const changedFields = this.collectStateChanges();
|
||||
|
||||
// Send batched changes update if there's changes
|
||||
if (changedFields > 0) {
|
||||
let send_type = 1;
|
||||
const timeSinceFull = Date.now() - this.lastFullTime;
|
||||
if (timeSinceFull > this.FULL_INTERVAL) {
|
||||
send_type = 0;
|
||||
this.lastFullTime = Date.now();
|
||||
}
|
||||
|
||||
this.sendBatchedState(changedFields, send_type);
|
||||
this.lastInputTime = Date.now();
|
||||
if (this.pollingState !== PollState.RUNNING) {
|
||||
this.pollingState = PollState.RUNNING;
|
||||
}
|
||||
}
|
||||
|
||||
const timeSinceInput = Date.now() - this.lastInputTime;
|
||||
if (timeSinceInput > this.IDLE_THRESHOLD) {
|
||||
// Changing from running to idle..
|
||||
if (this.pollingState === PollState.RUNNING) {
|
||||
// Send full state on idle assumption
|
||||
this.sendBatchedState(0xff, 0);
|
||||
this.pollingState = PollState.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
this.state.buttonState.forEach((b, i) =>
|
||||
this.state.previousButtonState.set(i, b),
|
||||
);
|
||||
}
|
||||
|
||||
private collectStateChanges(): number {
|
||||
let changedFields = 0;
|
||||
|
||||
// Collect analog values
|
||||
const leftTrigger = Math.round(
|
||||
this.remapFromTo(
|
||||
this.gamepad.buttons[6]?.value ?? 0,
|
||||
0,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
),
|
||||
);
|
||||
const rightTrigger = Math.round(
|
||||
this.remapFromTo(
|
||||
this.gamepad.buttons[7]?.value ?? 0,
|
||||
0,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
),
|
||||
);
|
||||
|
||||
const leftX = this.remapFromTo(
|
||||
this.gamepad.axes[0] ?? 0,
|
||||
-1,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
);
|
||||
const leftY = this.remapFromTo(
|
||||
this.gamepad.axes[1] ?? 0,
|
||||
-1,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
);
|
||||
const sendLeftX =
|
||||
Math.abs(leftX) > this.stickDeadzone ? Math.round(leftX) : 0;
|
||||
const sendLeftY =
|
||||
Math.abs(leftY) > this.stickDeadzone ? Math.round(leftY) : 0;
|
||||
|
||||
const rightX = this.remapFromTo(
|
||||
this.gamepad.axes[2] ?? 0,
|
||||
-1,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
);
|
||||
const rightY = this.remapFromTo(
|
||||
this.gamepad.axes[3] ?? 0,
|
||||
-1,
|
||||
1,
|
||||
-32768,
|
||||
32767,
|
||||
);
|
||||
const sendRightX =
|
||||
Math.abs(rightX) > this.stickDeadzone ? Math.round(rightX) : 0;
|
||||
const sendRightY =
|
||||
Math.abs(rightY) > this.stickDeadzone ? Math.round(rightY) : 0;
|
||||
|
||||
const dpadX =
|
||||
(this.gamepad.buttons[14]?.pressed ? -1 : 0) +
|
||||
(this.gamepad.buttons[15]?.pressed ? 1 : 0);
|
||||
const dpadY =
|
||||
(this.gamepad.buttons[12]?.pressed ? -1 : 0) +
|
||||
(this.gamepad.buttons[13]?.pressed ? 1 : 0);
|
||||
|
||||
// Check what changed
|
||||
for (let i = 0; i < this.gamepad.buttons.length; i++) {
|
||||
if (i >= 6 && i <= 7) continue; // Skip triggers
|
||||
if (i >= 12 && i <= 15) continue; // Skip d-pad
|
||||
if (this.state.buttonState.get(i) !== this.gamepad.buttons[i].pressed) {
|
||||
changedFields |= this.CHANGED_BUTTONS_STATE;
|
||||
}
|
||||
this.state.buttonState.set(i, this.gamepad.buttons[i].pressed);
|
||||
}
|
||||
if (leftTrigger !== this.state.leftTrigger) {
|
||||
changedFields |= this.CHANGED_LEFT_TRIGGER;
|
||||
}
|
||||
this.state.leftTrigger = leftTrigger;
|
||||
if (rightTrigger !== this.state.rightTrigger) {
|
||||
changedFields |= this.CHANGED_RIGHT_TRIGGER;
|
||||
}
|
||||
this.state.rightTrigger = rightTrigger;
|
||||
if (sendLeftX !== this.state.leftX) {
|
||||
changedFields |= this.CHANGED_LEFT_STICK_X;
|
||||
}
|
||||
this.state.leftX = sendLeftX;
|
||||
if (sendLeftY !== this.state.leftY) {
|
||||
changedFields |= this.CHANGED_LEFT_STICK_Y;
|
||||
}
|
||||
this.state.leftY = sendLeftY;
|
||||
if (sendRightX !== this.state.rightX) {
|
||||
changedFields |= this.CHANGED_RIGHT_STICK_X;
|
||||
}
|
||||
this.state.rightX = sendRightX;
|
||||
if (sendRightY !== this.state.rightY) {
|
||||
changedFields |= this.CHANGED_RIGHT_STICK_Y;
|
||||
}
|
||||
this.state.rightY = sendRightY;
|
||||
if (dpadX !== this.state.dpadX) {
|
||||
changedFields |= this.CHANGED_DPAD_X;
|
||||
}
|
||||
this.state.dpadX = dpadX;
|
||||
if (dpadY !== this.state.dpadY) {
|
||||
changedFields |= this.CHANGED_DPAD_Y;
|
||||
}
|
||||
this.state.dpadY = dpadY;
|
||||
|
||||
return changedFields;
|
||||
}
|
||||
|
||||
private sendBatchedState(changedFields: number, updateType: number) {
|
||||
// @ts-ignore
|
||||
let message: ProtoControllerStateBatch = {
|
||||
sessionSlot: this.gamepad.index,
|
||||
sessionId: this.wrtc.getSessionID(),
|
||||
updateType: updateType,
|
||||
sequence: this.sequence++,
|
||||
};
|
||||
|
||||
// For FULL_STATE, include everything
|
||||
if (updateType === 0) {
|
||||
message.changedFields = 0xff;
|
||||
|
||||
message.buttonChangedMask = Object.fromEntries(
|
||||
Array.from(this.state.buttonState)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
[this.controllerButtonToVirtualKeyCode(key), value] as const,
|
||||
)
|
||||
.filter(([code]) => code !== undefined),
|
||||
);
|
||||
message.leftStickX = this.state.leftX;
|
||||
message.leftStickY = this.state.leftY;
|
||||
message.rightStickX = this.state.rightX;
|
||||
message.rightStickY = this.state.rightY;
|
||||
message.leftTrigger = this.state.leftTrigger;
|
||||
message.rightTrigger = this.state.rightTrigger;
|
||||
message.dpadX = this.state.dpadX;
|
||||
message.dpadY = this.state.dpadY;
|
||||
}
|
||||
// For DELTA, only include changed fields
|
||||
else {
|
||||
message.changedFields = changedFields;
|
||||
|
||||
if (changedFields & this.CHANGED_BUTTONS_STATE) {
|
||||
const currentStateMap = this.state.buttonState;
|
||||
const previousStateMap = this.state.previousButtonState;
|
||||
const allKeys = new Set([
|
||||
// @ts-ignore
|
||||
...currentStateMap.keys(),
|
||||
// @ts-ignore
|
||||
...previousStateMap.keys(),
|
||||
]);
|
||||
message.buttonChangedMask = Object.fromEntries(
|
||||
Array.from(allKeys)
|
||||
.filter((key) => {
|
||||
const newState = currentStateMap.get(key);
|
||||
const oldState = previousStateMap.get(key);
|
||||
return newState !== oldState;
|
||||
})
|
||||
.map((key) => {
|
||||
const newValue = currentStateMap.get(key) ?? false;
|
||||
return [
|
||||
this.controllerButtonToVirtualKeyCode(key),
|
||||
newValue,
|
||||
] as const;
|
||||
})
|
||||
.filter(([code]) => code !== undefined),
|
||||
/* 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 (changedFields & this.CHANGED_LEFT_STICK_X) {
|
||||
message.leftStickX = this.state.leftX;
|
||||
}
|
||||
if (changedFields & this.CHANGED_LEFT_STICK_Y) {
|
||||
message.leftStickY = this.state.leftY;
|
||||
}
|
||||
if (changedFields & this.CHANGED_RIGHT_STICK_X) {
|
||||
message.rightStickX = this.state.rightX;
|
||||
}
|
||||
if (changedFields & this.CHANGED_RIGHT_STICK_Y) {
|
||||
message.rightStickY = this.state.rightY;
|
||||
}
|
||||
if (changedFields & this.CHANGED_LEFT_TRIGGER) {
|
||||
message.leftTrigger = this.state.leftTrigger;
|
||||
}
|
||||
if (changedFields & this.CHANGED_RIGHT_TRIGGER) {
|
||||
message.rightTrigger = this.state.rightTrigger;
|
||||
}
|
||||
if (changedFields & this.CHANGED_DPAD_X) {
|
||||
message.dpadX = this.state.dpadX;
|
||||
}
|
||||
if (changedFields & this.CHANGED_DPAD_Y) {
|
||||
message.dpadY = this.state.dpadY;
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send message
|
||||
const batchMessage = createMessage(
|
||||
create(
|
||||
ProtoControllerStateBatchSchema,
|
||||
message as ProtoControllerStateBatch,
|
||||
),
|
||||
"controllerInput",
|
||||
);
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, batchMessage));
|
||||
}
|
||||
|
||||
private loopInterval: any = null;
|
||||
|
||||
public run() {
|
||||
if (this.connected) this.stop();
|
||||
if (this.connected)
|
||||
this.stop();
|
||||
|
||||
this.connected = true;
|
||||
|
||||
// Start with active polling
|
||||
this.restartPolling();
|
||||
// Poll gamepads in setInterval loop
|
||||
this.loopInterval = setInterval(() => {
|
||||
if (this.connected) this.pollGamepad();
|
||||
}, this.updateInterval);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
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._dcHandler !== null) {
|
||||
this.wrtc.removeDataChannelCallback(this._dcHandler);
|
||||
this._dcHandler = null;
|
||||
if (this._dcRumbleHandler !== null) {
|
||||
this.wrtc.removeDataChannelCallback(this._dcRumbleHandler);
|
||||
this._dcRumbleHandler = null;
|
||||
}
|
||||
if (this.gamepad) {
|
||||
// Gamepad disconnected
|
||||
const detachMsg = createMessage(
|
||||
create(ProtoControllerDetachSchema, {
|
||||
sessionSlot: this.gamepad.index,
|
||||
// Gamepad disconnected
|
||||
const detachMsg = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "controllerDetach",
|
||||
value: create(ProtoControllerDetachSchema, {
|
||||
type: "ControllerDetach",
|
||||
slot: this.slot,
|
||||
}),
|
||||
"controllerInput",
|
||||
);
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, detachMsg));
|
||||
}
|
||||
},
|
||||
});
|
||||
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): number | undefined {
|
||||
private controllerButtonToVirtualKeyCode(code: number) {
|
||||
return controllerButtonToLinuxEventCode[code] || undefined;
|
||||
}
|
||||
|
||||
private rumbleCallback(rumbleMsg: ProtoControllerRumble) {
|
||||
if (!this.connected || !this.gamepad) return;
|
||||
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 this rumble is for us
|
||||
if (
|
||||
rumbleMsg.sessionId !== this.wrtc.getSessionID() ||
|
||||
rumbleMsg.sessionSlot !== this.gamepad.index
|
||||
)
|
||||
return;
|
||||
// Check if it contains controller rumble data
|
||||
if (messageWrapper.data?.inputType?.case === "controllerRumble") {
|
||||
const rumbleMsg = messageWrapper.data.inputType.value as ProtoControllerRumble;
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { keyCodeToLinuxEventCode } from "./codes";
|
||||
import { WebRTCStream } from "./webrtc-stream";
|
||||
import { ProtoKeyDownSchema, ProtoKeyUpSchema } from "./proto/types_pb";
|
||||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { createMessage } from "./utils";
|
||||
import { ProtoMessageSchema } from "./proto/messages_pb";
|
||||
import {keyCodeToLinuxEventCode} from "./codes"
|
||||
import {WebRTCStream} from "./webrtc-stream";
|
||||
import {LatencyTracker} from "./latency";
|
||||
import {ProtoLatencyTracker, ProtoTimestampEntry} from "./proto/latency_tracker_pb";
|
||||
import {timestampFromDate} from "@bufbuild/protobuf/wkt";
|
||||
import {ProtoMessageBase, ProtoMessageInput, ProtoMessageInputSchema} from "./proto/messages_pb";
|
||||
import {
|
||||
ProtoInput,
|
||||
ProtoInputSchema,
|
||||
ProtoKeyDownSchema,
|
||||
ProtoKeyUpSchema,
|
||||
} from "./proto/types_pb";
|
||||
import {create, toBinary} from "@bufbuild/protobuf";
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCStream;
|
||||
@@ -17,29 +24,38 @@ export class Keyboard {
|
||||
private readonly keydownListener: (e: KeyboardEvent) => void;
|
||||
private readonly keyupListener: (e: KeyboardEvent) => void;
|
||||
|
||||
constructor({ webrtc }: Props) {
|
||||
constructor({webrtc}: Props) {
|
||||
this.wrtc = webrtc;
|
||||
this.keydownListener = this.createKeyboardListener((e: any) =>
|
||||
create(ProtoKeyDownSchema, {
|
||||
key: this.keyToVirtualKeyCode(e.code),
|
||||
}),
|
||||
);
|
||||
this.keyupListener = this.createKeyboardListener((e: any) =>
|
||||
create(ProtoKeyUpSchema, {
|
||||
key: this.keyToVirtualKeyCode(e.code),
|
||||
}),
|
||||
);
|
||||
this.run();
|
||||
this.keydownListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "keyDown",
|
||||
value: create(ProtoKeyDownSchema, {
|
||||
type: "KeyDown",
|
||||
key: this.keyToVirtualKeyCode(e.code)
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.keyupListener = this.createKeyboardListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "keyUp",
|
||||
value: create(ProtoKeyUpSchema, {
|
||||
type: "KeyUp",
|
||||
key: this.keyToVirtualKeyCode(e.code)
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.run()
|
||||
}
|
||||
|
||||
private run() {
|
||||
if (this.connected) this.stop();
|
||||
if (this.connected)
|
||||
this.stop()
|
||||
|
||||
this.connected = true;
|
||||
document.addEventListener("keydown", this.keydownListener, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("keyup", this.keyupListener, { passive: false });
|
||||
this.connected = true
|
||||
document.addEventListener("keydown", this.keydownListener, {passive: false});
|
||||
document.addEventListener("keyup", this.keyupListener, {passive: false});
|
||||
}
|
||||
|
||||
private stop() {
|
||||
@@ -49,19 +65,42 @@ export class Keyboard {
|
||||
}
|
||||
|
||||
// Helper function to create and return mouse listeners
|
||||
private createKeyboardListener(
|
||||
dataCreator: (e: Event) => any,
|
||||
): (e: Event) => void {
|
||||
private createKeyboardListener(dataCreator: (e: Event) => ProtoInput): (e: Event) => void {
|
||||
return (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Prevent repeated key events from being sent (important for games)
|
||||
if ((e as any).repeat) return;
|
||||
if ((e as any).repeat)
|
||||
return;
|
||||
|
||||
const data = dataCreator(e as any);
|
||||
|
||||
const message = createMessage(data, "input");
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
|
||||
// Latency tracking
|
||||
const tracker = new LatencyTracker("input-keyboard");
|
||||
tracker.addTimestamp("client_send");
|
||||
const protoTracker: ProtoLatencyTracker = {
|
||||
$typeName: "proto.ProtoLatencyTracker",
|
||||
sequenceId: tracker.sequence_id,
|
||||
timestamps: [],
|
||||
};
|
||||
for (const t of tracker.timestamps) {
|
||||
protoTracker.timestamps.push({
|
||||
$typeName: "proto.ProtoTimestampEntry",
|
||||
stage: t.stage,
|
||||
time: timestampFromDate(t.time),
|
||||
} as ProtoTimestampEntry);
|
||||
}
|
||||
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "input",
|
||||
latency: protoTracker,
|
||||
} as ProtoMessageBase,
|
||||
data: data,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,4 +114,4 @@ export class Keyboard {
|
||||
if (code === "Home") return 1;
|
||||
return keyCodeToLinuxEventCode[code] || undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
305
packages/input/src/messages.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { LatencyTracker } from "./latency";
|
||||
import { Uint8ArrayList } from "uint8arraylist";
|
||||
import { allocUnsafe } from "uint8arrays/alloc";
|
||||
import { pipe } from "it-pipe";
|
||||
import { decode, encode } from "it-length-prefixed";
|
||||
import { Stream } from "@libp2p/interface";
|
||||
|
||||
export interface MessageBase {
|
||||
payload_type: string;
|
||||
latency?: LatencyTracker;
|
||||
}
|
||||
|
||||
export interface MessageRaw extends MessageBase {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export function NewMessageRaw(type: string, data: any): Uint8Array {
|
||||
const msg = {
|
||||
payload_type: type,
|
||||
data: data,
|
||||
};
|
||||
return new TextEncoder().encode(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
export interface MessageICE extends MessageBase {
|
||||
candidate: RTCIceCandidateInit;
|
||||
}
|
||||
|
||||
export function NewMessageICE(
|
||||
type: string,
|
||||
candidate: RTCIceCandidateInit,
|
||||
): Uint8Array {
|
||||
const msg = {
|
||||
payload_type: type,
|
||||
candidate: candidate,
|
||||
};
|
||||
return new TextEncoder().encode(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
export interface MessageSDP extends MessageBase {
|
||||
sdp: RTCSessionDescriptionInit;
|
||||
}
|
||||
|
||||
export function NewMessageSDP(
|
||||
type: string,
|
||||
sdp: RTCSessionDescriptionInit,
|
||||
): Uint8Array {
|
||||
const msg = {
|
||||
payload_type: type,
|
||||
sdp: sdp,
|
||||
};
|
||||
return new TextEncoder().encode(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
const MAX_SIZE = 1024 * 1024; // 1MB
|
||||
const MAX_QUEUE_SIZE = 1000; // Maximum number of messages in the queue
|
||||
|
||||
// Custom 4-byte length encoder
|
||||
export const length4ByteEncoder = (length: number) => {
|
||||
const buf = allocUnsafe(4);
|
||||
|
||||
// Write the length as a 32-bit unsigned integer (4 bytes)
|
||||
buf[0] = length >>> 24;
|
||||
buf[1] = (length >>> 16) & 0xff;
|
||||
buf[2] = (length >>> 8) & 0xff;
|
||||
buf[3] = length & 0xff;
|
||||
|
||||
// Set the bytes property to 4
|
||||
length4ByteEncoder.bytes = 4;
|
||||
|
||||
return buf;
|
||||
};
|
||||
length4ByteEncoder.bytes = 4;
|
||||
|
||||
// Custom 4-byte length decoder
|
||||
export const length4ByteDecoder = (data: Uint8ArrayList) => {
|
||||
if (data.byteLength < 4) {
|
||||
// Not enough bytes to read the length
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the length from the first 4 bytes
|
||||
let length = 0;
|
||||
length =
|
||||
(data.subarray(0, 1)[0] >>> 0) * 0x1000000 +
|
||||
(data.subarray(1, 2)[0] >>> 0) * 0x10000 +
|
||||
(data.subarray(2, 3)[0] >>> 0) * 0x100 +
|
||||
(data.subarray(3, 4)[0] >>> 0);
|
||||
|
||||
// Set bytes read to 4
|
||||
length4ByteDecoder.bytes = 4;
|
||||
|
||||
return length;
|
||||
};
|
||||
length4ByteDecoder.bytes = 4;
|
||||
|
||||
interface PromiseMessage {
|
||||
data: Uint8Array;
|
||||
resolve: () => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
export class SafeStream {
|
||||
private stream: Stream;
|
||||
private callbacks: Map<string, ((data: any) => void)[]> = new Map();
|
||||
private isReading: boolean = false;
|
||||
private isWriting: boolean = false;
|
||||
private closed: boolean = false;
|
||||
private messageQueue: PromiseMessage[] = [];
|
||||
private writeLock = false;
|
||||
private readRetries = 0;
|
||||
private writeRetries = 0;
|
||||
private readonly MAX_RETRIES = 5;
|
||||
|
||||
constructor(stream: Stream) {
|
||||
this.stream = stream;
|
||||
this.startReading();
|
||||
this.startWriting();
|
||||
}
|
||||
|
||||
private async startReading(): Promise<void> {
|
||||
if (this.isReading || this.closed) return;
|
||||
|
||||
this.isReading = true;
|
||||
|
||||
try {
|
||||
const source = this.stream.source;
|
||||
const decodedSource = decode(source, {
|
||||
maxDataLength: MAX_SIZE,
|
||||
lengthDecoder: length4ByteDecoder,
|
||||
});
|
||||
|
||||
for await (const chunk of decodedSource) {
|
||||
if (this.closed) break;
|
||||
|
||||
this.readRetries = 0;
|
||||
|
||||
try {
|
||||
const data = chunk.slice();
|
||||
const message = JSON.parse(
|
||||
new TextDecoder().decode(data),
|
||||
) as MessageBase;
|
||||
const msgType = message.payload_type;
|
||||
|
||||
if (this.callbacks.has(msgType)) {
|
||||
const handlers = this.callbacks.get(msgType)!;
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
handler(message);
|
||||
} catch (err) {
|
||||
console.error(`Error in message handler for ${msgType}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error processing message:", err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Stream reading error:", err);
|
||||
} finally {
|
||||
this.isReading = false;
|
||||
this.readRetries++;
|
||||
|
||||
// If not closed, try to restart reading
|
||||
if (!this.closed && this.readRetries < this.MAX_RETRIES)
|
||||
setTimeout(() => this.startReading(), 100);
|
||||
else if (this.readRetries >= this.MAX_RETRIES)
|
||||
console.error(
|
||||
"Max retries reached for reading stream, stopping attempts",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public registerCallback(
|
||||
msgType: string,
|
||||
callback: (data: any) => void,
|
||||
): void {
|
||||
if (!this.callbacks.has(msgType)) {
|
||||
this.callbacks.set(msgType, []);
|
||||
}
|
||||
|
||||
this.callbacks.get(msgType)!.push(callback);
|
||||
}
|
||||
|
||||
public removeCallback(msgType: string, callback: (data: any) => void): void {
|
||||
if (this.callbacks.has(msgType)) {
|
||||
const callbacks = this.callbacks.get(msgType)!;
|
||||
const index = callbacks.indexOf(callback);
|
||||
|
||||
if (index !== -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
if (callbacks.length === 0) {
|
||||
this.callbacks.delete(msgType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async startWriting(): Promise<void> {
|
||||
if (this.isWriting || this.closed) return;
|
||||
|
||||
this.isWriting = true;
|
||||
|
||||
try {
|
||||
// Create an async generator for real-time message processing
|
||||
const messageSource = async function* (this: SafeStream) {
|
||||
while (!this.closed) {
|
||||
// Check if we have messages to send
|
||||
if (this.messageQueue.length > 0) {
|
||||
this.writeLock = true;
|
||||
|
||||
try {
|
||||
const message = this.messageQueue[0];
|
||||
|
||||
// Encode the message
|
||||
const encoded = encode([message.data], {
|
||||
maxDataLength: MAX_SIZE,
|
||||
lengthEncoder: length4ByteEncoder,
|
||||
});
|
||||
|
||||
for await (const chunk of encoded) {
|
||||
yield chunk;
|
||||
}
|
||||
|
||||
// Remove message after successful sending
|
||||
this.writeRetries = 0;
|
||||
const sentMessage = this.messageQueue.shift();
|
||||
if (sentMessage)
|
||||
sentMessage.resolve();
|
||||
} catch (err) {
|
||||
console.error("Error encoding or sending message:", err);
|
||||
const failedMessage = this.messageQueue.shift();
|
||||
if (failedMessage)
|
||||
failedMessage.reject(new Error(`Failed to send message: ${err}`));
|
||||
} finally {
|
||||
this.writeLock = false;
|
||||
}
|
||||
} else {
|
||||
// No messages to send, wait for a short period
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
await pipe(messageSource(), this.stream.sink).catch((err) => {
|
||||
console.error("Sink error:", err);
|
||||
this.isWriting = false;
|
||||
this.writeRetries++;
|
||||
|
||||
// Try to restart if not closed
|
||||
if (!this.closed && this.writeRetries < this.MAX_RETRIES) {
|
||||
setTimeout(() => this.startWriting(), 1000);
|
||||
} else if (this.writeRetries >= this.MAX_RETRIES) {
|
||||
console.error("Max retries reached for writing to stream sink, stopping attempts");
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Stream writing error:", err);
|
||||
this.isWriting = false;
|
||||
this.writeRetries++;
|
||||
|
||||
// Try to restart if not closed
|
||||
if (!this.closed && this.writeRetries < this.MAX_RETRIES) {
|
||||
setTimeout(() => this.startWriting(), 1000);
|
||||
} else if (this.writeRetries >= this.MAX_RETRIES) {
|
||||
console.error("Max retries reached for writing stream, stopping attempts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async writeMessage(message: Uint8Array): Promise<void> {
|
||||
if (this.closed) {
|
||||
throw new Error("Cannot write to closed stream");
|
||||
}
|
||||
|
||||
// Validate message size before queuing
|
||||
if (message.length > MAX_SIZE) {
|
||||
throw new Error("Message size exceeds maximum size limit");
|
||||
}
|
||||
|
||||
// Check if the message queue is too large
|
||||
if (this.messageQueue.length >= MAX_QUEUE_SIZE) {
|
||||
throw new Error("Message queue is full, cannot write message");
|
||||
}
|
||||
|
||||
// Create a promise to resolve when the message is sent
|
||||
return new Promise((resolve, reject) => {
|
||||
this.messageQueue.push({ data: message, resolve, reject } as PromiseMessage);
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.closed = true;
|
||||
this.callbacks.clear();
|
||||
// Reject pending messages
|
||||
for (const msg of this.messageQueue)
|
||||
msg.reject(new Error("Stream closed"));
|
||||
|
||||
this.messageQueue = [];
|
||||
this.readRetries = 0;
|
||||
this.writeRetries = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import { WebRTCStream } from "./webrtc-stream";
|
||||
import {WebRTCStream} from "./webrtc-stream";
|
||||
import {LatencyTracker} from "./latency";
|
||||
import {ProtoMessageInput, ProtoMessageBase, ProtoMessageInputSchema} from "./proto/messages_pb";
|
||||
import {
|
||||
ProtoMouseKeyDownSchema,
|
||||
ProtoMouseKeyUpSchema,
|
||||
ProtoInput, ProtoInputSchema,
|
||||
ProtoMouseKeyDown, ProtoMouseKeyDownSchema,
|
||||
ProtoMouseKeyUp, ProtoMouseKeyUpSchema,
|
||||
ProtoMouseMove,
|
||||
ProtoMouseMoveSchema,
|
||||
ProtoMouseWheelSchema,
|
||||
ProtoMouseWheel, ProtoMouseWheelSchema
|
||||
} from "./proto/types_pb";
|
||||
import { mouseButtonToLinuxEventCode } from "./codes";
|
||||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { createMessage } from "./utils";
|
||||
import { ProtoMessageSchema } from "./proto/messages_pb";
|
||||
import {mouseButtonToLinuxEventCode} from "./codes";
|
||||
import {ProtoLatencyTracker, ProtoTimestampEntry} from "./proto/latency_tracker_pb";
|
||||
import {create, toBinary} from "@bufbuild/protobuf";
|
||||
import {timestampFromDate} from "@bufbuild/protobuf/wkt";
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCStream;
|
||||
@@ -20,7 +24,7 @@ export class Mouse {
|
||||
protected canvas: HTMLCanvasElement;
|
||||
protected connected!: boolean;
|
||||
|
||||
private sendInterval = 10; // 100 updates per second
|
||||
private sendInterval = 10 // 100 updates per second
|
||||
|
||||
// Store references to event listeners
|
||||
private readonly mousemoveListener: (e: MouseEvent) => void;
|
||||
@@ -31,7 +35,7 @@ export class Mouse {
|
||||
private readonly mouseupListener: (e: MouseEvent) => void;
|
||||
private readonly mousewheelListener: (e: WheelEvent) => void;
|
||||
|
||||
constructor({ webrtc, canvas }: Props) {
|
||||
constructor({webrtc, canvas}: Props) {
|
||||
this.wrtc = webrtc;
|
||||
this.canvas = canvas;
|
||||
|
||||
@@ -44,56 +48,65 @@ export class Mouse {
|
||||
this.movementY += e.movementY;
|
||||
};
|
||||
|
||||
this.mousedownListener = this.createMouseListener((e: any) =>
|
||||
create(ProtoMouseKeyDownSchema, {
|
||||
key: this.keyToVirtualKeyCode(e.button),
|
||||
}),
|
||||
);
|
||||
this.mouseupListener = this.createMouseListener((e: any) =>
|
||||
create(ProtoMouseKeyUpSchema, {
|
||||
key: this.keyToVirtualKeyCode(e.button),
|
||||
}),
|
||||
);
|
||||
this.mousewheelListener = this.createMouseListener((e: any) =>
|
||||
create(ProtoMouseWheelSchema, {
|
||||
x: Math.round(e.deltaX),
|
||||
y: Math.round(e.deltaY),
|
||||
}),
|
||||
);
|
||||
this.mousedownListener = this.createMouseListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "mouseKeyDown",
|
||||
value: create(ProtoMouseKeyDownSchema, {
|
||||
type: "MouseKeyDown",
|
||||
key: this.keyToVirtualKeyCode(e.button)
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.mouseupListener = this.createMouseListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "mouseKeyUp",
|
||||
value: create(ProtoMouseKeyUpSchema, {
|
||||
type: "MouseKeyUp",
|
||||
key: this.keyToVirtualKeyCode(e.button)
|
||||
}),
|
||||
}
|
||||
}));
|
||||
this.mousewheelListener = this.createMouseListener((e: any) => create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "mouseWheel",
|
||||
value: create(ProtoMouseWheelSchema, {
|
||||
type: "MouseWheel",
|
||||
x: Math.round(e.deltaX),
|
||||
y: Math.round(e.deltaY),
|
||||
}),
|
||||
}
|
||||
}));
|
||||
|
||||
this.run();
|
||||
this.run()
|
||||
this.startProcessing();
|
||||
}
|
||||
|
||||
private run() {
|
||||
//calls all the other functions
|
||||
if (!document.pointerLockElement) {
|
||||
console.log("no pointerlock");
|
||||
console.log("no pointerlock")
|
||||
if (this.connected) {
|
||||
this.stop();
|
||||
this.stop()
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.pointerLockElement == this.canvas) {
|
||||
this.connected = true;
|
||||
this.canvas.addEventListener("mousemove", this.mousemoveListener, {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("mousedown", this.mousedownListener, {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("mouseup", this.mouseupListener, {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("wheel", this.mousewheelListener, {
|
||||
passive: false,
|
||||
});
|
||||
this.connected = true
|
||||
this.canvas.addEventListener("mousemove", this.mousemoveListener, {passive: false});
|
||||
this.canvas.addEventListener("mousedown", this.mousedownListener, {passive: false});
|
||||
this.canvas.addEventListener("mouseup", this.mouseupListener, {passive: false});
|
||||
this.canvas.addEventListener("wheel", this.mousewheelListener, {passive: false});
|
||||
|
||||
} else {
|
||||
if (this.connected) {
|
||||
this.stop();
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private stop() {
|
||||
@@ -115,26 +128,79 @@ export class Mouse {
|
||||
}
|
||||
|
||||
private sendAggregatedMouseMove() {
|
||||
const data = create(ProtoMouseMoveSchema, {
|
||||
x: Math.round(this.movementX),
|
||||
y: Math.round(this.movementY),
|
||||
const data = create(ProtoInputSchema, {
|
||||
$typeName: "proto.ProtoInput",
|
||||
inputType: {
|
||||
case: "mouseMove",
|
||||
value: create(ProtoMouseMoveSchema, {
|
||||
type: "MouseMove",
|
||||
x: Math.round(this.movementX),
|
||||
y: Math.round(this.movementY),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const message = createMessage(data, "input");
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
|
||||
// Latency tracking
|
||||
const tracker = new LatencyTracker("input-mouse");
|
||||
tracker.addTimestamp("client_send");
|
||||
const protoTracker: ProtoLatencyTracker = {
|
||||
$typeName: "proto.ProtoLatencyTracker",
|
||||
sequenceId: tracker.sequence_id,
|
||||
timestamps: [],
|
||||
};
|
||||
for (const t of tracker.timestamps) {
|
||||
protoTracker.timestamps.push({
|
||||
$typeName: "proto.ProtoTimestampEntry",
|
||||
stage: t.stage,
|
||||
time: timestampFromDate(t.time),
|
||||
} as ProtoTimestampEntry);
|
||||
}
|
||||
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "input",
|
||||
latency: protoTracker,
|
||||
} as ProtoMessageBase,
|
||||
data: data,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
}
|
||||
|
||||
// Helper function to create and return mouse listeners
|
||||
private createMouseListener(
|
||||
dataCreator: (e: Event) => any,
|
||||
): (e: Event) => void {
|
||||
private createMouseListener(dataCreator: (e: Event) => ProtoInput): (e: Event) => void {
|
||||
return (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const data = dataCreator(e as any);
|
||||
|
||||
const message = createMessage(data, "input");
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageSchema, message));
|
||||
// Latency tracking
|
||||
const tracker = new LatencyTracker("input-mouse");
|
||||
tracker.addTimestamp("client_send");
|
||||
const protoTracker: ProtoLatencyTracker = {
|
||||
$typeName: "proto.ProtoLatencyTracker",
|
||||
sequenceId: tracker.sequence_id,
|
||||
timestamps: [],
|
||||
};
|
||||
for (const t of tracker.timestamps) {
|
||||
protoTracker.timestamps.push({
|
||||
$typeName: "proto.ProtoTimestampEntry",
|
||||
stage: t.stage,
|
||||
time: timestampFromDate(t.time),
|
||||
} as ProtoTimestampEntry);
|
||||
}
|
||||
|
||||
const message: ProtoMessageInput = {
|
||||
$typeName: "proto.ProtoMessageInput",
|
||||
messageBase: {
|
||||
$typeName: "proto.ProtoMessageBase",
|
||||
payloadType: "input",
|
||||
latency: protoTracker,
|
||||
} as ProtoMessageBase,
|
||||
data: data,
|
||||
};
|
||||
this.wrtc.sendBinary(toBinary(ProtoMessageInputSchema, message));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,4 +213,4 @@ export class Mouse {
|
||||
private keyToVirtualKeyCode(code: number) {
|
||||
return mouseButtonToLinuxEventCode[code] || undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated from file latency_tracker.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated from file messages.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { ProtoClientDisconnected, ProtoClientRequestRoomStream, ProtoControllerAttach, ProtoControllerDetach, ProtoControllerRumble, ProtoControllerStateBatch, ProtoICE, ProtoKeyDown, ProtoKeyUp, ProtoMouseKeyDown, ProtoMouseKeyUp, ProtoMouseMove, ProtoMouseMoveAbs, ProtoMouseWheel, ProtoRaw, ProtoSDP, ProtoServerPushStream } from "./types_pb";
|
||||
import type { ProtoInput } from "./types_pb";
|
||||
import { file_types } from "./types_pb";
|
||||
import type { ProtoLatencyTracker } from "./latency_tracker_pb";
|
||||
import { file_latency_tracker } from "./latency_tracker_pb";
|
||||
@@ -14,7 +14,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file messages.proto.
|
||||
*/
|
||||
export const file_messages: GenFile = /*@__PURE__*/
|
||||
fileDesc("Cg5tZXNzYWdlcy5wcm90bxIFcHJvdG8iVQoQUHJvdG9NZXNzYWdlQmFzZRIUCgxwYXlsb2FkX3R5cGUYASABKAkSKwoHbGF0ZW5jeRgCIAEoCzIaLnByb3RvLlByb3RvTGF0ZW5jeVRyYWNrZXIipQcKDFByb3RvTWVzc2FnZRItCgxtZXNzYWdlX2Jhc2UYASABKAsyFy5wcm90by5Qcm90b01lc3NhZ2VCYXNlEisKCm1vdXNlX21vdmUYAiABKAsyFS5wcm90by5Qcm90b01vdXNlTW92ZUgAEjIKDm1vdXNlX21vdmVfYWJzGAMgASgLMhgucHJvdG8uUHJvdG9Nb3VzZU1vdmVBYnNIABItCgttb3VzZV93aGVlbBgEIAEoCzIWLnByb3RvLlByb3RvTW91c2VXaGVlbEgAEjIKDm1vdXNlX2tleV9kb3duGAUgASgLMhgucHJvdG8uUHJvdG9Nb3VzZUtleURvd25IABIuCgxtb3VzZV9rZXlfdXAYBiABKAsyFi5wcm90by5Qcm90b01vdXNlS2V5VXBIABInCghrZXlfZG93bhgHIAEoCzITLnByb3RvLlByb3RvS2V5RG93bkgAEiMKBmtleV91cBgIIAEoCzIRLnByb3RvLlByb3RvS2V5VXBIABI5ChFjb250cm9sbGVyX2F0dGFjaBgJIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckF0dGFjaEgAEjkKEWNvbnRyb2xsZXJfZGV0YWNoGAogASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyRGV0YWNoSAASOQoRY29udHJvbGxlcl9ydW1ibGUYCyABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIABJCChZjb250cm9sbGVyX3N0YXRlX2JhdGNoGAwgASgLMiAucHJvdG8uUHJvdG9Db250cm9sbGVyU3RhdGVCYXRjaEgAEh4KA2ljZRgUIAEoCzIPLnByb3RvLlByb3RvSUNFSAASHgoDc2RwGBUgASgLMg8ucHJvdG8uUHJvdG9TRFBIABIeCgNyYXcYFiABKAsyDy5wcm90by5Qcm90b1Jhd0gAEkkKGmNsaWVudF9yZXF1ZXN0X3Jvb21fc3RyZWFtGBcgASgLMiMucHJvdG8uUHJvdG9DbGllbnRSZXF1ZXN0Um9vbVN0cmVhbUgAEj0KE2NsaWVudF9kaXNjb25uZWN0ZWQYGCABKAsyHi5wcm90by5Qcm90b0NsaWVudERpc2Nvbm5lY3RlZEgAEjoKEnNlcnZlcl9wdXNoX3N0cmVhbRgZIAEoCzIcLnByb3RvLlByb3RvU2VydmVyUHVzaFN0cmVhbUgAQgkKB3BheWxvYWRCFloUcmVsYXkvaW50ZXJuYWwvcHJvdG9iBnByb3RvMw", [file_types, file_latency_tracker]);
|
||||
fileDesc("Cg5tZXNzYWdlcy5wcm90bxIFcHJvdG8iVQoQUHJvdG9NZXNzYWdlQmFzZRIUCgxwYXlsb2FkX3R5cGUYASABKAkSKwoHbGF0ZW5jeRgCIAEoCzIaLnByb3RvLlByb3RvTGF0ZW5jeVRyYWNrZXIiYwoRUHJvdG9NZXNzYWdlSW5wdXQSLQoMbWVzc2FnZV9iYXNlGAEgASgLMhcucHJvdG8uUHJvdG9NZXNzYWdlQmFzZRIfCgRkYXRhGAIgASgLMhEucHJvdG8uUHJvdG9JbnB1dEIWWhRyZWxheS9pbnRlcm5hbC9wcm90b2IGcHJvdG8z", [file_types, file_latency_tracker]);
|
||||
|
||||
/**
|
||||
* @generated from message proto.ProtoMessageBase
|
||||
@@ -39,132 +39,24 @@ export const ProtoMessageBaseSchema: GenMessage<ProtoMessageBase> = /*@__PURE__*
|
||||
messageDesc(file_messages, 0);
|
||||
|
||||
/**
|
||||
* @generated from message proto.ProtoMessage
|
||||
* @generated from message proto.ProtoMessageInput
|
||||
*/
|
||||
export type ProtoMessage = Message<"proto.ProtoMessage"> & {
|
||||
export type ProtoMessageInput = Message<"proto.ProtoMessageInput"> & {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMessageBase message_base = 1;
|
||||
*/
|
||||
messageBase?: ProtoMessageBase;
|
||||
|
||||
/**
|
||||
* @generated from oneof proto.ProtoMessage.payload
|
||||
* @generated from field: proto.ProtoInput data = 2;
|
||||
*/
|
||||
payload: {
|
||||
/**
|
||||
* Input types
|
||||
*
|
||||
* @generated from field: proto.ProtoMouseMove mouse_move = 2;
|
||||
*/
|
||||
value: ProtoMouseMove;
|
||||
case: "mouseMove";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseMoveAbs mouse_move_abs = 3;
|
||||
*/
|
||||
value: ProtoMouseMoveAbs;
|
||||
case: "mouseMoveAbs";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseWheel mouse_wheel = 4;
|
||||
*/
|
||||
value: ProtoMouseWheel;
|
||||
case: "mouseWheel";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseKeyDown mouse_key_down = 5;
|
||||
*/
|
||||
value: ProtoMouseKeyDown;
|
||||
case: "mouseKeyDown";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseKeyUp mouse_key_up = 6;
|
||||
*/
|
||||
value: ProtoMouseKeyUp;
|
||||
case: "mouseKeyUp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoKeyDown key_down = 7;
|
||||
*/
|
||||
value: ProtoKeyDown;
|
||||
case: "keyDown";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoKeyUp key_up = 8;
|
||||
*/
|
||||
value: ProtoKeyUp;
|
||||
case: "keyUp";
|
||||
} | {
|
||||
/**
|
||||
* Controller input types
|
||||
*
|
||||
* @generated from field: proto.ProtoControllerAttach controller_attach = 9;
|
||||
*/
|
||||
value: ProtoControllerAttach;
|
||||
case: "controllerAttach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerDetach controller_detach = 10;
|
||||
*/
|
||||
value: ProtoControllerDetach;
|
||||
case: "controllerDetach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerRumble controller_rumble = 11;
|
||||
*/
|
||||
value: ProtoControllerRumble;
|
||||
case: "controllerRumble";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerStateBatch controller_state_batch = 12;
|
||||
*/
|
||||
value: ProtoControllerStateBatch;
|
||||
case: "controllerStateBatch";
|
||||
} | {
|
||||
/**
|
||||
* Signaling types
|
||||
*
|
||||
* @generated from field: proto.ProtoICE ice = 20;
|
||||
*/
|
||||
value: ProtoICE;
|
||||
case: "ice";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoSDP sdp = 21;
|
||||
*/
|
||||
value: ProtoSDP;
|
||||
case: "sdp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoRaw raw = 22;
|
||||
*/
|
||||
value: ProtoRaw;
|
||||
case: "raw";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoClientRequestRoomStream client_request_room_stream = 23;
|
||||
*/
|
||||
value: ProtoClientRequestRoomStream;
|
||||
case: "clientRequestRoomStream";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoClientDisconnected client_disconnected = 24;
|
||||
*/
|
||||
value: ProtoClientDisconnected;
|
||||
case: "clientDisconnected";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoServerPushStream server_push_stream = 25;
|
||||
*/
|
||||
value: ProtoServerPushStream;
|
||||
case: "serverPushStream";
|
||||
} | { case: undefined; value?: undefined };
|
||||
data?: ProtoInput;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoMessage.
|
||||
* Use `create(ProtoMessageSchema)` to create a new message.
|
||||
* Describes the message proto.ProtoMessageInput.
|
||||
* Use `create(ProtoMessageInputSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoMessageSchema: GenMessage<ProtoMessage> = /*@__PURE__*/
|
||||
export const ProtoMessageInputSchema: GenMessage<ProtoMessageInput> = /*@__PURE__*/
|
||||
messageDesc(file_messages, 1);
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.9.0 with parameter "target=ts"
|
||||
// @generated from file types.proto (package proto, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file types.proto.
|
||||
*/
|
||||
export const file_types: GenFile = /*@__PURE__*/
|
||||
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iJgoOUHJvdG9Nb3VzZU1vdmUSCQoBeBgBIAEoBRIJCgF5GAIgASgFIikKEVByb3RvTW91c2VNb3ZlQWJzEgkKAXgYASABKAUSCQoBeRgCIAEoBSInCg9Qcm90b01vdXNlV2hlZWwSCQoBeBgBIAEoBRIJCgF5GAIgASgFIiAKEVByb3RvTW91c2VLZXlEb3duEgsKA2tleRgBIAEoBSIeCg9Qcm90b01vdXNlS2V5VXASCwoDa2V5GAEgASgFIhsKDFByb3RvS2V5RG93bhILCgNrZXkYASABKAUiGQoKUHJvdG9LZXlVcBILCgNrZXkYASABKAUiTQoVUHJvdG9Db250cm9sbGVyQXR0YWNoEgoKAmlkGAEgASgJEhQKDHNlc3Npb25fc2xvdBgCIAEoBRISCgpzZXNzaW9uX2lkGAMgASgJIkEKFVByb3RvQ29udHJvbGxlckRldGFjaBIUCgxzZXNzaW9uX3Nsb3QYASABKAUSEgoKc2Vzc2lvbl9pZBgCIAEoCSKCAQoVUHJvdG9Db250cm9sbGVyUnVtYmxlEhQKDHNlc3Npb25fc2xvdBgBIAEoBRISCgpzZXNzaW9uX2lkGAIgASgJEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi0AUKGVByb3RvQ29udHJvbGxlclN0YXRlQmF0Y2gSFAoMc2Vzc2lvbl9zbG90GAEgASgFEhIKCnNlc3Npb25faWQYAiABKAkSQAoLdXBkYXRlX3R5cGUYAyABKA4yKy5wcm90by5Qcm90b0NvbnRyb2xsZXJTdGF0ZUJhdGNoLlVwZGF0ZVR5cGUSEAoIc2VxdWVuY2UYBCABKA0SVAoTYnV0dG9uX2NoYW5nZWRfbWFzaxgFIAMoCzI3LnByb3RvLlByb3RvQ29udHJvbGxlclN0YXRlQmF0Y2guQnV0dG9uQ2hhbmdlZE1hc2tFbnRyeRIZCgxsZWZ0X3N0aWNrX3gYBiABKAVIAIgBARIZCgxsZWZ0X3N0aWNrX3kYByABKAVIAYgBARIaCg1yaWdodF9zdGlja194GAggASgFSAKIAQESGgoNcmlnaHRfc3RpY2tfeRgJIAEoBUgDiAEBEhkKDGxlZnRfdHJpZ2dlchgKIAEoBUgEiAEBEhoKDXJpZ2h0X3RyaWdnZXIYCyABKAVIBYgBARITCgZkcGFkX3gYDCABKAVIBogBARITCgZkcGFkX3kYDSABKAVIB4gBARIbCg5jaGFuZ2VkX2ZpZWxkcxgOIAEoDUgIiAEBGjgKFkJ1dHRvbkNoYW5nZWRNYXNrRW50cnkSCwoDa2V5GAEgASgFEg0KBXZhbHVlGAIgASgIOgI4ASInCgpVcGRhdGVUeXBlEg4KCkZVTExfU1RBVEUQABIJCgVERUxUQRABQg8KDV9sZWZ0X3N0aWNrX3hCDwoNX2xlZnRfc3RpY2tfeUIQCg5fcmlnaHRfc3RpY2tfeEIQCg5fcmlnaHRfc3RpY2tfeUIPCg1fbGVmdF90cmlnZ2VyQhAKDl9yaWdodF90cmlnZ2VyQgkKB19kcGFkX3hCCQoHX2RwYWRfeUIRCg9fY2hhbmdlZF9maWVsZHMiqgEKE1JUQ0ljZUNhbmRpZGF0ZUluaXQSEQoJY2FuZGlkYXRlGAEgASgJEhoKDXNkcE1MaW5lSW5kZXgYAiABKA1IAIgBARITCgZzZHBNaWQYAyABKAlIAYgBARIdChB1c2VybmFtZUZyYWdtZW50GAQgASgJSAKIAQFCEAoOX3NkcE1MaW5lSW5kZXhCCQoHX3NkcE1pZEITChFfdXNlcm5hbWVGcmFnbWVudCI2ChlSVENTZXNzaW9uRGVzY3JpcHRpb25Jbml0EgsKA3NkcBgBIAEoCRIMCgR0eXBlGAIgASgJIjkKCFByb3RvSUNFEi0KCWNhbmRpZGF0ZRgBIAEoCzIaLnByb3RvLlJUQ0ljZUNhbmRpZGF0ZUluaXQiOQoIUHJvdG9TRFASLQoDc2RwGAEgASgLMiAucHJvdG8uUlRDU2Vzc2lvbkRlc2NyaXB0aW9uSW5pdCIYCghQcm90b1JhdxIMCgRkYXRhGAEgASgJIkUKHFByb3RvQ2xpZW50UmVxdWVzdFJvb21TdHJlYW0SEQoJcm9vbV9uYW1lGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkiRwoXUHJvdG9DbGllbnREaXNjb25uZWN0ZWQSEgoKc2Vzc2lvbl9pZBgBIAEoCRIYChBjb250cm9sbGVyX3Nsb3RzGAIgAygFIioKFVByb3RvU2VydmVyUHVzaFN0cmVhbRIRCglyb29tX25hbWUYASABKAlCFloUcmVsYXkvaW50ZXJuYWwvcHJvdG9iBnByb3RvMw");
|
||||
fileDesc("Cgt0eXBlcy5wcm90bxIFcHJvdG8iNAoOUHJvdG9Nb3VzZU1vdmUSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNwoRUHJvdG9Nb3VzZU1vdmVBYnMSDAoEdHlwZRgBIAEoCRIJCgF4GAIgASgFEgkKAXkYAyABKAUiNQoPUHJvdG9Nb3VzZVdoZWVsEgwKBHR5cGUYASABKAkSCQoBeBgCIAEoBRIJCgF5GAMgASgFIi4KEVByb3RvTW91c2VLZXlEb3duEgwKBHR5cGUYASABKAkSCwoDa2V5GAIgASgFIiwKD1Byb3RvTW91c2VLZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSIpCgxQcm90b0tleURvd24SDAoEdHlwZRgBIAEoCRILCgNrZXkYAiABKAUiJwoKUHJvdG9LZXlVcBIMCgR0eXBlGAEgASgJEgsKA2tleRgCIAEoBSI/ChVQcm90b0NvbnRyb2xsZXJBdHRhY2gSDAoEdHlwZRgBIAEoCRIKCgJpZBgCIAEoCRIMCgRzbG90GAMgASgFIjMKFVByb3RvQ29udHJvbGxlckRldGFjaBIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUiVAoVUHJvdG9Db250cm9sbGVyQnV0dG9uEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIOCgZidXR0b24YAyABKAUSDwoHcHJlc3NlZBgEIAEoCCJUChZQcm90b0NvbnRyb2xsZXJUcmlnZ2VyEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRIPCgd0cmlnZ2VyGAMgASgFEg0KBXZhbHVlGAQgASgFIlcKFFByb3RvQ29udHJvbGxlclN0aWNrEgwKBHR5cGUYASABKAkSDAoEc2xvdBgCIAEoBRINCgVzdGljaxgDIAEoBRIJCgF4GAQgASgFEgkKAXkYBSABKAUiTgoTUHJvdG9Db250cm9sbGVyQXhpcxIMCgR0eXBlGAEgASgJEgwKBHNsb3QYAiABKAUSDAoEYXhpcxgDIAEoBRINCgV2YWx1ZRgEIAEoBSJ0ChVQcm90b0NvbnRyb2xsZXJSdW1ibGUSDAoEdHlwZRgBIAEoCRIMCgRzbG90GAIgASgFEhUKDWxvd19mcmVxdWVuY3kYAyABKAUSFgoOaGlnaF9mcmVxdWVuY3kYBCABKAUSEAoIZHVyYXRpb24YBSABKAUi9QUKClByb3RvSW5wdXQSKwoKbW91c2VfbW92ZRgBIAEoCzIVLnByb3RvLlByb3RvTW91c2VNb3ZlSAASMgoObW91c2VfbW92ZV9hYnMYAiABKAsyGC5wcm90by5Qcm90b01vdXNlTW92ZUFic0gAEi0KC21vdXNlX3doZWVsGAMgASgLMhYucHJvdG8uUHJvdG9Nb3VzZVdoZWVsSAASMgoObW91c2Vfa2V5X2Rvd24YBCABKAsyGC5wcm90by5Qcm90b01vdXNlS2V5RG93bkgAEi4KDG1vdXNlX2tleV91cBgFIAEoCzIWLnByb3RvLlByb3RvTW91c2VLZXlVcEgAEicKCGtleV9kb3duGAYgASgLMhMucHJvdG8uUHJvdG9LZXlEb3duSAASIwoGa2V5X3VwGAcgASgLMhEucHJvdG8uUHJvdG9LZXlVcEgAEjkKEWNvbnRyb2xsZXJfYXR0YWNoGAggASgLMhwucHJvdG8uUHJvdG9Db250cm9sbGVyQXR0YWNoSAASOQoRY29udHJvbGxlcl9kZXRhY2gYCSABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJEZXRhY2hIABI5ChFjb250cm9sbGVyX2J1dHRvbhgKIAEoCzIcLnByb3RvLlByb3RvQ29udHJvbGxlckJ1dHRvbkgAEjsKEmNvbnRyb2xsZXJfdHJpZ2dlchgLIAEoCzIdLnByb3RvLlByb3RvQ29udHJvbGxlclRyaWdnZXJIABI3ChBjb250cm9sbGVyX3N0aWNrGAwgASgLMhsucHJvdG8uUHJvdG9Db250cm9sbGVyU3RpY2tIABI1Cg9jb250cm9sbGVyX2F4aXMYDSABKAsyGi5wcm90by5Qcm90b0NvbnRyb2xsZXJBeGlzSAASOQoRY29udHJvbGxlcl9ydW1ibGUYDiABKAsyHC5wcm90by5Qcm90b0NvbnRyb2xsZXJSdW1ibGVIAEIMCgppbnB1dF90eXBlQhZaFHJlbGF5L2ludGVybmFsL3Byb3RvYgZwcm90bzM");
|
||||
|
||||
/**
|
||||
* MouseMove message
|
||||
@@ -19,12 +19,19 @@ export const file_types: GenFile = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoMouseMove = Message<"proto.ProtoMouseMove"> & {
|
||||
/**
|
||||
* @generated from field: int32 x = 1;
|
||||
* Fixed value "MouseMove"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 x = 2;
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 y = 2;
|
||||
* @generated from field: int32 y = 3;
|
||||
*/
|
||||
y: number;
|
||||
};
|
||||
@@ -43,12 +50,19 @@ export const ProtoMouseMoveSchema: GenMessage<ProtoMouseMove> = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoMouseMoveAbs = Message<"proto.ProtoMouseMoveAbs"> & {
|
||||
/**
|
||||
* @generated from field: int32 x = 1;
|
||||
* Fixed value "MouseMoveAbs"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 x = 2;
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 y = 2;
|
||||
* @generated from field: int32 y = 3;
|
||||
*/
|
||||
y: number;
|
||||
};
|
||||
@@ -67,12 +81,19 @@ export const ProtoMouseMoveAbsSchema: GenMessage<ProtoMouseMoveAbs> = /*@__PURE_
|
||||
*/
|
||||
export type ProtoMouseWheel = Message<"proto.ProtoMouseWheel"> & {
|
||||
/**
|
||||
* @generated from field: int32 x = 1;
|
||||
* Fixed value "MouseWheel"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 x = 2;
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 y = 2;
|
||||
* @generated from field: int32 y = 3;
|
||||
*/
|
||||
y: number;
|
||||
};
|
||||
@@ -91,7 +112,14 @@ export const ProtoMouseWheelSchema: GenMessage<ProtoMouseWheel> = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoMouseKeyDown = Message<"proto.ProtoMouseKeyDown"> & {
|
||||
/**
|
||||
* @generated from field: int32 key = 1;
|
||||
* Fixed value "MouseKeyDown"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 key = 2;
|
||||
*/
|
||||
key: number;
|
||||
};
|
||||
@@ -110,7 +138,14 @@ export const ProtoMouseKeyDownSchema: GenMessage<ProtoMouseKeyDown> = /*@__PURE_
|
||||
*/
|
||||
export type ProtoMouseKeyUp = Message<"proto.ProtoMouseKeyUp"> & {
|
||||
/**
|
||||
* @generated from field: int32 key = 1;
|
||||
* Fixed value "MouseKeyUp"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 key = 2;
|
||||
*/
|
||||
key: number;
|
||||
};
|
||||
@@ -129,7 +164,14 @@ export const ProtoMouseKeyUpSchema: GenMessage<ProtoMouseKeyUp> = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoKeyDown = Message<"proto.ProtoKeyDown"> & {
|
||||
/**
|
||||
* @generated from field: int32 key = 1;
|
||||
* Fixed value "KeyDown"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 key = 2;
|
||||
*/
|
||||
key: number;
|
||||
};
|
||||
@@ -148,7 +190,14 @@ export const ProtoKeyDownSchema: GenMessage<ProtoKeyDown> = /*@__PURE__*/
|
||||
*/
|
||||
export type ProtoKeyUp = Message<"proto.ProtoKeyUp"> & {
|
||||
/**
|
||||
* @generated from field: int32 key = 1;
|
||||
* Fixed value "KeyUp"
|
||||
*
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 key = 2;
|
||||
*/
|
||||
key: number;
|
||||
};
|
||||
@@ -166,26 +215,26 @@ export const ProtoKeyUpSchema: GenMessage<ProtoKeyUp> = /*@__PURE__*/
|
||||
* @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 = 1;
|
||||
* @generated from field: string id = 2;
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Session specific slot number (0-3)
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 session_slot = 2;
|
||||
* @generated from field: int32 slot = 3;
|
||||
*/
|
||||
sessionSlot: number;
|
||||
|
||||
/**
|
||||
* Session ID of the client
|
||||
*
|
||||
* @generated from field: string session_id = 3;
|
||||
*/
|
||||
sessionId: string;
|
||||
slot: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -202,18 +251,18 @@ export const ProtoControllerAttachSchema: GenMessage<ProtoControllerAttach> = /*
|
||||
*/
|
||||
export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & {
|
||||
/**
|
||||
* Session specific slot number (0-3)
|
||||
* Fixed value "ControllerDetach"
|
||||
*
|
||||
* @generated from field: int32 session_slot = 1;
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
sessionSlot: number;
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Session ID of the client
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: string session_id = 2;
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
sessionId: string;
|
||||
slot: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -223,6 +272,181 @@ export type ProtoControllerDetach = Message<"proto.ProtoControllerDetach"> & {
|
||||
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
|
||||
*
|
||||
@@ -230,18 +454,18 @@ export const ProtoControllerDetachSchema: GenMessage<ProtoControllerDetach> = /*
|
||||
*/
|
||||
export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & {
|
||||
/**
|
||||
* Session specific slot number (0-3)
|
||||
* Fixed value "ControllerRumble"
|
||||
*
|
||||
* @generated from field: int32 session_slot = 1;
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
sessionSlot: number;
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Session ID of the client
|
||||
* Slot number (0-3)
|
||||
*
|
||||
* @generated from field: string session_id = 2;
|
||||
* @generated from field: int32 slot = 2;
|
||||
*/
|
||||
sessionId: string;
|
||||
slot: number;
|
||||
|
||||
/**
|
||||
* Low frequency rumble (0-65535)
|
||||
@@ -270,321 +494,108 @@ export type ProtoControllerRumble = Message<"proto.ProtoControllerRumble"> & {
|
||||
* Use `create(ProtoControllerRumbleSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerRumbleSchema: GenMessage<ProtoControllerRumble> = /*@__PURE__*/
|
||||
messageDesc(file_types, 9);
|
||||
|
||||
/**
|
||||
* ControllerStateBatch - single message containing full or partial controller state
|
||||
*
|
||||
* @generated from message proto.ProtoControllerStateBatch
|
||||
*/
|
||||
export type ProtoControllerStateBatch = Message<"proto.ProtoControllerStateBatch"> & {
|
||||
/**
|
||||
* Session specific slot number (0-3)
|
||||
*
|
||||
* @generated from field: int32 session_slot = 1;
|
||||
*/
|
||||
sessionSlot: number;
|
||||
|
||||
/**
|
||||
* Session ID of the client
|
||||
*
|
||||
* @generated from field: string session_id = 2;
|
||||
*/
|
||||
sessionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerStateBatch.UpdateType update_type = 3;
|
||||
*/
|
||||
updateType: ProtoControllerStateBatch_UpdateType;
|
||||
|
||||
/**
|
||||
* Sequence number for packet loss detection
|
||||
*
|
||||
* @generated from field: uint32 sequence = 4;
|
||||
*/
|
||||
sequence: number;
|
||||
|
||||
/**
|
||||
* Button state map (Linux event codes)
|
||||
*
|
||||
* @generated from field: map<int32, bool> button_changed_mask = 5;
|
||||
*/
|
||||
buttonChangedMask: { [key: number]: boolean };
|
||||
|
||||
/**
|
||||
* Analog inputs
|
||||
*
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 left_stick_x = 6;
|
||||
*/
|
||||
leftStickX?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 left_stick_y = 7;
|
||||
*/
|
||||
leftStickY?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 right_stick_x = 8;
|
||||
*/
|
||||
rightStickX?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 right_stick_y = 9;
|
||||
*/
|
||||
rightStickY?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 left_trigger = 10;
|
||||
*/
|
||||
leftTrigger?: number;
|
||||
|
||||
/**
|
||||
* -32768 to 32767
|
||||
*
|
||||
* @generated from field: optional int32 right_trigger = 11;
|
||||
*/
|
||||
rightTrigger?: number;
|
||||
|
||||
/**
|
||||
* -1, 0, or 1
|
||||
*
|
||||
* @generated from field: optional int32 dpad_x = 12;
|
||||
*/
|
||||
dpadX?: number;
|
||||
|
||||
/**
|
||||
* -1, 0, or 1
|
||||
*
|
||||
* @generated from field: optional int32 dpad_y = 13;
|
||||
*/
|
||||
dpadY?: number;
|
||||
|
||||
/**
|
||||
* Bitmask indicating which fields have changed
|
||||
* Bit 0: button_changed_mask, Bit 1: left_stick_x, Bit 2: left_stick_y, etc.
|
||||
*
|
||||
* @generated from field: optional uint32 changed_fields = 14;
|
||||
*/
|
||||
changedFields?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoControllerStateBatch.
|
||||
* Use `create(ProtoControllerStateBatchSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoControllerStateBatchSchema: GenMessage<ProtoControllerStateBatch> = /*@__PURE__*/
|
||||
messageDesc(file_types, 10);
|
||||
|
||||
/**
|
||||
* @generated from enum proto.ProtoControllerStateBatch.UpdateType
|
||||
*/
|
||||
export enum ProtoControllerStateBatch_UpdateType {
|
||||
/**
|
||||
* Complete controller state
|
||||
*
|
||||
* @generated from enum value: FULL_STATE = 0;
|
||||
*/
|
||||
FULL_STATE = 0,
|
||||
|
||||
/**
|
||||
* Only changed fields
|
||||
*
|
||||
* @generated from enum value: DELTA = 1;
|
||||
*/
|
||||
DELTA = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum proto.ProtoControllerStateBatch.UpdateType.
|
||||
*/
|
||||
export const ProtoControllerStateBatch_UpdateTypeSchema: GenEnum<ProtoControllerStateBatch_UpdateType> = /*@__PURE__*/
|
||||
enumDesc(file_types, 10, 0);
|
||||
|
||||
/**
|
||||
* @generated from message proto.RTCIceCandidateInit
|
||||
*/
|
||||
export type RTCIceCandidateInit = Message<"proto.RTCIceCandidateInit"> & {
|
||||
/**
|
||||
* @generated from field: string candidate = 1;
|
||||
*/
|
||||
candidate: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional uint32 sdpMLineIndex = 2;
|
||||
*/
|
||||
sdpMLineIndex?: number;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string sdpMid = 3;
|
||||
*/
|
||||
sdpMid?: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string usernameFragment = 4;
|
||||
*/
|
||||
usernameFragment?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.RTCIceCandidateInit.
|
||||
* Use `create(RTCIceCandidateInitSchema)` to create a new message.
|
||||
*/
|
||||
export const RTCIceCandidateInitSchema: GenMessage<RTCIceCandidateInit> = /*@__PURE__*/
|
||||
messageDesc(file_types, 11);
|
||||
|
||||
/**
|
||||
* @generated from message proto.RTCSessionDescriptionInit
|
||||
*/
|
||||
export type RTCSessionDescriptionInit = Message<"proto.RTCSessionDescriptionInit"> & {
|
||||
/**
|
||||
* @generated from field: string sdp = 1;
|
||||
*/
|
||||
sdp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string type = 2;
|
||||
*/
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.RTCSessionDescriptionInit.
|
||||
* Use `create(RTCSessionDescriptionInitSchema)` to create a new message.
|
||||
*/
|
||||
export const RTCSessionDescriptionInitSchema: GenMessage<RTCSessionDescriptionInit> = /*@__PURE__*/
|
||||
messageDesc(file_types, 12);
|
||||
|
||||
/**
|
||||
* ProtoICE message
|
||||
*
|
||||
* @generated from message proto.ProtoICE
|
||||
*/
|
||||
export type ProtoICE = Message<"proto.ProtoICE"> & {
|
||||
/**
|
||||
* @generated from field: proto.RTCIceCandidateInit candidate = 1;
|
||||
*/
|
||||
candidate?: RTCIceCandidateInit;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoICE.
|
||||
* Use `create(ProtoICESchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoICESchema: GenMessage<ProtoICE> = /*@__PURE__*/
|
||||
messageDesc(file_types, 13);
|
||||
|
||||
/**
|
||||
* ProtoSDP message
|
||||
* Union of all Input types
|
||||
*
|
||||
* @generated from message proto.ProtoSDP
|
||||
* @generated from message proto.ProtoInput
|
||||
*/
|
||||
export type ProtoSDP = Message<"proto.ProtoSDP"> & {
|
||||
export type ProtoInput = Message<"proto.ProtoInput"> & {
|
||||
/**
|
||||
* @generated from field: proto.RTCSessionDescriptionInit sdp = 1;
|
||||
* @generated from oneof proto.ProtoInput.input_type
|
||||
*/
|
||||
sdp?: RTCSessionDescriptionInit;
|
||||
inputType: {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseMove mouse_move = 1;
|
||||
*/
|
||||
value: ProtoMouseMove;
|
||||
case: "mouseMove";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseMoveAbs mouse_move_abs = 2;
|
||||
*/
|
||||
value: ProtoMouseMoveAbs;
|
||||
case: "mouseMoveAbs";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseWheel mouse_wheel = 3;
|
||||
*/
|
||||
value: ProtoMouseWheel;
|
||||
case: "mouseWheel";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseKeyDown mouse_key_down = 4;
|
||||
*/
|
||||
value: ProtoMouseKeyDown;
|
||||
case: "mouseKeyDown";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoMouseKeyUp mouse_key_up = 5;
|
||||
*/
|
||||
value: ProtoMouseKeyUp;
|
||||
case: "mouseKeyUp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoKeyDown key_down = 6;
|
||||
*/
|
||||
value: ProtoKeyDown;
|
||||
case: "keyDown";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoKeyUp key_up = 7;
|
||||
*/
|
||||
value: ProtoKeyUp;
|
||||
case: "keyUp";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerAttach controller_attach = 8;
|
||||
*/
|
||||
value: ProtoControllerAttach;
|
||||
case: "controllerAttach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerDetach controller_detach = 9;
|
||||
*/
|
||||
value: ProtoControllerDetach;
|
||||
case: "controllerDetach";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerButton controller_button = 10;
|
||||
*/
|
||||
value: ProtoControllerButton;
|
||||
case: "controllerButton";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerTrigger controller_trigger = 11;
|
||||
*/
|
||||
value: ProtoControllerTrigger;
|
||||
case: "controllerTrigger";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerStick controller_stick = 12;
|
||||
*/
|
||||
value: ProtoControllerStick;
|
||||
case: "controllerStick";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerAxis controller_axis = 13;
|
||||
*/
|
||||
value: ProtoControllerAxis;
|
||||
case: "controllerAxis";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: proto.ProtoControllerRumble controller_rumble = 14;
|
||||
*/
|
||||
value: ProtoControllerRumble;
|
||||
case: "controllerRumble";
|
||||
} | { case: undefined; value?: undefined };
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoSDP.
|
||||
* Use `create(ProtoSDPSchema)` to create a new message.
|
||||
* Describes the message proto.ProtoInput.
|
||||
* Use `create(ProtoInputSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoSDPSchema: GenMessage<ProtoSDP> = /*@__PURE__*/
|
||||
export const ProtoInputSchema: GenMessage<ProtoInput> = /*@__PURE__*/
|
||||
messageDesc(file_types, 14);
|
||||
|
||||
/**
|
||||
* ProtoRaw message
|
||||
*
|
||||
* @generated from message proto.ProtoRaw
|
||||
*/
|
||||
export type ProtoRaw = Message<"proto.ProtoRaw"> & {
|
||||
/**
|
||||
* @generated from field: string data = 1;
|
||||
*/
|
||||
data: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoRaw.
|
||||
* Use `create(ProtoRawSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoRawSchema: GenMessage<ProtoRaw> = /*@__PURE__*/
|
||||
messageDesc(file_types, 15);
|
||||
|
||||
/**
|
||||
* ProtoClientRequestRoomStream message
|
||||
*
|
||||
* @generated from message proto.ProtoClientRequestRoomStream
|
||||
*/
|
||||
export type ProtoClientRequestRoomStream = Message<"proto.ProtoClientRequestRoomStream"> & {
|
||||
/**
|
||||
* @generated from field: string room_name = 1;
|
||||
*/
|
||||
roomName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string session_id = 2;
|
||||
*/
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoClientRequestRoomStream.
|
||||
* Use `create(ProtoClientRequestRoomStreamSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoClientRequestRoomStreamSchema: GenMessage<ProtoClientRequestRoomStream> = /*@__PURE__*/
|
||||
messageDesc(file_types, 16);
|
||||
|
||||
/**
|
||||
* ProtoClientDisconnected message
|
||||
*
|
||||
* @generated from message proto.ProtoClientDisconnected
|
||||
*/
|
||||
export type ProtoClientDisconnected = Message<"proto.ProtoClientDisconnected"> & {
|
||||
/**
|
||||
* @generated from field: string session_id = 1;
|
||||
*/
|
||||
sessionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated int32 controller_slots = 2;
|
||||
*/
|
||||
controllerSlots: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoClientDisconnected.
|
||||
* Use `create(ProtoClientDisconnectedSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoClientDisconnectedSchema: GenMessage<ProtoClientDisconnected> = /*@__PURE__*/
|
||||
messageDesc(file_types, 17);
|
||||
|
||||
/**
|
||||
* ProtoServerPushStream message
|
||||
*
|
||||
* @generated from message proto.ProtoServerPushStream
|
||||
*/
|
||||
export type ProtoServerPushStream = Message<"proto.ProtoServerPushStream"> & {
|
||||
/**
|
||||
* @generated from field: string room_name = 1;
|
||||
*/
|
||||
roomName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message proto.ProtoServerPushStream.
|
||||
* Use `create(ProtoServerPushStreamSchema)` to create a new message.
|
||||
*/
|
||||
export const ProtoServerPushStreamSchema: GenMessage<ProtoServerPushStream> = /*@__PURE__*/
|
||||
messageDesc(file_types, 18);
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { pbStream, type ProtobufStream } from "@libp2p/utils";
|
||||
import type { Stream } from "@libp2p/interface";
|
||||
import { bufbuildAdapter } from "./utils";
|
||||
import {
|
||||
ProtoMessage,
|
||||
ProtoMessageSchema,
|
||||
ProtoMessageBase,
|
||||
} from "./proto/messages_pb";
|
||||
|
||||
type MessageHandler = (
|
||||
data: any,
|
||||
base: ProtoMessageBase,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export class P2PMessageStream {
|
||||
private pb: ProtobufStream;
|
||||
private handlers = new Map<string, MessageHandler[]>();
|
||||
private closed = false;
|
||||
private readLoopRunning = false;
|
||||
|
||||
constructor(stream: Stream) {
|
||||
this.pb = pbStream(stream);
|
||||
}
|
||||
|
||||
public on(payloadType: string, handler: MessageHandler): void {
|
||||
if (!this.handlers.has(payloadType)) {
|
||||
this.handlers.set(payloadType, []);
|
||||
}
|
||||
this.handlers.get(payloadType)!.push(handler);
|
||||
|
||||
if (!this.readLoopRunning) this.startReading().catch(console.error);
|
||||
}
|
||||
|
||||
private async startReading(): Promise<void> {
|
||||
if (this.readLoopRunning || this.closed) return;
|
||||
this.readLoopRunning = true;
|
||||
|
||||
while (!this.closed) {
|
||||
try {
|
||||
const msg: ProtoMessage = await this.pb.read(
|
||||
bufbuildAdapter(ProtoMessageSchema),
|
||||
);
|
||||
|
||||
const payloadType = msg.messageBase?.payloadType;
|
||||
if (payloadType && this.handlers.has(payloadType)) {
|
||||
const handlers = this.handlers.get(payloadType)!;
|
||||
if (msg.payload.value) {
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
await handler(msg.payload.value, msg.messageBase);
|
||||
} catch (err) {
|
||||
console.error(`Error in handler for ${payloadType}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.closed) break;
|
||||
console.error("Stream read error:", err);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
this.readLoopRunning = false;
|
||||
}
|
||||
|
||||
public async write(
|
||||
message: ProtoMessage,
|
||||
options?: { signal?: AbortSignal },
|
||||
): Promise<void> {
|
||||
if (this.closed)
|
||||
throw new Error("Cannot write to closed stream");
|
||||
|
||||
await this.pb.write(message, bufbuildAdapter(ProtoMessageSchema), options);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.closed = true;
|
||||
this.handlers.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { create, toBinary, fromBinary } from "@bufbuild/protobuf";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
import { Uint8ArrayList } from "uint8arraylist";
|
||||
import type { GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
||||
import {
|
||||
ProtoLatencyTracker,
|
||||
ProtoLatencyTrackerSchema,
|
||||
ProtoTimestampEntrySchema,
|
||||
} from "./proto/latency_tracker_pb";
|
||||
import {
|
||||
ProtoMessage,
|
||||
ProtoMessageSchema,
|
||||
ProtoMessageBaseSchema,
|
||||
} from "./proto/messages_pb";
|
||||
|
||||
export function bufbuildAdapter<T extends Message>(schema: GenMessage<T>) {
|
||||
return {
|
||||
encode: (data: T): Uint8Array => {
|
||||
return toBinary(schema, data);
|
||||
},
|
||||
decode: (data: Uint8Array | Uint8ArrayList): T => {
|
||||
// Convert Uint8ArrayList to Uint8Array if needed
|
||||
const bytes = data instanceof Uint8ArrayList ? data.subarray() : data;
|
||||
return fromBinary(schema, bytes);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Latency tracker helpers
|
||||
export function createLatencyTracker(sequenceId?: string): ProtoLatencyTracker {
|
||||
return create(ProtoLatencyTrackerSchema, {
|
||||
sequenceId: sequenceId || crypto.randomUUID(),
|
||||
timestamps: [],
|
||||
});
|
||||
}
|
||||
|
||||
export function addLatencyTimestamp(
|
||||
tracker: ProtoLatencyTracker,
|
||||
stage: string,
|
||||
): ProtoLatencyTracker {
|
||||
const entry = create(ProtoTimestampEntrySchema, {
|
||||
stage,
|
||||
time: timestampFromDate(new Date()),
|
||||
});
|
||||
|
||||
return {
|
||||
...tracker,
|
||||
timestamps: [...tracker.timestamps, entry],
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateMessageOptions {
|
||||
sequenceId?: string;
|
||||
}
|
||||
|
||||
function derivePayloadCase(data: Message): string {
|
||||
// Extract case from $typeName: "proto.ProtoICE" -> "ice"
|
||||
// "proto.ProtoControllerAttach" -> "controllerAttach"
|
||||
const typeName = data.$typeName;
|
||||
if (!typeName)
|
||||
throw new Error("Message has no $typeName");
|
||||
|
||||
// Remove "proto.Proto" prefix and convert first char to lowercase
|
||||
const caseName = typeName.replace(/^proto\.Proto/, "");
|
||||
|
||||
// Convert PascalCase to camelCase
|
||||
// If it's all caps (like SDP, ICE), lowercase everything
|
||||
// Otherwise, just lowercase the first character
|
||||
if (caseName === caseName.toUpperCase()) {
|
||||
return caseName.toLowerCase();
|
||||
}
|
||||
return caseName.charAt(0).toLowerCase() + caseName.slice(1);
|
||||
}
|
||||
|
||||
export function createMessage(
|
||||
data: Message,
|
||||
payloadType: string,
|
||||
options?: CreateMessageOptions,
|
||||
): ProtoMessage {
|
||||
const payloadCase = derivePayloadCase(data);
|
||||
|
||||
return create(ProtoMessageSchema, {
|
||||
messageBase: create(ProtoMessageBaseSchema, {
|
||||
payloadType,
|
||||
latency: options?.sequenceId
|
||||
? createLatencyTracker(options.sequenceId)
|
||||
: undefined,
|
||||
}),
|
||||
payload: {
|
||||
case: payloadCase,
|
||||
value: data,
|
||||
} as any, // Type assertion needed for dynamic case
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
NewMessageRaw,
|
||||
NewMessageSDP,
|
||||
NewMessageICE,
|
||||
SafeStream,
|
||||
} from "./messages";
|
||||
import { webSockets } from "@libp2p/websockets";
|
||||
import { webTransport } from "@libp2p/webtransport";
|
||||
import { createLibp2p, Libp2p } from "libp2p";
|
||||
@@ -7,33 +13,19 @@ import { identify } from "@libp2p/identify";
|
||||
import { multiaddr } from "@multiformats/multiaddr";
|
||||
import { Connection } from "@libp2p/interface";
|
||||
import { ping } from "@libp2p/ping";
|
||||
import { createMessage } from "./utils";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import {
|
||||
ProtoClientRequestRoomStream,
|
||||
ProtoClientRequestRoomStreamSchema,
|
||||
ProtoICE,
|
||||
ProtoICESchema, ProtoRaw,
|
||||
ProtoSDP,
|
||||
ProtoSDPSchema
|
||||
} from "./proto/types_pb";
|
||||
import { P2PMessageStream } from "./streamwrapper";
|
||||
|
||||
const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0";
|
||||
|
||||
export class WebRTCStream {
|
||||
private _sessionId: string | null = null;
|
||||
private _p2p: Libp2p | undefined = undefined;
|
||||
private _p2pConn: Connection | undefined = undefined;
|
||||
private _msgStream: P2PMessageStream | undefined = undefined;
|
||||
private _p2pSafeStream: SafeStream | undefined = undefined;
|
||||
private _pc: RTCPeerConnection | undefined = undefined;
|
||||
private _audioTrack: MediaStreamTrack | undefined = undefined;
|
||||
private _videoTrack: MediaStreamTrack | undefined = undefined;
|
||||
private _dataChannel: RTCDataChannel | undefined = undefined;
|
||||
private _onConnected: ((stream: MediaStream | null) => void) | undefined =
|
||||
undefined;
|
||||
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined =
|
||||
undefined;
|
||||
private _onConnected: ((stream: MediaStream | null) => void) | undefined = undefined;
|
||||
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined;
|
||||
private _serverURL: string | undefined = undefined;
|
||||
private _roomName: string | undefined = undefined;
|
||||
private _isConnected: boolean = false;
|
||||
@@ -97,20 +89,14 @@ export class WebRTCStream {
|
||||
.newStream(NESTRI_PROTOCOL_STREAM_REQUEST)
|
||||
.catch(console.error);
|
||||
if (stream) {
|
||||
this._msgStream = new P2PMessageStream(stream);
|
||||
this._p2pSafeStream = new SafeStream(stream);
|
||||
console.log("Stream opened with peer");
|
||||
|
||||
let iceHolder: RTCIceCandidateInit[] = [];
|
||||
this._msgStream.on("ice-candidate", (data: ProtoICE) => {
|
||||
const cand: RTCIceCandidateInit = {
|
||||
candidate: data.candidate.candidate,
|
||||
sdpMLineIndex: data.candidate.sdpMLineIndex,
|
||||
sdpMid: data.candidate.sdpMid,
|
||||
usernameFragment: data.candidate.usernameFragment,
|
||||
};
|
||||
this._p2pSafeStream.registerCallback("ice-candidate", (data) => {
|
||||
if (this._pc) {
|
||||
if (this._pc.remoteDescription) {
|
||||
this._pc.addIceCandidate(cand).catch((err) => {
|
||||
this._pc.addIceCandidate(data.candidate).catch((err) => {
|
||||
console.error("Error adding ICE candidate:", err);
|
||||
});
|
||||
// Add held candidates
|
||||
@@ -121,78 +107,45 @@ export class WebRTCStream {
|
||||
});
|
||||
iceHolder = [];
|
||||
} else {
|
||||
iceHolder.push(cand);
|
||||
iceHolder.push(data.candidate);
|
||||
}
|
||||
} else {
|
||||
iceHolder.push(data.candidate);
|
||||
}
|
||||
});
|
||||
|
||||
this._msgStream.on("session-assigned", (data: ProtoClientRequestRoomStream) => {
|
||||
this._sessionId = data.sessionId;
|
||||
localStorage.setItem("nestri-session-id", this._sessionId);
|
||||
console.log("Session ID assigned:", this._sessionId, "for room:", data.roomName);
|
||||
});
|
||||
|
||||
this._msgStream.on("offer", async (data: ProtoSDP) => {
|
||||
this._p2pSafeStream.registerCallback("offer", async (data) => {
|
||||
if (!this._pc) {
|
||||
// Setup peer connection now
|
||||
this._setupPeerConnection();
|
||||
}
|
||||
await this._pc!.setRemoteDescription({
|
||||
sdp: data.sdp.sdp,
|
||||
type: data.sdp.type as RTCSdpType,
|
||||
});
|
||||
// Add held candidates
|
||||
iceHolder.forEach((candidate) => {
|
||||
this._pc!.addIceCandidate(candidate).catch((err) => {
|
||||
console.error("Error adding held ICE candidate:", err);
|
||||
});
|
||||
});
|
||||
iceHolder = [];
|
||||
|
||||
await this._pc!.setRemoteDescription(data.sdp);
|
||||
// Create our answer
|
||||
const answer = await this._pc!.createAnswer();
|
||||
// Force stereo in Chromium browsers
|
||||
answer.sdp = this.forceOpusStereo(answer.sdp!);
|
||||
await this._pc!.setLocalDescription(answer);
|
||||
// Send answer back
|
||||
const answerMsg = createMessage(
|
||||
create(ProtoSDPSchema, {
|
||||
sdp: answer,
|
||||
}),
|
||||
"answer",
|
||||
);
|
||||
await this._msgStream?.write(answerMsg);
|
||||
const answerMsg = NewMessageSDP("answer", answer);
|
||||
await this._p2pSafeStream?.writeMessage(answerMsg);
|
||||
});
|
||||
|
||||
this._msgStream.on("request-stream-offline", (msg: ProtoRaw) => {
|
||||
console.warn("Stream is offline for room:", msg.data);
|
||||
this._p2pSafeStream.registerCallback("request-stream-offline", (data) => {
|
||||
console.warn("Stream is offline for room:", data.roomName);
|
||||
this._onConnected?.(null);
|
||||
});
|
||||
|
||||
const clientId = this.getSessionID();
|
||||
if (clientId) {
|
||||
console.debug("Using existing session ID:", clientId);
|
||||
}
|
||||
|
||||
// Send stream request
|
||||
const requestMsg = createMessage(
|
||||
create(ProtoClientRequestRoomStreamSchema, {
|
||||
roomName: roomName,
|
||||
sessionId: clientId ?? "",
|
||||
}),
|
||||
// marshal room name into json
|
||||
const request = NewMessageRaw(
|
||||
"request-stream-room",
|
||||
roomName,
|
||||
);
|
||||
await this._msgStream.write(requestMsg);
|
||||
await this._p2pSafeStream.writeMessage(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getSessionID(): string | null {
|
||||
if (this._sessionId === null)
|
||||
this._sessionId = localStorage.getItem("nestri-session-id");
|
||||
return this._sessionId;
|
||||
}
|
||||
|
||||
// Forces opus to stereo in Chromium browsers, because of course
|
||||
private forceOpusStereo(SDP: string): string {
|
||||
// Look for "minptime=10;useinbandfec=1" and replace with "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1;"
|
||||
@@ -247,16 +200,11 @@ export class WebRTCStream {
|
||||
|
||||
this._pc.onicecandidate = (e) => {
|
||||
if (e.candidate) {
|
||||
const iceMsg = createMessage(
|
||||
create(ProtoICESchema, {
|
||||
candidate: e.candidate,
|
||||
}),
|
||||
"ice-candidate",
|
||||
);
|
||||
if (this._msgStream) {
|
||||
this._msgStream
|
||||
.write(iceMsg)
|
||||
.catch((err) => console.error("Error sending ICE candidate:", err));
|
||||
const iceMsg = NewMessageICE("ice-candidate", e.candidate);
|
||||
if (this._p2pSafeStream) {
|
||||
this._p2pSafeStream.writeMessage(iceMsg).catch((err) =>
|
||||
console.error("Error sending ICE candidate:", err),
|
||||
);
|
||||
} else {
|
||||
console.warn("P2P stream not established, cannot send ICE candidate");
|
||||
}
|
||||
@@ -270,7 +218,8 @@ export class WebRTCStream {
|
||||
}
|
||||
|
||||
private _checkConnectionState() {
|
||||
if (!this._pc || !this._p2p || !this._p2pConn) return;
|
||||
if (!this._pc || !this._p2p || !this._p2pConn)
|
||||
return;
|
||||
|
||||
console.debug("Checking connection state:", {
|
||||
connectionState: this._pc.connectionState,
|
||||
@@ -307,7 +256,7 @@ export class WebRTCStream {
|
||||
// @ts-ignore
|
||||
receiver.jitterBufferTarget = receiver.jitterBufferDelayHint = receiver.playoutDelayHint = 0;
|
||||
}
|
||||
}, 50);
|
||||
}, 15);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -337,9 +286,7 @@ export class WebRTCStream {
|
||||
|
||||
// Attempt to reconnect only if not already connected
|
||||
if (!this._isConnected && this._serverURL && this._roomName) {
|
||||
this._setup(this._serverURL, this._roomName).catch((err) =>
|
||||
console.error("Reconnection failed:", err),
|
||||
);
|
||||
this._setup(this._serverURL, this._roomName).catch((err) => console.error("Reconnection failed:", err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,9 +335,7 @@ export class WebRTCStream {
|
||||
}
|
||||
|
||||
public removeDataChannelCallback(callback: (data: any) => void) {
|
||||
this._dataChannelCallbacks = this._dataChannelCallbacks.filter(
|
||||
(cb) => cb !== callback,
|
||||
);
|
||||
this._dataChannelCallbacks = this._dataChannelCallbacks.filter(cb => cb !== callback);
|
||||
}
|
||||
|
||||
private _setupDataChannelEvents() {
|
||||
@@ -398,7 +343,7 @@ export class WebRTCStream {
|
||||
|
||||
this._dataChannel.onclose = () => console.log("sendChannel has closed");
|
||||
this._dataChannel.onopen = () => console.log("sendChannel has opened");
|
||||
this._dataChannel.onmessage = (event) => {
|
||||
this._dataChannel.onmessage = (event => {
|
||||
// Parse as ProtoBuf message
|
||||
const data = event.data;
|
||||
// Call registered callback if exists
|
||||
@@ -409,7 +354,7 @@ export class WebRTCStream {
|
||||
console.error("Error in data channel callback:", err);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _gatherFrameRate() {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
> Why do I have a folder named ".expo" in my project?
|
||||
|
||||
The ".expo" folder is created when an Expo project is started using "expo start" command.
|
||||
|
||||
> What do the files contain?
|
||||
|
||||
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
|
||||
- "settings.json": contains the server configuration that is used to serve the application manifest.
|
||||
|
||||
> Should I commit the ".expo" folder?
|
||||
|
||||
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
|
||||
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"devices": []
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
101
packages/play-standalone/android/.gitignore
vendored
@@ -1,101 +0,0 @@
|
||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-android-plugins
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
|
||||
# Generated Config files
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
@@ -1,2 +0,0 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
@@ -1,54 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace "com.nestri.play"
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.nestri.play"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
try {
|
||||
def servicesJSON = file('google-services.json')
|
||||
if (servicesJSON.text) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-screen-orientation')
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (hasProperty('postBuildExtras')) {
|
||||
postBuildExtras()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.nestri.play;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 13 KiB |