Compare commits
3 Commits
feat/cf
...
feat/image
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fd5608e6e | ||
|
|
a47dc91b22 | ||
|
|
0124af1b70 |
@@ -1,4 +1,3 @@
|
||||
**/target/
|
||||
**/.git
|
||||
**/.env
|
||||
**/.idea
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
CLOUDFLARE_API_TOKEN=
|
||||
NEON_API_KEY=
|
||||
136
README.md
@@ -1,23 +1,115 @@
|
||||
<p align="center">
|
||||
<a href="https://nestri.io">
|
||||
<picture>
|
||||
<source srcset="packages/web/public/logo.white.svg" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="packages/web/public/logo.black.svg" media="(prefers-color-scheme: light)">
|
||||
<img src="packages/web/public/logo.black.svg" alt="Nestri logo">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">Deploy and stream games/apps in the cloud. Use our GPUs or bring your own.</p>
|
||||
<p align="center">
|
||||
<a href="https://discord.com/invite/Y6etn3qKZ3"><img alt="Discord" src="https://img.shields.io/discord/1080111004698021909?style=flat-square&label=discord" /></a>
|
||||
<a href="https://github.com/nestrilabs/nestri/blob/main/LICENSE"><img alt="Nestri License" src="https://img.shields.io/github/license/nestriness/nestri?style=flat-square" /></a>
|
||||
<a href="https://github.com/nestrilabs/nestri/actions/workflows/runner.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/nestrilabs/nestri/runner.yml?style=flat-square&branch=main" /></a>
|
||||
<!-- <a href="https://nestri.io" style="text-decoration: none;">
|
||||
<img src="https://img.shields.io/badge/Start%20Playing%20Now-For%20$1/hour-brightgreen?style=flat-square" alt="Umami Demo" />
|
||||
</a> -->
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
<!-- TODO: Add a link to the demo app when it's ready -->
|
||||
<!-- TODO: Add a link to install for self-hosters -->
|
||||
<!-- TODO: Add a CTA for hosted option -->
|
||||
<!-- TODO: Add feature imagery like Lobechat -->
|
||||
<div align="center">
|
||||
<h1>
|
||||
|
||||
<a href="https://nestri.io" >
|
||||
<img src="/apps/www/public/seo/banner.png" alt="Nestri - What will you play next?">
|
||||
</a>
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
Nestri is an open-source, self-hosted Geforce Now alternative with Stadia's social features. <strong>Built and shaped by our gaming community.</strong>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][discord-shield]][discord-link]
|
||||
[![][github-license-shield]][github-license-link]
|
||||
[![][github-stars-shield]][github-stars-link]
|
||||
|
||||
**Share the Nestri Repository on Social Media**
|
||||
|
||||
[![][share-x-shield]][share-x-link]
|
||||
[![][share-reddit-shield]][share-reddit-link]
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
> **Note**
|
||||
> Nestri is more closer (in feature comparison) to Jellyfin/Plex than Moonlight. Our goal is to develop a comprehensive self-hosted cloud gaming solution for your home server.
|
||||
|
||||
## Features
|
||||
|
||||
- Save and share your game progress easily with friends
|
||||
- Simultaneously run multiple games on your GPU using Virtio-GPU Venus and/or Virgl
|
||||
- Play games using either your integrated GPU or dedicated GPU
|
||||
- Enjoy titles from your preferred Game Stores - Steam, Epic Games, Amazon Games, GOG.com
|
||||
- Experience Android gaming
|
||||
- Organize gaming sessions with friends and family through Nestri Parties
|
||||
- Stream directly to YouTube and Twitch straight from your setup
|
||||
- Family sharing capabilities
|
||||
- Support for Controller, Touchscreen, Keyboard, and Mouse devices
|
||||
|
||||
## Possible Use Cases
|
||||
|
||||
- Organize game nights or LAN parties with friends online or locally
|
||||
- For game developers, showcase your proof-of-concept multiplayer games for testing without installation
|
||||
- Create and manage your custom cloud-gaming platform using our robust API
|
||||
- Establish a game server for your family to enjoy gaming on the go
|
||||
|
||||
## Goals
|
||||
|
||||
- Provide a user-friendly setup - fire and forget
|
||||
- Deliver a simple and elegant interface for managing and playing your game library
|
||||
- Ensure a high-quality gaming experience out-of-the-box
|
||||
- Optimize for the best gaming performance right from the start
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Become a generic cloud-gaming service
|
||||
|
||||
## Built With
|
||||
|
||||
- Cloudflare Workers
|
||||
- Cloudflare Pages
|
||||
- Supabase
|
||||
- CrosVM (with Virtio-GPU Venus and Virgl support)
|
||||
- Docker
|
||||
- Qwik
|
||||
- Media-Over-Quic
|
||||
- AWS Route53
|
||||
|
||||
## Known Issues
|
||||
|
||||
- CrosVM is still under development and needs to be merged
|
||||
- Currently, the Intel dGPU, particularly the Arc A780, is the only tested and verified GPU
|
||||
|
||||
## Donation
|
||||
|
||||
If you appreciate our work and wish to support the development of Nestri, consider making a donation [here](https://polar.sh/nestri/donate). Your contributions will help us improve the platform and enhance your gaming experience. Thank you for your support!
|
||||
|
||||
## Demo
|
||||
|
||||
Nestri is still in development, but here is some footage from Behind-The-Scenes
|
||||
|
||||
<img src="/apps/www/public/seo/code.avif" alt="Nestri - What will you play next?">
|
||||
|
||||
|
||||
[github-release-link]: https://github.com/nestriness/nestri/releases
|
||||
[github-release-shield]: https://img.shields.io/github/v/release/nestriness/nestri?color=369eff&labelColor=black&logo=github&style=flat-square
|
||||
[discord-shield]: https://img.shields.io/discord/1080111004698021909?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
|
||||
[discord-link]: https://discord.com/invite/Y6etn3qKZ3
|
||||
[github-license-shield]: https://img.shields.io/github/license/nestriness/nestri?color=white&labelColor=black&style=flat-square
|
||||
[github-license-link]: https://github.com/nestriness/nestri/blob/main/LICENSE
|
||||
[github-stars-shield]: https://img.shields.io/github/stars/nestriness/nestri?color=ffcb47&labelColor=black&style=flat-square
|
||||
[github-stars-link]: https://github.com/nestriness/nestri/network/stargazers
|
||||
[share-x-shield]: https://img.shields.io/badge/-share%20on%20x-black?labelColor=black&logo=x&logoColor=white&style=flat-square
|
||||
[share-x-link]: https://twitter.com/intent/tweet?text=Hey%2C%20check%20out%20this%20Github%20repository.%20It%20is%20an%20open-source%20self-hosted%20Geforce%20Now%20alternative.&url=https%3A%2F%2Fgithub.com%2Fnestriness%2Fnestri
|
||||
[share-reddit-shield]: https://img.shields.io/badge/-share%20on%20reddit-black?labelColor=black&logo=reddit&logoColor=white&style=flat-square
|
||||
[share-reddit-link]: https://www.reddit.com/submit?title=Hey%2C%20check%20out%20this%20Github%20repository.%20It%20is%20an%20open-source%20self-hosted%20Geforce%20Now%20alternative.&url=https%3A%2F%2Fgithub.com%2Fnestriness%2Fnestri
|
||||
[image-overview]: assets/banner.png
|
||||
[website-link]: https://nestri.io
|
||||
[neko-url]: https://github.com/m1k1o/neko
|
||||
[image-star]: assets/star-us.png
|
||||
[moq-github-url]: https://quic.video
|
||||
[vmaf-cuda-link]: https://developer.nvidia.com/blog/calculating-video-quality-using-nvidia-gpus-and-vmaf-cuda/
|
||||
14
apps/docs/.eslintrc.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['@nuxt/eslint-config'],
|
||||
ignorePatterns: [
|
||||
'dist',
|
||||
'node_modules',
|
||||
'.output',
|
||||
'.nuxt'
|
||||
],
|
||||
rules: {
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/multi-word-component-names': 'off'
|
||||
}
|
||||
}
|
||||
29
apps/docs/.gitignore
vendored
@@ -1,25 +1,12 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
*.iml
|
||||
.idea
|
||||
*.log*
|
||||
.nuxt
|
||||
.vscode
|
||||
|
||||
# Local env files
|
||||
.DS_Store
|
||||
coverage
|
||||
dist
|
||||
sw.*
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.output
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# shadcn-docs-nuxt Minimal Starter
|
||||
|
||||
Starter template for [shadcn-docs-nuxt](https://github.com/ZTL-UwU/shadcn-docs-nuxt).
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on http://localhost:3000
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
[](https://hub.nuxt.com/new?repo=ZTL-UwU/shadcn-docs-nuxt-starter)
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FZTL-UwU%2Fshadcn-docs-nuxt-starter)
|
||||
[](https://app.netlify.com/start/deploy?repository=https%3A%2F%2Fgithub.com%2FZTL-UwU%2Fshadcn-docs-nuxt-starter)
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Checkout the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
87
apps/docs/RELAY.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# How to Deploy Your Own MoQ Relay on a Server
|
||||
|
||||
This guide will walk you through the steps to deploy your own MoQ relay on a server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Server Requirements:**
|
||||
- Ensure port 443 is open for both TCP and UDP (`:443/udp & :443/tcp`).
|
||||
- The server should have a minimum of **4GB RAM** and **2 vCPUs**.
|
||||
- Supports ARM or AMD64 architecture.
|
||||
|
||||
2. **Software Requirements:**
|
||||
- Docker and `docker-compose` must be installed on the server. You can use [this installation script](https://github.com/docker/docker-install) for Docker.
|
||||
- Git must be installed to clone the necessary repository.
|
||||
|
||||
3. **Certificates:**
|
||||
- You will need private and public certificates. It is recommended to use certificates from a trusted CA rather than self-signed certificates.
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### Step 1: Clone the Repository
|
||||
|
||||
Clone the `kixelated/moq-rs` repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kixelated/moq-rs moq
|
||||
```
|
||||
|
||||
### Step 2: Verify Port Availability
|
||||
|
||||
Check if port 443 is already in use on your server:
|
||||
|
||||
```bash
|
||||
sudo netstat -tulpn | grep ':443' | grep LISTEN
|
||||
```
|
||||
or
|
||||
```bash
|
||||
sudo lsof -i -P -n | grep LISTEN | grep 443
|
||||
```
|
||||
|
||||
If you find any processes using port 443, consider terminating them.
|
||||
|
||||
### Step 3: Configure Ports
|
||||
|
||||
Navigate to the cloned directory and edit the Docker compose file to use port 443:
|
||||
|
||||
```bash
|
||||
cd moq
|
||||
vim docker-compose.yml
|
||||
```
|
||||
|
||||
Change the ports section from lines 34 to 35 to:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
```
|
||||
|
||||
### Step 4: Prepare Certificates
|
||||
|
||||
Copy your generated certificates into the `moq/dev` directory and rename them:
|
||||
|
||||
```bash
|
||||
cp cert.pem moq/dev/localhost.crt
|
||||
cp key.pem moq/dev/localhost.key
|
||||
```
|
||||
|
||||
### Step 5: Start Docker Instances
|
||||
|
||||
Ensure you are in the root directory of the `moq` project, then start the Docker containers:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Step 6: Link Domain to Server IP
|
||||
|
||||
Configure your DNS settings to connect your server's IP address to your domain:
|
||||
|
||||
```
|
||||
Record Type: A
|
||||
Subdomain: relay.fst.so
|
||||
IP Address: xx.xxx.xx.xxx
|
||||
```
|
||||
|
||||
Congratulations, your MoQ server is now set up! You can verify its functionality by using the [MoQ Checker](https://nestri.pages.dev/moq/checker).
|
||||
@@ -1,79 +1,44 @@
|
||||
// https://github.com/nuxt-themes/docus/blob/main/nuxt.schema.ts
|
||||
export default defineAppConfig({
|
||||
shadcnDocs: {
|
||||
site: {
|
||||
name: 'Nestri Docs',
|
||||
description: 'Beautifully designed Nuxt Content template built with shadcn-vue. Customizable. Compatible. Open Source.',
|
||||
docus: {
|
||||
title: 'Nestri',
|
||||
description: 'An open-source, self-hosted Geforce Now alternative',
|
||||
image: 'https://feat-relay-hetzner.nestri.pages.dev/logo.webp',
|
||||
socials: {
|
||||
twitter: 'nestriness',
|
||||
github: 'nestriness/nestri',
|
||||
reddit: '/r/nestri',
|
||||
website: {
|
||||
label: 'Website',
|
||||
icon: 'lucide:house',
|
||||
href: 'https://nestri.io'
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
customizable: false,
|
||||
color: 'orange',
|
||||
radius: 0.5,
|
||||
},
|
||||
header: {
|
||||
title: 'Nestri Docs',
|
||||
showTitle: true,
|
||||
darkModeToggle: true,
|
||||
logo: {
|
||||
light: '/logo.webp',
|
||||
dark: '/logo.webp',
|
||||
},
|
||||
nav: [{
|
||||
title: 'Star on GitHub',
|
||||
icon: 'lucide:star',
|
||||
to: 'https://github.com/nestrilabs/nestri',
|
||||
target: '_blank',
|
||||
}, {
|
||||
title: 'Create Issues',
|
||||
icon: 'lucide:circle-dot',
|
||||
to: 'https://github.com/nestrilabs/nestri/issues',
|
||||
target: '_blank',
|
||||
}],
|
||||
links: [
|
||||
{
|
||||
icon: 'lucide:github',
|
||||
to: 'https://github.com/nestrilabs/nestri',
|
||||
target: '_blank',
|
||||
}],
|
||||
github: {
|
||||
dir: 'apps/docs/content',
|
||||
branch: 'main',
|
||||
repo: 'nestri',
|
||||
owner: 'nestriness',
|
||||
edit: true
|
||||
},
|
||||
aside: {
|
||||
useLevel: true,
|
||||
collapse: false,
|
||||
level: 0,
|
||||
collapsed: false,
|
||||
exclude: []
|
||||
},
|
||||
main: {
|
||||
breadCrumb: true,
|
||||
showTitle: true,
|
||||
padded: true,
|
||||
fluid: true
|
||||
},
|
||||
logo: "/nestri-logo.svg",
|
||||
header: {
|
||||
logo: true,
|
||||
showLinkIcon: true,
|
||||
exclude: [],
|
||||
fluid: true
|
||||
},
|
||||
footer: {
|
||||
credits: 'Copyright © 2025',
|
||||
links: [{
|
||||
icon: 'lucide:github',
|
||||
to: 'https://github.com/nestrilabs/nestri',
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
icon: 'ri:discord-line',
|
||||
to: 'https://discord.com/invite/Y6etn3qKZ3',
|
||||
target: '_blank',
|
||||
}],
|
||||
},
|
||||
toc: {
|
||||
enable: true,
|
||||
title: 'On This Page',
|
||||
links: [{
|
||||
title: 'Star on GitHub',
|
||||
icon: 'lucide:star',
|
||||
to: 'https://github.com/nestrilabs/nestri',
|
||||
target: '_blank',
|
||||
}, {
|
||||
title: 'Create Issues',
|
||||
icon: 'lucide:circle-dot',
|
||||
to: 'https://github.com/nestrilabs/nestri/issues',
|
||||
target: '_blank',
|
||||
}],
|
||||
},
|
||||
search: {
|
||||
enable: true,
|
||||
inAside: false,
|
||||
credits: false,
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border:214.3 31.8% 91.4%;
|
||||
--input:214.3 31.8% 91.4%;
|
||||
--ring:221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background:222.2 84% 4.9%;
|
||||
--foreground:210 40% 98%;
|
||||
|
||||
--card:222.2 84% 4.9%;
|
||||
--card-foreground:210 40% 98%;
|
||||
|
||||
--popover:222.2 84% 4.9%;
|
||||
--popover-foreground:210 40% 98%;
|
||||
|
||||
--primary:217.2 91.2% 59.8%;
|
||||
--primary-foreground:222.2 47.4% 11.2%;
|
||||
|
||||
--secondary:217.2 32.6% 17.5%;
|
||||
--secondary-foreground:210 40% 98%;
|
||||
|
||||
--muted:217.2 32.6% 17.5%;
|
||||
--muted-foreground:215 20.2% 65.1%;
|
||||
|
||||
--accent:217.2 32.6% 17.5%;
|
||||
--accent-foreground:210 40% 98%;
|
||||
|
||||
--destructive:0 62.8% 30.6%;
|
||||
--destructive-foreground:210 40% 98%;
|
||||
|
||||
--border:217.2 32.6% 17.5%;
|
||||
--input:217.2 32.6% 17.5%;
|
||||
--ring:224.3 76.3% 48%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.step {
|
||||
counter-increment: step;
|
||||
}
|
||||
|
||||
.step:before {
|
||||
@apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background;
|
||||
@apply -ml-[50px] -mt-1;
|
||||
content: counter(step);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
62
apps/docs/components/AppSocialIcons.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
const socials = ['twitter', 'facebook', 'instagram', 'tiktok', 'youtube', 'github', 'medium', 'reddit', 'discord']
|
||||
|
||||
const { config } = useDocus()
|
||||
|
||||
const icons = computed<any>(() => {
|
||||
return Object.entries(config.value.socials || {})
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === 'object') {
|
||||
return value
|
||||
} else if (typeof value === 'string' && value && socials.includes(key)) {
|
||||
return {
|
||||
href: /^https?:\/\//.test(value) ? value : `https://${key}.com/${value}`,
|
||||
icon: `fa-brands:${key}`,
|
||||
label: value,
|
||||
rel: 'noopener noreferrer'
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
v-for="icon in icons"
|
||||
:key="icon.label"
|
||||
:rel="icon.rel"
|
||||
:title="icon.label"
|
||||
:aria-label="icon.label"
|
||||
:href="icon.href"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon
|
||||
v-if="icon.icon"
|
||||
:name="icon.icon"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<style lang="ts" scoped>
|
||||
css({
|
||||
a: {
|
||||
display: 'flex',
|
||||
color: '{color.gray.500}',
|
||||
padding: '{space.4}',
|
||||
|
||||
'@dark': {
|
||||
color: '{color.gray.400}'
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
color: '{color.gray.700}',
|
||||
'@dark': {
|
||||
color: '{color.gray.200}',
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
</style>
|
||||
3
apps/docs/components/Logo.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<img width="120" src="/img/nestri-logo-sm.svg"/>
|
||||
</template>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
<div class="py-8">
|
||||
<h2 class="text-3xl lg:text-4xl font-bold mb-12 text-gray-900 dark:text-white">
|
||||
<h2 class="text-3xl lg:text-4xl font-bold mb-12 text-gray-900">
|
||||
Contributors made <span class="text-orange-500">Nestri</span>
|
||||
</h2>
|
||||
<div class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-8 gap-4 sm:gap-5 lg:gap-6">
|
||||
@@ -22,7 +22,7 @@
|
||||
}"
|
||||
>
|
||||
<UTooltip class="w-full text-orange-500" :text="contributor.login">
|
||||
<img
|
||||
<NuxtImg
|
||||
:src="contributor.avatar_url"
|
||||
provider="ipx"
|
||||
densities="x1 x2"
|
||||
@@ -33,7 +33,7 @@
|
||||
class="rounded-xl w-full h-full transition lg:hover:scale-110"
|
||||
/>
|
||||
</UTooltip>
|
||||
<span class="inline-block rounded-t px-1 bg-gray-950 dark:bg-white text-white dark:text-gray-950 absolute -bottom-2 right-0 font-medium text-sm">
|
||||
<span class="inline-block rounded-t px-1 bg-gray-950 text-white absolute -bottom-2 right-0 font-medium text-sm">
|
||||
<span class="font-light text-xs text-gray-400">#</span>{{ index + 1 }}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtLink v-if="logo.light && logo.dark" class="flex">
|
||||
<img :src="logo.light" class="h-7 dark:hidden" />
|
||||
<img :src="logo.dark" class="hidden h-7 dark:block" />
|
||||
<span v-if="showTitle && title" class="ml-3 self-center font-bold">
|
||||
{{ title }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { logo, title, showTitle } = useConfig().value.header;
|
||||
</script>
|
||||
55
apps/docs/content/0.index.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Home
|
||||
navigation: false
|
||||
layout: page
|
||||
main:
|
||||
fluid: false
|
||||
---
|
||||
|
||||
:ellipsis{right=0px width=75% blur=150px}
|
||||
|
||||
::block-hero
|
||||
---
|
||||
cta:
|
||||
- Get started
|
||||
- /introduction/what-is-nestri
|
||||
|
||||
secondary:
|
||||
- Open on GitHub →
|
||||
- https://github.com/nestriness/nestri
|
||||
---
|
||||
|
||||
#title
|
||||
An open-source, self-hosted Geforce Now alternative.
|
||||
|
||||
#description
|
||||
Play your favorite games on the go or with your friends on your own game cloud.
|
||||
|
||||
#extra
|
||||
::list
|
||||
- **Selfhosted** cloud gaming
|
||||
- **Open Source** and **Free**
|
||||
- 1.5k ⭐️ on GitHub
|
||||
|
||||
|
||||
::
|
||||
|
||||
<!--#support
|
||||
::terminal
|
||||
---
|
||||
content:
|
||||
- npx nuxi@latest init -t themes/docus
|
||||
- cd docs
|
||||
- npm install
|
||||
- npm run dev
|
||||
---
|
||||
::-->
|
||||
::
|
||||
|
||||
|
||||
|
||||
::contributors
|
||||
|
||||
|
||||
|
||||
::
|
||||
@@ -1,25 +1,21 @@
|
||||
---
|
||||
title: What is Nestri?
|
||||
description: Learn about Nestri, an open-source, self-hostable cloud gaming platform that gives you full control over your gaming server, streaming, and setup.
|
||||
icon: 'lucide:gamepad'
|
||||
---
|
||||
# What is Nestri?
|
||||
|
||||
Nestri is a self-hosted cloud gaming platform that enables you to spin up dedicated gaming sessions remotely and play your own games from any device with a browser. Unlike remote desktop solutions like Parsec, which focus on streaming a desktop environment, Nestri is designed specifically for cloud gaming. It works similarly to services like NVIDIA GeForce Now, allowing you to enjoy high-performance gaming without needing to be physically near your gaming PC.
|
||||
|
||||
The key difference with Nestri is that it’s open-source and can be self-hosted, so you have full control over the server, the games you install, and the entire setup. Nestri is ideal for gamers who prioritize privacy, flexibility, and control, offering a way to manage your own gaming infrastructure rather than relying on third-party services. As long as you have a stable internet connection and access to a web browser, you can game from virtually anywhere.
|
||||
The key difference with Nestri is that it’s entirely self-hosted, so you have full control over the server, the games you install, and the entire setup. Nestri is ideal for gamers who prioritize privacy, flexibility, and control, offering a way to manage your own gaming infrastructure rather than relying on third-party services. As long as you have a stable internet connection and access to a web browser, you can game from virtually anywhere.
|
||||
|
||||
## Nestri Architecture
|
||||
|
||||
Nestri is composed of the following key components:
|
||||
## Nestri Modules
|
||||
|
||||
#### Nestri Node
|
||||
To provide a smooth and efficient gaming experience, Nestri is composed of the following key components:
|
||||
|
||||
The **Nestri Node** (also referred to as *Instance*) is the core of your Nestri setup. It acts as the game server where you install and run your games. The Nestri Node streams gameplay from the machine it’s installed on, allowing you to access your games remotely. It runs on most Linux-based systems and major vendor's GPUs (Intel, AMD, NVIDIA).
|
||||
### Nestri Node
|
||||
The Nestri Node is the core of your Nestri setup. It acts as the game server where you install and run your games. The Nestri Node streams gameplay from the machine it’s installed on, allowing you to access your games remotely. It runs on most Linux-based systems and requires an NVIDIA graphics card to ensure a high-quality gaming experience.
|
||||
|
||||
**Nestri Node** runs within a container, which isolates it from the host system, keeping the host environment clean and secure. This containerization also allows for easy updates, management and recovery of your gaming environment.
|
||||
Since Nestri Node cannot run alongside Xorg (the graphical interface), it’s recommended to install it on a dedicated machine. This way, your server can focus solely on streaming your games while avoiding conflicts with your local display setup.
|
||||
|
||||
#### Nestri Relay
|
||||
### Nestri Relay
|
||||
The Nestri Relay is responsible for transporting the video stream from your Nestri Node to the device you're gaming on. By default, Nestri connects to the Nestri-hosted Relay, which requires no configuration and is available for all users. This simplifies the setup process, ensuring a smooth streaming experience without the need for advanced networking or SSL certificate management.
|
||||
|
||||
The **Nestri Relay** is responsible for taking the audio-video stream from your **Nestri Node** and sending that forward to the device you're gaming on with minimal latency. This is essentially a WebRTC SFU (Selective Forwarding Unit) that splits single incoming stream to multiple potential players, allowing multiple devices to connect to the same game session without overwhelming the **Nestri Node** with multiple outgoing streams.
|
||||
For advanced users, it's possible to self-host the relay, but this requires the setup of secure SSL certificates. This option is typically more complex and is recommended only for developers or those familiar with network configuration.
|
||||
|
||||
**Nestri Relay** runs within a container, similar to the **Nestri Node**, and can be deployed on the same machine or a different one.
|
||||
@@ -1,23 +1,10 @@
|
||||
---
|
||||
title: FAQ
|
||||
description: Got questions about Nestri? This FAQ covers everything from pricing and setup to game compatibility and system requirements. Whether you're exploring the free self-hosted version, the Bring Your Own GPU (BYOG) option, or the hosted service, you’ll find all the details here.
|
||||
icon: 'lucide:message-circle-question'
|
||||
---
|
||||
# FAQ
|
||||
|
||||
|
||||
## Is Nestri free?
|
||||
Yes! Nestri offers three options: a free, self-hosted version, a free and paid **Bring Your Own GPU (BYOG)** version, and a paid, hosted version.
|
||||
|
||||
- **Self-Hosted Version (Free):**
|
||||
If you have your own server, you can install and run Nestri for free. Since Nestri is open-source, you have full access to the codebase, allowing for transparency and flexibility in your setup.
|
||||
|
||||
- **Bring Your Own GPU (BYOG):**
|
||||
With BYOG, you can use your own server with a GPU to play your games while avoiding the hassle of setting up relays, web interfaces, port forwarding, and other technical configurations. BYOG is available in both a free and a paid package:
|
||||
- The **Free BYOG package** lets you get started with basic functionality.
|
||||
- The **Paid BYOG package** unlocks exclusive features only available in BYOG and Hosted versions.
|
||||
|
||||
- **[Hosted Version (Paid)](https://nestri.io/pricing):**
|
||||
The hosted version of Nestri operates similarly to services like NVIDIA GeForce Now. With a subscription, you can play your games on Nestri’s infrastructure without needing any technical knowledge—just sign up, log in, and start gaming!
|
||||
|
||||
Yes! Nestri offers two options: a free, self-hosted version and a paid, hosted version.
|
||||
- Self-Hosted Version (Free): If you have your own server, you can install and run Nestri for free. Since Nestri is open-source, you have full access to the codebase, allowing for transparency and flexibility in your setup.
|
||||
- Hosted Version (Paid): The hosted version of Nestri operates similarly to services like NVIDIA GeForce Now. With a subscription, you can play your games on Nestri’s infrastructure without needing any technical knowledge—just sign up, log in, and start gaming!
|
||||
|
||||
## Does Nestri require a high-speed internet connection?
|
||||
Yes, a stable and fast internet connection is essential for a smooth gaming experience. While you don’t need extremely high speeds (like 1 Gbps fiber), low latency is critical. Since cloud gaming is sensitive to delay, your device needs to connect to one of our relays with minimal lag. Ensuring a strong, stable network connection close to a relay server is important to avoid delays in gameplay, especially during fast-paced action sequences.
|
||||
@@ -29,28 +16,4 @@ Currently, we have one relay deployed in Helsinki, Finland. As we grow, we plan
|
||||
No, Nestri is not like Parsec, which is used to access and game on an existing desktop remotely. Nestri is a server application designed specifically for cloud gaming. Rather than connecting to a physical Windows desktop, Nestri runs your games within a Docker or Podman container, allowing you to play remotely without needing to access a traditional desktop environment.
|
||||
|
||||
## Do I need a high-end server with a 4090 GPU and a 64-core CPU?
|
||||
Not necessarily! Nestri doesn’t have strict hardware requirements in terms of having the latest or most powerful CPU or GPU. Just as with traditional gaming, better hardware will enhance your experience with improved graphics and higher FPS. The exact specs you need will depend on the games you want to play and the performance you’re aiming for. Keep in mind that, because Nestri has to use a GPU to encode the game stream for lowest possible latency, there will be a bit of additional processing required.
|
||||
|
||||
## Do you have an app for phone or TV?
|
||||
Not yet! At the moment, we don’t have a dedicated app. However, since the Nestri interface works on most devices with a Chromium-based browser, you can play your games that way on your phone, TV, or other devices.
|
||||
|
||||
We’re actively working on developing an app that will make it even easier to play your games on mobile, your TV, or install a client directly on your PC. Stay tuned for updates!
|
||||
|
||||
## Do I need to port forward to use Nestri?
|
||||
No! If you’re using Nestri BYOG, you won’t need to port forward anything on your router or firewall.
|
||||
|
||||
Since Nestri is built with WebRTC, the Nestri node connects directly with the client via our relays. All you need to do is install Nestri on your server and start your game through our web interface — no complicated networking setup required!
|
||||
|
||||
## What games can I play on Nestri?
|
||||
Currently, Nestri only supports Steam games that are compatible with Proton, as Nestri is Linux-based.
|
||||
|
||||
When you launch Nestri, you’ll have access to Steam Big Picture mode, just like on your PC. You can check which games are supported by Proton and their ratings on [ProtonDB](https://www.protondb.com/).
|
||||
|
||||
This ensures a smooth gaming experience for a wide range of titles, and we’re continually working to expand compatibility!
|
||||
|
||||
## Do I need my own server?
|
||||
No! We also offer a **[Hosted version](https://nestri.io/pricing)**, where you can use our infrastructure. All you need to do is start your game through our interface, and we’ll handle the rest.
|
||||
|
||||
If you don’t have your own physical server, you can also run Nestri in the cloud. Simply use a dedicated server with a GPU or platforms like AWS, Digital Ocean, or similar services that offer GPU solutions.
|
||||
|
||||
Whether you prefer using your own setup or a hassle-free hosted solution, Nestri has you covered!
|
||||
Not necessarily! Nestri doesn’t have strict hardware requirements in terms of having the latest or most powerful CPU or GPU. Just as with traditional gaming, better hardware will enhance your experience with improved graphics and higher FPS. The exact specs you need will depend on the games you want to play and the performance you’re aiming for. Keep in mind that, because Nestri runs games on Linux using Proton and the Gstreamer encoding, there will be a bit of additional processing required, so some extra power will be helpful.
|
||||
@@ -1,3 +1,2 @@
|
||||
title: Getting started
|
||||
icon: lucide:rocket
|
||||
icon: ph:star-duotone
|
||||
navigation.redirect: /introduction/what-is-nestri
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
title: What is Nestri Node?
|
||||
description: What is Nestri Node and how does it powers the Nestri eco-system and your self-hosted cloud gaming experience.
|
||||
icon: 'lucide:message-circle-question'
|
||||
---
|
||||
# What is Nestri Node?
|
||||
|
||||
Nestri Node is the core component of Nestri's self-hosted cloud-gaming solution, designed for users who want the freedom and flexibility of running their own game-streaming server. Similar to services like NVIDIA GeForce Now, Nestri allows you to play your games remotely via your browser. However, unlike other cloud-gaming platforms, Nestri is fully self-hosted, giving you complete control over your server and gaming experience.
|
||||
|
||||
The Nestri Node is the actual server where you install your games. Once set up, you can stream and play your games remotely from any compatible device. It runs on machines with Linux and requires an NVIDIA, AMD or an Intel graphics card .
|
||||
## ⚠️ Important Note
|
||||
|
||||
We recommend not installing Nestri Node on your primary PC if you only intend to use it over a weekend. This is because Nestri Node cannot run simultaneously with Xorg, the display server responsible for managing the graphical user interface (GUI). This means that while Nestri Node is running, you will not be able to use an attached screen. For this reason, Nestri Node is best set up on a dedicated machine that won’t be used for other tasks.
|
||||
|
||||
**Nestri Node** is the core component of Nestri's self-hosted cloud-gaming solution. It is the actual server where you install your games. Once set up, you can stream and play your games remotely from any compatible device. It runs on most Linux-based systems and requires a NVIDIA, AMD or Intel graphics card.
|
||||
|
||||
@@ -1,27 +1,53 @@
|
||||
---
|
||||
title: Prerequisites
|
||||
description: Essential system and software requirements for setting up Nestri on your server, including GPU compatibility, OS recommendations, and necessary configurations.
|
||||
icon: 'lucide:check-circle'
|
||||
---
|
||||
# Prerequisite
|
||||
|
||||
To run Nestri on your own server, there are several essential preparations required before installing Nestri Node. This page outlines the key requirements to get Nestri up and running smoothly.
|
||||
To run Nestri on your own server, there are several essential preparations required before installing nestri-node. This page outlines the key requirements to get Nestri up and running smoothly.
|
||||
|
||||
Nestri Node supports AMD, NVIDIA, and Intel graphics cards.
|
||||
Nestri-node supports AMD, NVIDIA, and Intel graphics cards. For optimal performance, however, we recommend using Intel or NVIDIA GPUs. Our testing has shown that these GPUs provide the best results, while AMD graphics cards may encounter limitations due to partial support for Arch Linux in AMD's AMF drivers. As a workaround, we utilize the VA-API plugin for GStreamer with AMD cards to ensure functionality.
|
||||
|
||||
While it might be tempting to skip this setup, we advise against it. Taking the time to prepare now will help you avoid potential issues and wasted hours later.
|
||||
|
||||
## Recommended host configuration
|
||||
|
||||
::list{type="primary"}
|
||||
- **AMD, NVIDIA or Intel GPU**
|
||||
- **CPU with AVX2 support**
|
||||
- **Fedora or Arch** based distribution
|
||||
- **NVIDIA or Intel GPU** (AMD is supported, but not reccomended, due to lack of natively supported API-drivers in CachyOS)
|
||||
- **AVX supported CPU** (If your CPU doesent support AVX, you can use our `noavx` image)
|
||||
- **Fedora or Arch** based distributions ( [Debian and Ubuntu is **not** supported](/nestri-node/node-faq#can-i-run-nestri-node-on-debianubuntu) )
|
||||
::
|
||||
|
||||
## Software Requirements
|
||||
|
||||
::list{type="primary"}
|
||||
- **GPU Drivers** (if not provided by the kernel)
|
||||
- **Podman or Docker** (Podman is recommended for better compatibility)
|
||||
- **Nvidia Drivers**
|
||||
- **[NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt)**
|
||||
- **[Docker](https://linuxiac.com/how-to-install-docker-on-ubuntu-24-04-lts/)**
|
||||
|
||||
::
|
||||
|
||||
## Disconnect monitor
|
||||
Since Nestri requires access to your GPU, then you need to unplug you screen from it.
|
||||
If you want to see the Desktop and have a integrated graphicscard in your CPU, then you can connect your monitor to the motherboard.
|
||||
### Change the Default Boot Target to Multi-User (Non-GUI Mode)
|
||||
Ubuntu typically starts in graphical mode (using the graphical.target systemd target). You should change to the non-graphical multi-user.target, which will prevent Xorg from starting.
|
||||
|
||||
1. Open a terminal or access your system via SSH.
|
||||
2. To check your current default target (which should be graphical.target)
|
||||
|
||||
```bash
|
||||
systemctl get-default
|
||||
|
||||
```
|
||||
|
||||
3. Change the default target to multi-user.target (which corresponds to text mode, without Xorg):
|
||||
```bash
|
||||
sudo systemctl set-default multi-user.target
|
||||
|
||||
|
||||
```
|
||||
|
||||
4. Reboot the system
|
||||
|
||||
5. Verify that Xorg is not running
|
||||
```bash
|
||||
nvidia-smi
|
||||
```
|
||||
|
||||
|
||||
@@ -1,72 +1,101 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: Follow this guide to set up and run your own Nestri Node for cloud gaming.
|
||||
icon: 'lucide:message-circle-question'
|
||||
---
|
||||
# Getting Started
|
||||
|
||||
::alert{type="danger"}
|
||||
Nestri is in a **very early phase**, so errors and bugs may occur.
|
||||
Nestri is in a **very early-beta phase**, so errors and bugs may occur.
|
||||
::
|
||||
|
||||
### Step 0: Construct Your Docker Image
|
||||
Checkout your branch with the latest version of nestri and build the image `<your-nestri-image>` within git root folder:
|
||||
```bash
|
||||
docker buildx build -t <your-nestri-image>:latest -f Containerfile.runner .
|
||||
```
|
||||
|
||||
::alert{type="info"}
|
||||
You can pull the docker image from GitHub Container Registry with:
|
||||
```bash [pull image command]
|
||||
podman pull ghcr.io/nestrilabs/nestri/runner:nightly
|
||||
You can right now also pull the docker image from DatHorse GitHub Containter Registry with:
|
||||
```bash
|
||||
docker pull ghcr.io/datcaptainhorse/nestri-cachyos:latest
|
||||
```
|
||||
::
|
||||
|
||||
### Step 1: Create a home directory for your Nestri Node
|
||||
This will be the directory where Steam, games and other persistent files will be saved.
|
||||
You may use any directory you like, but for simplicity, we will use `~/nestri` as the home directory in this guide.
|
||||
```bash [create home directory command]
|
||||
mkdir -p ~/nestri
|
||||
sudo chmod 777 ~/nestri
|
||||
### Step 1: Navigate to Your Game Directory
|
||||
First, change your directory to the location of your `.exe` file. For Steam games, this typically means:
|
||||
```bash
|
||||
cd $HOME/.steam/steam/steamapps
|
||||
ls -la .
|
||||
```
|
||||
The above will create a directory called `nestri` in your home directory and set the permissions to allow read, write, and execute for all users.
|
||||
This is important for the Nestri Node to function properly.
|
||||
### Step 2: Launch the Nestri Runner
|
||||
With your home directory ready, insert it into the command below, replacing `<relay_url>` with the relay's URL you want to use.
|
||||
You will also need to replace `<room_name>` with an unique name for the room you will be using to play your games.
|
||||
### Step 2: Generate a Session ID
|
||||
Create a unique session ID using the following command:
|
||||
```bash
|
||||
echo "$(head /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | head -c 16)"
|
||||
```
|
||||
This command generates a random 16-character string. Be sure to note this string carefully, as you'll need it for the next step.
|
||||
### Step 3: Launch the Nestri Server
|
||||
With your SESSION_ID ready, insert it into the command below, replacing `<your_session_id>` with your actual session ID, also replace `<relay_url>` with your relay URL and `<your-nestri-image>` with your build nestri image or nestri remote image. Then run the command to start the Nestri server:
|
||||
|
||||
```bash [run container (nvidia)]
|
||||
podman run --replace -d --name=nestri --shm-size=6g --cap-add=SYS_NICE --device /dev/dri/ -e RELAY_URL='<relay_url>' -e NESTRI_ROOM='<room_name>' -e RESOLUTION=1920x1080 -e FRAMERATE=60 -e NESTRI_PARAMS='--verbose=true --dma-buf=true --audio-rate-control=cbr --video-codec=h264 --video-rate-control=cbr --video-bitrate=8000' -v ~/nestri:/home/nestri --device /dev/nvidia-uvm --device /dev/nvidia-uvm-tools --device /dev/nvidiactl --device /dev/nvidia0 --device /dev/nvidia-modeset ghcr.io/nestrilabs/nestri/runner:nightly
|
||||
```bash
|
||||
docker run --rm -it --shm-size=1g --gpus all -e NVIDIA_DRIVER_CAPABILITIES=all --runtime=nvidia -e RELAY_URL='<relay_url>' -e NESTRI_ROOM=<your_session_id> -e RESOLUTION=1920x1080 -e FRAMERATE=60 -e NESTRI_PARAMS='--verbose=true --video-codec=h264 --video-bitrate=4000 --video-bitrate-max=6000'--name nestri -d -v "$(pwd)":/mnt/game/ <your-nestri-image>:latest
|
||||
```
|
||||
|
||||
```bash [run container (amd/intel)]
|
||||
podman run --replace -d --name=nestri --shm-size=6g --cap-add=SYS_NICE --device /dev/dri/ -e RELAY_URL='<relay_url>' -e NESTRI_ROOM='<room_name>' -e RESOLUTION=1920x1080 -e FRAMERATE=60 -e NESTRI_PARAMS='--verbose=true --dma-buf=true --audio-rate-control=cbr --video-codec=h264 --video-rate-control=cbr --video-bitrate=8000' -v ~/nestri:/home/nestri ghcr.io/nestrilabs/nestri/runner:nightly
|
||||
### Step 4: Get Into your container
|
||||
Get into your container to start your game:
|
||||
```bash
|
||||
sudo docker exec -it nestri bash
|
||||
```
|
||||
### Step 5: Installing a Launcher
|
||||
For most games that are not DRM free you need a launcher. In this case use the umu launcher and optional mangohud:
|
||||
```bash
|
||||
pacman -S --overwrite="*" umu-launcher mangohud
|
||||
```
|
||||
|
||||
### Step 3: Begin Playing
|
||||
Finally, construct the play URL with your room name and relay URL:
|
||||
`https://nestri.io/play/<room_name>?peerURL=<relay_url>`
|
||||
|
||||
Navigate to this URL in your browser, click on the button to capture your mouse pointer and keyboard, and start playing!
|
||||
|
||||
### Stop the Nestri Container
|
||||
If you want to stop the Nestri container, you can use the following command:
|
||||
|
||||
```bash [stop container command]
|
||||
podman stop nestri
|
||||
### Step 5: Running Your Game
|
||||
You have to execute your game now with nestri user. If you have a linux game just execute it with the nestri user
|
||||
```bash
|
||||
su nestri
|
||||
source /etc/nestri/envs.sh
|
||||
GAMEID=0 PROTONPATH=GE-Proton mangohud umu-run /mnt/game/<your-game.exe>
|
||||
```
|
||||
|
||||
### Start the Nestri Container
|
||||
If you want to start the Nestri container after stopping it, you can use the following command:
|
||||
### Step 6: Begin Playing
|
||||
Finally, construct the play URL with your session ID:
|
||||
`https://nestri.io/play/<your_session_id>`
|
||||
|
||||
Navigate to this URL in your browser, click on the page to capture your mouse pointer, and start playing!
|
||||
|
||||
::alert{type="info"}
|
||||
You can also use other relays/frontends depending on your choosen `<relay_url>`
|
||||
For testing you can use DatHorse Relay and Frontend:
|
||||
|
||||
| **Placeholder** | **URL** |
|
||||
| ---------------------------- | ---------- |
|
||||
| `<relay_url>` | `https://relay.dathorse.com/` |
|
||||
| `<frontend_url>` | `https://nestritest.dathorse.com/play/<your_session_id>` |
|
||||
::
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
Nestri Node is easy to install using the provided installation script. Follow the steps below to get started.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the installation script using `wget`:
|
||||
|
||||
```bash
|
||||
wget https://github.com/nestriness/nestri/nestri-node-install.sh
|
||||
|
||||
```bash [start container command]
|
||||
podman start nestri
|
||||
```
|
||||
|
||||
### Remove the Nestri Container
|
||||
To remove the container, you can use the following command:
|
||||
2. Make the script executable:
|
||||
```bash
|
||||
chmod +x nestri-node-install.sh
|
||||
|
||||
|
||||
```bash [remove container command]
|
||||
podman rm nestri
|
||||
```
|
||||
|
||||
### Update Nestri Container
|
||||
To update the Nestri container, you can use the following command:
|
||||
|
||||
```bash [update container command]
|
||||
podman pull ghcr.io/nestrilabs/nestri/runner:nightly
|
||||
3. Run the script to start the installation process:
|
||||
```bash
|
||||
./nestri-node-install.sh
|
||||
```
|
||||
After which, you can recreate the container with the latest image using the same command you used in Step 2.
|
||||
::-->
|
||||
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
# Troubleshooting
|
||||
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
---
|
||||
title: Container CLI
|
||||
description: Configure and manage your Nestri container environment using CLI parameters for relay settings, video resolution, GPU selection, and encoding options.
|
||||
icon: 'lucide:terminal'
|
||||
---
|
||||
|
||||
# Container CLI
|
||||
The Container CLI for Nestri provides parameters to configure and manage your container environment. Use these options to set values like `relay-url`, `video resolution`, and `frame rate`. Additionally, activate `verbose` mode and logging to assist in debugging and error tracking. This documentation details each parameter to help you optimize your container setup effectively
|
||||
|
||||
| **Parameter** | **Type** | **Default** | **Description** |
|
||||
|--------------------------|----------|-------------|-----------------------------------------------------------------------------------|
|
||||
| `-v, --verbose` | `string` | false | Enable verbose output. Set to `true` for detailed logs. |
|
||||
| `-d, --debug` | `string` | false | Enable additional debugging features. Set to `true` for extra debug information. |
|
||||
| `-u, --relay-url` | `string` | | Nestri relay URL. Specify the URL for the Nestri relay server. |
|
||||
| `-r, --resolution` | `string` | 1280x720 | Display/stream resolution in 'WxH' format. Default is 1280x720. |
|
||||
| `-f, --framerate` | `string` | 60 | Display/stream framerate. Default is 60 FPS. |
|
||||
| `--room` | `string` | | Nestri room name/identifier. Specify the room for your Nestri session. |
|
||||
| `-g, --gpu-vendor` | `string` | | GPU vendor to use (e.g., NVIDIA, AMD, Intel). |
|
||||
| `-n, --gpu-name` | `string` | | GPU name to use. Specify the exact GPU model. |
|
||||
| `-i, --gpu-index` | `string` | -1 | GPU index to use. Default is -1 (auto-select). |
|
||||
| `--gpu-card-path` | `string` | | Force a specific GPU by `/dev/dri/` card or render path. |
|
||||
| `-c, --video-codec` | `string` | h264 | Preferred video codec. Options: h264, h265, av1. Default is h264. |
|
||||
| `--video-encoder` | `string` | | Override video encoder (e.g., `nvenc`, `libx264`). |
|
||||
| `--video-rate-control` | `string` | cbr | Rate control method. Options: cqp, vbr, cbr. Default is cbr. |
|
||||
| `--video-cqp` | `string` | 26 | Constant Quantization Parameter (CQP) quality. Default is 26. |
|
||||
| `--video-bitrate` | `string` | 6000 | Target bitrate in kbps. Default is 6000 kbps. |
|
||||
| `--video-bitrate-max` | `string` | 8000 | Maximum bitrate in kbps. Default is 8000 kbps. |
|
||||
| `--video-encoder-type` | `string` | hardware | Encoder type. Options: software, hardware. Default is hardware. |
|
||||
| `--audio-capture-method` | `string` | pulseaudio | Audio capture method. Options: pulseaudio, pipewire, alsa. Default is pulseaudio. |
|
||||
| `--audio-codec` | `string` | opus | Preferred audio codec. Default is opus. |
|
||||
| `--audio-encoder` | `string` | | Override audio encoder (e.g., `opusenc`). |
|
||||
| `--audio-rate-control` | `string` | cbr | Audio rate control method. Options: cqp, vbr, cbr. Default is cbr. |
|
||||
| `--audio-bitrate` | `string` | 128 | Target audio bitrate in kbps. Default is 128 kbps. |
|
||||
| `--audio-bitrate-max` | `string` | 192 | Maximum audio bitrate in kbps. Default is 192 kbps. |
|
||||
| `--dma-buf` | `string` | false | Use DMA-BUF for pipeline. Set to `true` to enable DMA-BUF support. |
|
||||
| `-h, --help` | | | Print help information for the CLI parameters. |
|
||||
|
||||
| **Parameter** | **Type** | **Default** | **Description** |
|
||||
| ---------------------------- | ---------- | --------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `-v, --verbose` | `string` | false | Shows more logs, for issues we recommend turning it on before running nestri-server and sending the logs for debugging (i.e. `nestri-server --verbose=true > logs.txt`) |
|
||||
| `-d, --debug-feed` | `string` | false | Adds a timer overlay at bottom-right in the video stream, along with spawning an X11 window on host for doing comparisons against |
|
||||
| `-u, --relay-url` | `string` | https://relay.fst.so | [MoQ relay](/nestri-relay/what-is-nestri-node) endpoint URL (must begin with `https://` as MoQ __can't work with unsafe connections__) |
|
||||
| `-p, --relay-path` | `string` | default generated on start if not set | namespace/path for the stream, identifies the stream (basically stream name), must be unique |
|
||||
| **Video** | | | |
|
||||
| `-r, --resolution ` | `string` | 1280x720 | Sets nestri virtual display + stream resolution using `WIDTHxHEIGHT` format |
|
||||
| `-f, --framerate` | `integer` | 60 | Framerate for nestri virtual display + stream |
|
||||
| `-g, --gpu-vendor` | `string` | | allows selecting specific GPU by vendor name (`nvidia`, `amd` or `intel`) |
|
||||
| `-i, --gpu-index` | `string` | | allows selecting a GPU by it's general name, doesn't have to be full name as it's matched partially (i.e. `3060` would get you `RTX 3060` GPU, but it would also let `RTX 3060 Ti` pass) |
|
||||
| `-a, --gpu-card-path` | `string` | | allows specifying GPU by `/dev/dri/cardX` or `/dev/dri/renderX` path, this won't work with the other 3 gpu parameters as it's explicitly setting the GPU |
|
||||
| **Encoder** | | | |
|
||||
| `-c, --encoder-vcodec` | `string` | h264 | Sets the stream video codec (`h264` or `av1`) |
|
||||
| `-t, --encoder-type` | `string` | hardware | Sets whether to use GPU encoder (`hardware`), or CPU encoder (`software`, only should be used with debugging or if GPU has no encoding capabilities) |
|
||||
| `-e, --encoder-name` | `string` | | forces a specific encoder by GStreamer element name (i.e. `vah264enc`) |
|
||||
| `-q, --encoder-cqp` | `string` | 25 | sets the stream quality level, lower means higher quality and much more bitrate used |
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
---
|
||||
title: Node FAQ
|
||||
description: This FAQ is made to address common questions about Nestri Node, the container which runs your games. Whether you're curious about compatibility, setup, or performance, you'll find answers to help you get started.
|
||||
icon: 'lucide:info'
|
||||
---
|
||||
# Node FAQ
|
||||
|
||||
## Can I run Nestri Node on Debian/Ubuntu?
|
||||
Yes, this is now possible, but not recommended due to several issues from those distributions.
|
||||
This FAQ is made to address common questions about Nestri Node, the container which runs your games. Whether you're curious about compatibility, setup, or performance, you'll find answers to help you get started..
|
||||
|
||||
## Can I run Nestri Node on Debian/Ubuntu? :icon{name="logos:ubuntu" style="opacity:100"} :icon{name="logos:debian" style="opacity:100"}
|
||||
Unfortunately, it is not possible to run Nestri Node on Debian-based distributions like Ubuntu at this time. After extensive debugging efforts, we have decided to focus on platforms that currently work well, such as Fedora and Arch-based distributions. We may revisit the possibility of supporting Debian in the future, but for now, it is not supported.
|
||||
|
||||
## Can I run Nestri Node in a virtualized environment like Proxmox?
|
||||
Yes, you can run Nestri Node in a virtualized environment, provided you passthrough your GPU to the virtual machine.
|
||||
Yes, you can run Nestri Node in a virtualized environment, provided you passthrough your GPU to the virtual machine. However, we do not recommend this setup as virtualization may introduce additional overhead and latency. For the best performance, we recommend running Nestri Node on bare-metal hardware.
|
||||
|
||||
## Can I run Nestri Node on Windows-based systems?
|
||||
No, the Nestri Node service does not support Windows-based systems. It can only be deployed on Linux-based systems.
|
||||
No, the Nestri Node service does not support Windows-based systems. It can only be deployed on Linux servers.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
title: Developer Notes and Tips
|
||||
description: This is a collection of developer notes for Nestri Node.
|
||||
icon: 'lucide:wrench'
|
||||
|
||||
---
|
||||
|
||||
### Construct The Nestri Runner Docker Image
|
||||
Checkout your branch with the latest version of nestri and build the image `<your-nestri-image>` within git root folder:
|
||||
```bash [build docker image command]
|
||||
podman build -t <your-nestri-image>:latest -f containers/Containerfile.runner .
|
||||
```
|
||||
|
||||
### Running other applications besides Steam
|
||||
When you followed the getting started guide, you already have a container running. You can get into your container to start your games or other applications:
|
||||
```bash [get into container command]
|
||||
podman exec -it nestri /bin/bash
|
||||
```
|
||||
|
||||
For most games that are not DRM free you need a launcher. In this case use the umu launcher:
|
||||
```bash [install umu and mangohud command]
|
||||
pacman -S umu-launcher
|
||||
```
|
||||
|
||||
You have to execute your game with the nestri user. If you have a linux game execute it like so:
|
||||
```bash [execute game command]
|
||||
su nestri
|
||||
source /etc/nestri/envs.sh
|
||||
GAMEID=0 PROTONPATH=GE-Proton mangohud umu-run <your-game.exe>
|
||||
```
|
||||
|
||||
You could also use other launchers like Lutris to run other games.
|
||||
|
||||
::alert{type="danger"}
|
||||
**Warning:** Running other applications besides Steam is not supported and may cause issues. We cannot provide support for this.
|
||||
::
|
||||
@@ -1,3 +1,2 @@
|
||||
title: Nestri Node
|
||||
navigation.redirect: /nestri-node/what-is-nestri-node
|
||||
icon: lucide:box
|
||||
title: 'Nestri Node'
|
||||
icon: heroicons-outline:bookmark-alt
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# What is Nestri Relay?
|
||||
|
||||
Nestri Relay is an essential component in the Nestri cloud-gaming ecosystem, responsible for transporting the video gameplay stream from your Nestri Node to the device you’re playing on. It is built on the moq-rs protocol, designed for efficient and smooth video transmission, ensuring a low-latency gaming experience.
|
||||
|
||||
By default, your Nestri Node will connect to the Nestri-hosted Relay, which we manage and is available for all users. This is the simplest and most straightforward option, requiring no additional configuration on your end.
|
||||
## ⚠️ Important Note
|
||||
|
||||
We recommend not installing Nestri Node on your primary PC if you only intend to use it over a weekend. This is because Nestri Node cannot run simultaneously with Xorg, the display server responsible for managing the graphical user interface (GUI). This means that while Nestri Node is running, you will not be able to use an attached screen. For this reason, Nestri Node is best set up on a dedicated machine that won’t be used for other tasks.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
title: What is Nestri Relay?
|
||||
description: This FAQ is made to address common questions about Nestri Node, the container which runs your games. Whether you're curious about compatibility, setup, or performance, you'll find answers to help you get started.
|
||||
icon: 'lucide:info'
|
||||
---
|
||||
|
||||
Nestri Relay is an essential component in the Nestri cloud-gaming ecosystem, responsible for taking the audio-video stream from your Nestri Node and further forwarding that to the device you’re playing on.
|
||||
It is built using WebRTC, for lowest latency streaming.
|
||||
@@ -1,37 +1,20 @@
|
||||
## Should I Self-Host a Nestri Relay?
|
||||
|
||||
If you want to use and enjoy the simplicity of the Nestri ecosystem, then you should not set up the Nestri Relay locally. Our free BYOG (Bring Your Own GPU) plan includes free shared relay access, which we highly recommend for those who want to start playing quickly on their own hardware without additional setup.
|
||||
|
||||
However, if you prefer to install and manage the Nestri Relay yourself, there are some important considerations to keep in mind.
|
||||
|
||||
### Important Considerations for Self-Hosting Nestri Relay
|
||||
|
||||
1. WebRTC and Firewall Issues
|
||||
* WebRTC, by default, attempts to access your public IP even if both the relay and Nestri Node are on the same local network.
|
||||
* This behavior can cause firewalls to block traffic, as the connection may attempt to access itself, resulting in connection failures.
|
||||
* Unordered Third
|
||||
2. Recommended Deployment Strategy
|
||||
* Instead of hosting the relay on your local network, we strongly recommend deploying the Nestri Relay on a VPS (Virtual Private Server) in the cloud.
|
||||
* Using a cloud-based VPS minimizes potential firewall conflicts and ensures a more stable connection between your Nestri Node and the relay.
|
||||
|
||||
If you're set on self-hosting despite the potential challenges, proceed with caution and ensure you have a proper understanding of firewall configurations and networking setups to mitigate connectivity issues.
|
||||
|
||||
## Self-hosted Nestri Relay
|
||||
|
||||
For those who prefer full control over the Nestri stack, it is possible to self-host the Nestri Relay. However, setting this up can be a bit complex, as it requires SSL Certificates for secure communication between your Nestri Node and your gaming devices. There are three main options:
|
||||
For those who prefer full control over their infrastructure, it is possible to self-host the Nestri Relay. However, setting this up can be a bit complex, as it requires generating SSL certificates for secure communication between your Nestri Node and your gaming devices. There are three main options:
|
||||
|
||||
- **Let's Encrypt Certificate**: This is the most common certificates for self-hosting and requires a domain name. You can generate a certificate using tools like **certbot** or **acme.sh**. Let's Encrypt provides free SSL certificates that are trusted by most browsers and are relatively straightforward to set up.
|
||||
- **Let's Encrypt Certificate**: This is the **recommended option** for self-hosting and requires a domain name. You can generate a certificate using tools like **certbot** or **acme.sh**. Let's Encrypt provides free SSL certificates that are trusted by most browsers and are relatively straightforward to set up.
|
||||
|
||||
- **Purchased SSL Certificate**: The **easiest option** for most users is to buy an SSL certificate from a trusted Certificate Authority (CA). This option eliminates much of the hassle involved with certificate generation and renewals, as these certificates are already trusted by browsers and don’t require as much manual setup.
|
||||
- **Purchased SSL Certificate**: The **easiest option** for most users is to buy an SSL certificate from a trusted Certificate Authority (CA). This option eliminates much of the hassle involved with certificate generation, as these certificates are already trusted by browsers and don’t require as much manual setup.
|
||||
|
||||
While self-hosting offers more flexibility, most users will find the **Nestri-hosted Relay** to be the easiest and most reliable option for getting started with cloud gaming on Nestri. This hosted relay is available to everyone using the BYOG package and requires no configuration.
|
||||
While self-hosting offers more flexibility, most users will find the **Nestri-hosted Relay** to be the easiest and most reliable option for getting started with cloud gaming on Nestri. This hosted relay is available to everyone and requires no configuration.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Server Requirements:**
|
||||
- Ensure **port 443** is open for both **TCP and UDP** (`:443/udp & :443/tcp`).
|
||||
- The server should have at least **6-8GB RAM** and **2 vCPUs**.
|
||||
- The server should have at least **4GB RAM** and **2 vCPUs**.
|
||||
- Supports both ARM or AMD64 architecture.
|
||||
|
||||
2. **Software Requirements:**
|
||||
@@ -40,127 +23,3 @@ While self-hosting offers more flexibility, most users will find the **Nestri-ho
|
||||
|
||||
3. **Certificates:**
|
||||
- You will need both private and public SSL certificates. It is recommended to use certificates from a **trusted Certificate Authority** (CA), either by using **Let's Encrypt** or purchasing a commercial SSL certificate, for secure communication. Avoid using self-signed certificates, as they can lead to compatibility issues and security warnings in browsers.
|
||||
|
||||
## Self-hosted Nestri Relay with an Reverse Proxy
|
||||
|
||||
### Caddy
|
||||
As caddy user you can use the following docker-compose.yml file:
|
||||
|
||||
```yaml [docker-compose.caddy.yml]
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: caddy
|
||||
ports:
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile # your caddyfile
|
||||
- ./cert:/etc/caddy/certs
|
||||
depends_on:
|
||||
- relay
|
||||
networks:
|
||||
- relay_network
|
||||
restart: unless-stopped
|
||||
|
||||
relay:
|
||||
#image: ghcr.io/nestrilabs/nestri/relay:nightly # Offical relay image
|
||||
image: ghcr.io/datcaptainhorse/nestri-relay:latest # Most current relay image
|
||||
container_name: relay
|
||||
environment:
|
||||
#- AUTO_ADD_LOCAL_IP=false # use with WEBRTC_NAT_IPS
|
||||
#- WEBRTC_NAT_IPS=1.2.3.4 # Add the LAN IP of your container here if connections fail
|
||||
- VERBOSE=true
|
||||
- DEBUG=true
|
||||
ports:
|
||||
- "8088:8088/udp"
|
||||
networks:
|
||||
- relay_network
|
||||
restart:
|
||||
unless-stopped
|
||||
networks:
|
||||
relay_network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
The Caddyfile should look like this:
|
||||
```caddyfile [Caddyfile]
|
||||
relay.example.com {
|
||||
@ws {
|
||||
header Connection Upgrade
|
||||
header Upgrade websocket
|
||||
}
|
||||
tls you@example.com
|
||||
reverse_proxy @ws relay:8088
|
||||
reverse_proxy relay:8088
|
||||
}
|
||||
```
|
||||
|
||||
Please modify it to your needs and replace the placeholder values with your own.
|
||||
You should also setup the Caddyfile to match your domain.
|
||||
|
||||
### Traefik
|
||||
As traefik user you can use the following docker-compose.yml file:
|
||||
|
||||
```yaml [docker-compose.relay.traefik.yml]
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.3"
|
||||
restart: always
|
||||
container_name: "traefik"
|
||||
networks:
|
||||
- traefik
|
||||
command:
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.network=traefik"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.to=web-secure"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||
- "--entrypoints.web-secure.address=:443"
|
||||
- "--certificatesresolvers.default.acme.tlschallenge=true"
|
||||
- "--certificatesresolvers.default.acme.email=foo@example.com" # Your email for tls challenge
|
||||
- "--certificatesresolvers.default.acme.storage=/letsencrypt/acme.json"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- "./letsencrypt:/letsencrypt" # Your letsencrypt folder for certificate persistence
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
restart:
|
||||
unless-stopped
|
||||
relay:
|
||||
#image: ghcr.io/nestrilabs/nestri/relay:nightly # Offical relay image
|
||||
image: ghcr.io/datcaptainhorse/nestri-relay:latest # Most current relay image
|
||||
container_name: relay
|
||||
environment:
|
||||
- AUTO_ADD_LOCAL_IP=false # Use with WEBRTC_NAT_IPS
|
||||
#- WEBRTC_NAT_IPS=1.2.3.4 # Add the LAN IP of your container here if connections fail
|
||||
- VERBOSE=true
|
||||
- DEBUG=true
|
||||
ports:
|
||||
- "8088:8088/udp"
|
||||
networks:
|
||||
- traefik
|
||||
restart:
|
||||
unless-stopped
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.relay.rule=Host(`relay.example.com`) # Your domain for tls challenge
|
||||
- traefik.http.routers.relay.tls=true
|
||||
- traefik.http.routers.relay.tls.certresolver=default
|
||||
- traefik.http.routers.relay.entrypoints=web-secure
|
||||
- traefik.http.services.relay.loadbalancer.server.port=8088
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
Please modify it to your needs and replace the placeholder values with your own.
|
||||
|
||||
### Where to find the relay compose files?
|
||||
|
||||
You will also find the relay compose files in our [github repository](https://github.com/nestrilabs/nestri/tree/main/containers).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
title: Container CLI
|
||||
description: Configure and manage your Nestri Relay environment using CLI parameters for WebRTC settings, STUN servers, local IP handling, and TLS options.
|
||||
icon: 'lucide:terminal'
|
||||
---
|
||||
|
||||
The Nestri Relay CLI provides configuration parameters to manage your relay environment. These options allow you to set values like `WebRTC ports`, `STUN servers`, and `TLS certificates`. Additionally, you can enable `verbose` mode and debugging for troubleshooting purposes. This documentation details each parameter to help you optimize your relay setup effectively.
|
||||
|
||||
## Parameters
|
||||
|
||||
| **Parameter** | **Type** | **Default** | **Description** |
|
||||
|----------------------------------|-----------|------------------------------------|------------------------------------------------------------------------------------------------------|
|
||||
| `-v, --verbose` | `boolean` | false | Shows more logs; useful for debugging issues. Recommended before reporting problems. |
|
||||
| `-d, --debug` | `boolean` | false | Enables debugging mode for additional logs and troubleshooting insights. |
|
||||
| `-p, --endpointPort` | `integer` | 8088 | Specifies the main port for the relay endpoint. |
|
||||
| **WebRTC Settings** | | | |
|
||||
| `--webrtcUDPStart` | `integer` | 10000 | Defines the starting UDP port for WebRTC connections. |
|
||||
| `--webrtcUDPEnd` | `integer` | 20000 | Defines the ending UDP port for WebRTC connections. |
|
||||
| `--webrtcUDPMux` | `integer` | 8088 | Specifies the WebRTC UDP multiplexing port. |
|
||||
| `--stunServer` | `string` | stun.l.google.com:19302 | Defines the STUN server address for NAT traversal. |
|
||||
| `--autoAddLocalIP` | `boolean` | true | Automatically adds local IP addresses to WebRTC candidates. |
|
||||
| `--WEBRTC_NAT_IPS` | `string` | "" | Comma-separated list of public IPs for WebRTC NAT traversal (e.g., `"192.168.0.1,192.168.0.2"`). |
|
||||
| **TLS Configuration** | | | |
|
||||
| `--tlsCert` | `string` | "" | Path to the TLS certificate file for secure connections. |
|
||||
| `--tlsKey` | `string` | "" | Path to the TLS private key file for secure connections. |
|
||||
69
apps/docs/content/3.nestri-relay/3.deploy-moq.md
Normal file
@@ -0,0 +1,69 @@
|
||||
## Installation Steps
|
||||
|
||||
### Step 1: Clone the Repository
|
||||
|
||||
Clone the `kixelated/moq-rs` repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kixelated/moq-rs moq
|
||||
```
|
||||
|
||||
### Step 2: Verify Port Availability
|
||||
|
||||
Check if port 443 is already in use on your server:
|
||||
|
||||
```bash
|
||||
sudo netstat -tulpn | grep ':443' | grep LISTEN
|
||||
```
|
||||
or
|
||||
```bash
|
||||
sudo lsof -i -P -n | grep LISTEN | grep 443
|
||||
```
|
||||
|
||||
If you find any processes using port 443, consider terminating them.
|
||||
|
||||
### Step 3: Configure Ports
|
||||
|
||||
Navigate to the cloned directory and edit the Docker compose file to use port 443:
|
||||
|
||||
```bash
|
||||
cd moq
|
||||
vim docker-compose.yml
|
||||
```
|
||||
|
||||
Change the ports section from lines 34 to 35 to:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
```
|
||||
|
||||
### Step 4: Prepare Certificates
|
||||
|
||||
Copy your generated certificates into the `moq/dev` directory and rename them:
|
||||
|
||||
```bash
|
||||
cp cert.pem moq/dev/localhost.crt
|
||||
cp key.pem moq/dev/localhost.key
|
||||
```
|
||||
|
||||
### Step 5: Start Docker Instances
|
||||
|
||||
Ensure you are in the root directory of the `moq` project, then start the Docker containers:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Step 6: Link Domain to Server IP
|
||||
|
||||
Configure your DNS settings to connect your server's IP address to your domain:
|
||||
|
||||
```
|
||||
Record Type: A
|
||||
Subdomain: relay.fst.so
|
||||
IP Address: xx.xxx.xx.xxx
|
||||
```
|
||||
|
||||
Congratulations, your MoQ server is now set up! You can verify its functionality by using the [MoQ Checker](https://nestri.pages.dev/moq/checker).
|
||||
42
apps/docs/content/3.nestri-relay/4.advanced-users.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# ⚠️ Advanced users
|
||||
|
||||
## Generating an SSL Certificate for Nestri Relay
|
||||
|
||||
This guide is for developers and advanced users who wish to self-host Nestri Relay. We strongly discourage this setup for general users due to its complexity, particularly when it comes to configuring SSL certificates correctly. Using a self-signed certificate or manually generating certificates can lead to issues with browser compatibility and security warnings, making it difficult to ensure a smooth experience.
|
||||
|
||||
For most users, we highly recommend using the **Nestri-hosted Relay**, which requires no manual setup and is ready to use out of the box.
|
||||
|
||||
---
|
||||
|
||||
## Generating an SSL Certificate Using Terraform
|
||||
|
||||
If you still wish to proceed with self-hosting, we recommend using Terraform to generate a valid SSL certificate. This method provides a secure, automated way to obtain the necessary certificates for Nestri Relay.
|
||||
|
||||
### Usage
|
||||
|
||||
1. **Update the `terraform.tfvars`** file with your domain and email.
|
||||
2. Run the following command to initialize the Terraform working directory:
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
```
|
||||
```bash
|
||||
terraform plan
|
||||
```
|
||||
```bash
|
||||
terraform apply
|
||||
```
|
||||
The configuration provides two sensitive outputs:
|
||||
```bash
|
||||
certificate_pem: The full certificate chain
|
||||
private_key_pem: The private key for the certificate
|
||||
```
|
||||
|
||||
These can be then be used in your `moq-relay` as it requires SSL/TLS certificates.
|
||||
|
||||
## Note
|
||||
The generated certificate and key files are saved locally and ignored by git:
|
||||
```git
|
||||
.terraform
|
||||
relay_*
|
||||
```
|
||||
4
apps/docs/content/3.nestri-relay/5.moq-tester.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## MOQ Tester
|
||||
Test your Nestri Relay, with our MOQ tester tool.
|
||||
|
||||
:button-link[Try MOQ Test Tool]{size="small" icon="IconStackBlitz" href="https://nestri.pages.dev/moq/checker" blank}
|
||||
@@ -1,3 +1,2 @@
|
||||
title: Nestri Relay
|
||||
navigation.redirect: /nestri-relay/what-is-nestri-relay
|
||||
icon: lucide:box
|
||||
title: 'Nestri Relay'
|
||||
icon: heroicons-outline:bookmark-alt
|
||||
|
||||
3
apps/docs/content/4.nestri-internal/1.what-is-this.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# What is this?
|
||||
|
||||
This is the part of the docs dedicated for the team working on Nestri
|
||||
27
apps/docs/content/4.nestri-internal/2.setup.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Setup
|
||||
|
||||
- Install bun [https://bun.sh/](https://bun.sh/)
|
||||
- Generate your Cloudflare token from [here](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22memberships%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22user_details%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_kv_storage%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_r2%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_routes%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_scripts%22%2C%22type%22%3A%22edit%22%7D%2C%7B%22key%22%3A%22workers_tail%22%2C%22type%22%3A%22read%22%7D%5D&name=sst&accountId=*&zoneId=all)
|
||||
- save it to a `.env` file like this
|
||||
```
|
||||
CLOUDFLARE_API_TOKEN=xxx
|
||||
```
|
||||
- Copy this to your `~/.aws/config` file
|
||||
```
|
||||
[sso-session nestri]
|
||||
sso_start_url = https://nestri.awsapps.com/start
|
||||
sso_region = us-east-1
|
||||
|
||||
[profile nestri-dev]
|
||||
sso_session = nestri
|
||||
sso_account_id = 535002871375
|
||||
sso_role_name = AdministratorAccess
|
||||
region = us-east-1
|
||||
|
||||
[profile nestri-production]
|
||||
sso_session = nestri
|
||||
sso_account_id = 209479283398
|
||||
sso_role_name = AdministratorAccess
|
||||
region = us-east-1
|
||||
```
|
||||
- You need to login once a day with `bun sso` in root
|
||||
2
apps/docs/content/4.nestri-internal/_dir.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
title: 'Nestri Internals'
|
||||
icon: heroicons-outline:bookmark-alt
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
title: Home
|
||||
navigation: false
|
||||
---
|
||||
|
||||
::hero
|
||||
---
|
||||
announcement:
|
||||
title: 'We are launching soon!'
|
||||
icon: '🎉'
|
||||
to: https://github.com/nestrilabs/nestri/releases/latest
|
||||
target: _blank
|
||||
actions:
|
||||
- name: Documentation
|
||||
to: /introduction/what-is-nestri
|
||||
- name: GitHub
|
||||
variant: outline
|
||||
to: https://github.com/nestrilabs/nestri
|
||||
leftIcon: 'lucide:github'
|
||||
---
|
||||
|
||||
#title
|
||||
Welcome to Nestri Docs
|
||||
|
||||
#description
|
||||
Play your favorite games on the go or with your friends on your own game cloud.
|
||||
::
|
||||
|
||||
::contributors
|
||||
::
|
||||
@@ -1,6 +1,14 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
// https://github.com/nuxt-themes/docus
|
||||
extends: ['@nuxt-themes/docus'],
|
||||
components: true,
|
||||
|
||||
|
||||
devtools: { enabled: true },
|
||||
extends: ['shadcn-docs-nuxt'],
|
||||
compatibilityDate: '2024-07-06',
|
||||
});
|
||||
|
||||
modules: [// Remove it if you don't use Plausible analytics
|
||||
// https://github.com/nuxt-modules/plausible
|
||||
'@nuxtjs/plausible', '@nuxt/ui'],
|
||||
|
||||
compatibilityDate: '2024-09-29'
|
||||
})
|
||||
14298
apps/docs/package-lock.json
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "shadcn-docs-nuxt-starter",
|
||||
"name": "docus-starter",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"nestri.dev": "nuxi dev",
|
||||
"build": "nuxi build --preset=cloudflare_pages",
|
||||
@@ -9,10 +9,14 @@
|
||||
"preview": "nuxi preview",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^3.15.4",
|
||||
"shadcn-docs-nuxt": "^0.8.14",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
"devDependencies": {
|
||||
"@nuxt-themes/docus": "latest",
|
||||
"@nuxt/devtools": "^2.3.2",
|
||||
"@nuxt/eslint-config": "^0.5.6",
|
||||
"@nuxt/ui": "^2.19.2",
|
||||
"@nuxtjs/plausible": "^1.0.2",
|
||||
"@types/node": "^20.16.5",
|
||||
"eslint": "^9.10.0",
|
||||
"nuxt": "^3.16.1"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 155 KiB |
BIN
apps/docs/public/cover.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
1
apps/docs/public/img/nestri-logo-sm.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 167 44" width="167" height="44"><defs><image width="47" height="36" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAkCAMAAAAuPpNdAAAAAXNSR0IB2cksfwAAAEJQTFRF/1gO/1MG/1MG/1UJ/08B/08B/14X/1oQ/1oQAAAA/18Z/1oR/1oR/1kP/1MH/1MH/2ck/2Me/2Me/24v/2sq/2sqUcSXkwAAABZ0Uk5T//+f//+fp6hpACsrG3BwRmdoQUpKLmJsmB4AAAA6SURBVHicY2RgZCABMDIyj6ofUPVsJKrnJEU5UAOp6rkGWfiMqqeuet5Blt4EBln4jKqnrnph0tQDAHjbARfH6mW/AAAAAElFTkSuQmCC"/></defs><style>.a{fill:#0a0a0a}</style><use href="#img1" x="6" y="4"/><path class="a" d="m69.2 34h-5.8v-24h7.9l8.5 16.3h0.1v-16.3h5.8v24h-7.4l-9-16.8h-0.1zm28.4 0.5q-2.8 0-4.6-0.7-1.8-0.8-3-2.2-1.1-1.3-1.6-3.1-0.5-1.7-0.5-3.6 0-2.1 0.5-4 0.6-1.9 1.7-3.3 1.1-1.5 2.8-2.4 1.8-0.8 4.3-0.8 2.5 0 4.2 0.8 1.8 0.9 2.8 2.4 1.1 1.5 1.4 3.5 0.3 2-0.1 4.3l-13.9 0.2v-3.1l9.4-0.2-0.8 1.8q0.3-1.6 0-2.8-0.3-1.1-1-1.7-0.7-0.6-2-0.6-1.4 0-2.1 0.7-0.8 0.7-1.2 1.9-0.3 1.3-0.3 3 0 2.9 1 4.2 1 1.4 3 1.4 0.9 0 1.4-0.2 0.6-0.2 1-0.6 0.3-0.5 0.5-1.1 0.1-0.6 0.1-1.3l5.4 0.2q0.1 1.2-0.3 2.5-0.3 1.3-1.3 2.4-0.9 1.1-2.6 1.8-1.7 0.6-4.2 0.6zm18.7 0q-1.9 0-3.6-0.3-1.6-0.4-2.8-1.3-1.2-0.8-1.9-2.2-0.6-1.4-0.5-3.4l5.1-0.4q0.1 1.2 0.6 2 0.4 0.7 1.3 1.1 0.8 0.4 2 0.4 1.1 0 1.9-0.4 0.9-0.4 0.9-1.3 0-0.5-0.3-0.8-0.3-0.3-1.1-0.5-0.8-0.3-2.3-0.7-1.8-0.5-3.3-0.9-1.5-0.5-2.6-1.2-1-0.6-1.5-1.6-0.6-0.9-0.6-2.4 0-2 1.1-3.3 1-1.4 2.9-2.2 1.9-0.7 4.4-0.7 2.2 0 4.1 0.7 2 0.6 3 2.2 1.2 1.6 0.9 4.1l-5 0.5q0.1-1-0.3-1.8-0.4-0.7-1.2-1.1-0.7-0.4-1.8-0.4-1.3 0-2 0.5-0.6 0.4-0.6 1.1 0 0.5 0.4 0.9 0.4 0.4 1.3 0.7 0.9 0.3 2.3 0.6 1.3 0.2 2.6 0.6 1.3 0.4 2.5 1.1 1.2 0.7 1.9 1.8 0.7 1.1 0.7 2.7 0 1.8-1 3.1-0.9 1.4-2.8 2.1-1.9 0.7-4.7 0.7zm18.1 0q-3.4 0-5.1-1.7-1.6-1.8-1.6-5.4v-8h-2.1v-3.7h0.1q2.2-0.2 3.2-1.4 0.9-1.3 1.1-3.7v-0.1h3.5v4.4h4.4v4.7h-4.4v7.2q0 1.4 0.6 2 0.7 0.6 1.7 0.6 0.5 0 1.1-0.2 0.5-0.1 1-0.4v5.2q-1.1 0.3-2 0.4-0.9 0.1-1.5 0.1zm11.5-0.5h-5.8v-10.4-8.7h5v7.6h0.3q0.2-3.1 0.9-4.8 0.6-1.8 1.6-2.5 0.9-0.8 2.1-0.8 0.7 0 1.4 0.2 0.7 0.2 1.4 0.6l-0.3 6.5q-0.8-0.4-1.6-0.7-0.7-0.2-1.4-0.2-1.2 0-2 0.6-0.8 0.7-1.2 2-0.4 1.2-0.4 3zm14.5 0h-5.8v-19.1h5.8zm-2.9-20.6q-1.7 0-2.6-0.6-0.9-0.8-0.9-2.1 0-1.4 0.9-2.1 0.9-0.7 2.6-0.7 1.7 0 2.6 0.7 0.9 0.7 0.9 2.1 0 1.3-0.9 2-0.9 0.7-2.6 0.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
apps/docs/public/img/nestri-logo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
1
apps/docs/public/img/nestri-logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1080" width="1080" height="1080"><defs><image width="308" height="234" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAATQAAADqCAMAAAAbHElGAAAAAXNSR0IB2cksfwAAADNQTFRF/5Bf/2wr/3tB/08B/4VQ/2Uh/9K//9K/AAAA/5xw/3tA/5Fg/2wr/5Zo/4NN/9K//9K/u/+9NwAAABF0Uk5T/f////H/HCsAVVWAgLjVOVVGDSqkAAACDElEQVR4nO3cQQrCAAxFwUaKKCK9/217hryNCDMnCG/7IXPMwdI8RFsTLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLZhTtLV5irY2r19f8IdEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAvmbSNYM+EFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBfMRbc2nvkC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0QLRAtEC0YL6GlTUTXiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaIFogWiBaMKdoa3OJtnYDtq8EmnzQ7p8AAAAASUVORK5CYII="/></defs><style>.a{fill:#0a0a0a}</style><use href="#img1" x="8" y="423"/><path class="a" d="m426 624h-38.4v-158.4h52.1l56.4 107.3h0.5v-107.3h38.4v158.4h-49l-59.5-110.9h-0.5zm187.4 3.4q-18 0-30.2-5.1-12-5.2-19.4-14.1-7.5-9.1-10.8-20.7-3.2-11.5-3.2-24.2 0-13.7 3.4-26.2 3.6-12.4 10.8-22 7.4-9.9 19-15.4 11.7-5.8 28-5.8 16.4 0 27.9 5.8 11.7 5.5 18.7 15.6 7 10.1 8.9 23.3 2.1 13.2-0.5 28.5l-91.9 1.5v-21.1l62.1-1.2-5 12.2q1.4-11-0.5-18.5-1.7-7.7-6.5-11.5-4.8-4.1-13.2-4.1-8.8 0-14.1 4.6-5.3 4.5-7.5 12.9-2.1 8.2-2.1 19.5 0 19.4 6.5 28.3 6.4 8.9 19.9 8.9 5.7 0 9.6-1.5 3.8-1.4 6.2-4 2.4-2.9 3.4-6.8 0.9-4 0.7-9.1l35.5 1.9q0.7 8-1.7 16.4-2.1 8.4-8.4 15.6-6.2 7.2-17.5 11.7-11 4.6-28.1 4.6zm124.1 0q-12.7 0-23.7-2.4-10.8-2.7-19-8.2-7.9-5.5-12.2-14.6-4.1-9.4-3.4-22.6l33.8-2.9q0.5 8 3.6 13.2 3.2 5.1 8.7 7.5 5.5 2.4 13.4 2.4 7.5 0 12.7-2.4 5.6-2.7 5.6-8.7 0-3.1-2-5-1.9-2.2-7.2-3.8-5-2-14.8-4.4-12.3-3.1-22.1-6.2-9.9-3.1-16.8-7.4-7-4.4-10.6-10.6-3.6-6.5-3.6-15.8 0-13.2 7-22.4 7.2-9.3 19.7-14.1 12.4-5.1 28.8-5.1 14.6 0 27.3 4.6 12.7 4.6 19.9 14.9 7.5 10.3 5.6 27.3l-33.2 3.2q0.8-7-1.9-11.8-2.6-5-7.7-7.4-5-2.7-12.2-2.7-8.2 0-12.7 3.1-4.3 2.9-4.3 7.5 0 3.6 2.6 6.2 2.9 2.4 8.6 4.3 6 1.7 15.4 3.9 8.2 1.7 17 4.3 8.9 2.6 16.6 7.2 7.7 4.3 12.5 11.5 4.8 7.2 4.8 18.3 0 11.7-6.5 20.6-6.5 8.9-19 13.7-12.4 4.8-30.7 4.8zm119.3-0.5q-22.3 0-33.4-11.3-10.8-11.5-10.8-35.5v-52.6h-13.9v-24.4h0.7q14.9-2 21.2-9.9 6.2-8.1 7.6-24.5v-0.4h22.8v29h29.1v31.2h-29.1v48q0 9.1 4.4 13 4.5 3.8 11 3.8 3.4 0 7-1 3.6-0.9 6.7-2.6v33.8q-7.5 2.2-13.2 2.9-5.8 0.5-10.1 0.5zm76.3-2.9h-38.6v-68.9-57.8h33.6v50.4h1.9q1.4-20.2 5.5-31.7 4.3-11.7 10.6-16.5 6.5-5.1 14.4-5.1 4.3 0 8.9 1.2 4.8 1.2 9.3 3.9l-1.9 43.2q-5.3-3.2-10.3-4.6-5.1-1.7-9.6-1.7-7.7 0-13 4.3-5.3 4.4-8.1 12.8-2.7 8.4-2.7 20.4zm96.3 0h-38.9v-126.8h38.9zm-19.7-135.8q-11.1 0-17.1-4.6-5.7-4.8-5.7-13.6 0-9.2 5.7-13.7 6-4.8 17.1-4.8 11.3 0 17 4.8 6 4.8 6 13.7 0 8.6-6 13.4-5.7 4.8-17 4.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 68 52" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><use id="Hintergrund" xlink:href="#_Image1" x="0" y="0" width="90px" height="69px" transform="matrix(0.992647,0,0,0.995192,0,0)"/><defs><image id="_Image1" width="68px" height="52px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAAA0CAYAAAAzMZ5zAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACxElEQVRoge1aTa7UMAz+HAxCIJBYIBachBuwgj0n5TIc4C3fSE9saPwW03Yc122TUWfaF/JJI02TL67j+qdNQv9+vn5EDxIAEIA6BMi5UbAAWup8kWAAH/dW4kgIwx9a9IT/B6wNEQQQWokSUWFSX8SAk+kTAMS9dDkEGNQBGEImgGA8pEIvWAKP1QQESAQICBDEGUsEt7UeXOY3JhMvg9D4k55Raw7mZGZOqRECSCVSUZwaKxNPHjXZCxs6OmjqS8DTlFDhUy8Bz1eRpZxikBM7slKutpBRIsvK68dMDCIJT87dnh4CxKE9S88VJbeQUSTLyOvHMASnRBYpA2vBS7pcy9dj5oqb7VuziX6RytVn6CeAIfR92mME5wgt4UuGplbeGMFrj39D/RsaGtZA8oPe763EkcAAHvZW4khgAM1DFGr/mi9GM4hBM4hBM4hBM4hBM4hBM4hBM4gBu63XrF6tjTka344ZVsyiN7B09eoWfBJ/IWkr/oxOHLxV93utmF2xonVrPj+8/fSUdJAAiL0wJSVxScf8ZLav1viQ1K1L+dC78lvwAVAEn968+zIV1PX/9b6Lyr/i5GKKN+YLgE413Jrf0NCQAfrz6+u3tGkl1oA+/g0kYHbfdxM+AXiVwbdj9f40meSt7k8dAAF//vv4+zLQCBC72T3sedgDEdRP0KtzOtvLHfj6vzHGWGm8Tfx4rjIfuid1CnHOO4K5GaZPcfLE+5vsws+pXlZWB5CAu+TNbOZ4w3h+1XsSlqcuNuVLAT93TlpWBCDgxHHEE0QY67PM3XS4jun1pnxy+Jo382mwOCcyPHEOzEwUyt3wjFaT/fjZc5rK8792r0bpbvGt+eXy2nqIAU/Lz7LV6jtVlsL7yrm/FgdCCxmDJKleXkl6L3FW00LuwbcXimUPqXzyHlrIGHCQcEqbnNfmqDylcqd5BirQM2zm+G7BAAAAAElFTkSuQmCC"/></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 242 B |
6
apps/docs/renovate.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["github>nuxt/renovate-config-nuxt"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import animate from 'tailwindcss-animate';
|
||||
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
safelist: ['dark'],
|
||||
prefix: '',
|
||||
content: [
|
||||
'./content/**/*',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
xl: 'calc(var(--radius) + 4px)',
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
'collapsible-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-collapsible-content-height)' },
|
||||
},
|
||||
'collapsible-up': {
|
||||
from: { height: 'var(--radix-collapsible-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'collapsible-down': 'collapsible-down 0.2s ease-in-out',
|
||||
'collapsible-up': 'collapsible-up 0.2s ease-in-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [animate],
|
||||
};
|
||||
218
apps/docs/tokens.config.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { defineTheme } from 'pinceau'
|
||||
|
||||
export default defineTheme({
|
||||
color: {
|
||||
black: '#0B0A0A',
|
||||
// Primary is modified lightblue
|
||||
primary: {
|
||||
50: '#fff6ec',
|
||||
100: '#ffebd3',
|
||||
200: '#ffd4a5',
|
||||
300: '#ffb56d',
|
||||
400: '#ff8a32',
|
||||
500: '#ff680a',
|
||||
600: '#ff4f01',
|
||||
700: '#cc3602',
|
||||
800: '#a12b0b',
|
||||
900: '#82260c'
|
||||
},
|
||||
gray: {
|
||||
50: '#FBFBFB',
|
||||
100: '#F6F5F4',
|
||||
200: '#ECEBE8',
|
||||
300: '#DBD9D3',
|
||||
400: '#ADA9A4',
|
||||
500: '#97948F',
|
||||
600: '#67635D',
|
||||
700: '#36332E',
|
||||
800: '#201E1B',
|
||||
900: '#121110'
|
||||
},
|
||||
red: {
|
||||
50: '#FFF9F8',
|
||||
100: '#FFF3F0',
|
||||
200: '#FFDED7',
|
||||
300: '#FFA692',
|
||||
400: '#FF7353',
|
||||
500: '#FF3B10',
|
||||
600: '#BB2402',
|
||||
700: '#701704',
|
||||
800: '#340A01',
|
||||
900: '#1C0301'
|
||||
},
|
||||
blue: {
|
||||
50: '#F2FAFF',
|
||||
100: '#DFF3FF',
|
||||
200: '#C6EAFF',
|
||||
300: '#A1DDFF',
|
||||
400: '#64C7FF',
|
||||
500: '#1AADFF',
|
||||
600: '#0069A6',
|
||||
700: '#014267',
|
||||
800: '#002235',
|
||||
900: '#00131D'
|
||||
},
|
||||
green: {
|
||||
50: '#ECFFF7',
|
||||
100: '#DEFFF1',
|
||||
200: '#C3FFE6',
|
||||
300: '#86FBCB',
|
||||
400: '#3CEEA5',
|
||||
500: '#0DD885',
|
||||
600: '#00B467',
|
||||
700: '#006037',
|
||||
800: '#002817',
|
||||
900: '#00190F'
|
||||
},
|
||||
yellow: {
|
||||
50: '#FFFCEE',
|
||||
100: '#FFF6D3',
|
||||
200: '#FFF0B1',
|
||||
300: '#FFE372',
|
||||
400: '#FFDC4E',
|
||||
500: '#FBCA05',
|
||||
600: '#CBA408',
|
||||
700: '#614E02',
|
||||
800: '#292100',
|
||||
900: '#1B1500'
|
||||
},
|
||||
shadow: {
|
||||
initial: '{color.gray.400}',
|
||||
dark: '{color.gray.800}'
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
xs: '0px 1px 2px 0px {color.shadow}',
|
||||
sm: '0px 1px 3px 0px {color.shadow}, 0px 1px 2px -1px {color.shadow}',
|
||||
md: '0px 4px 6px -1px {color.shadow}, 0px 2px 4px -2px {color.shadow}',
|
||||
lg: '0px 10px 15px -3px {color.shadow}, 0px 4px 6px -4px {color.shadow}',
|
||||
xl: '0px 20px 25px -5px {color.shadow}, 0px 8px 10px -6px {color.shadow}',
|
||||
'2xl': '0px 25px 50px -12px {color.shadow}',
|
||||
none: '0px 0px 0px 0px transparent'
|
||||
},
|
||||
docus: {
|
||||
$schema: {
|
||||
title: 'All the configurable tokens from Docus.',
|
||||
tags: [
|
||||
'@studioIcon material-symbols:docs'
|
||||
]
|
||||
},
|
||||
body: {
|
||||
backgroundColor: {
|
||||
initial: '{color.white}',
|
||||
dark: '{color.black}'
|
||||
},
|
||||
color: {
|
||||
initial: '{color.gray.800}',
|
||||
dark: '{color.gray.200}'
|
||||
},
|
||||
fontFamily: '{font.sans}'
|
||||
},
|
||||
header: {
|
||||
height: '64px',
|
||||
logo: {
|
||||
height: {
|
||||
initial: '{space.6}',
|
||||
sm: '{space.7}'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
fontSize: '{fontSize.2xl}',
|
||||
fontWeight: '{fontWeight.bold}',
|
||||
color: {
|
||||
static: {
|
||||
initial: '{color.gray.900}',
|
||||
dark: '{color.gray.100}',
|
||||
},
|
||||
hover: '{color.primary.500}',
|
||||
}
|
||||
}
|
||||
},
|
||||
footer: { height: { initial: '145px', sm: '100px' }, padding: '{space.4} 0' },
|
||||
readableLine: '78ch',
|
||||
loadingBar: {
|
||||
height: '3px',
|
||||
gradientColorStop1: '#00dc82',
|
||||
gradientColorStop2: '#34cdfe',
|
||||
gradientColorStop3: '#0047e1'
|
||||
},
|
||||
search: {
|
||||
backdropFilter: 'blur(24px)',
|
||||
input: {
|
||||
borderRadius: '{radii.2xs}',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: {
|
||||
initial: '{color.gray.200}',
|
||||
dark: 'transparent'
|
||||
},
|
||||
fontSize: '{fontSize.sm}',
|
||||
gap: '{space.2}',
|
||||
padding: '{space.2} {space.4}',
|
||||
backgroundColor: {
|
||||
initial: '{color.gray.200}',
|
||||
dark: '{color.gray.800}'
|
||||
},
|
||||
},
|
||||
results: {
|
||||
window: {
|
||||
marginX: {
|
||||
initial: '0',
|
||||
sm: '{space.4}'
|
||||
},
|
||||
borderRadius: {
|
||||
initial: 'none',
|
||||
sm: '{radii.xs}'
|
||||
},
|
||||
marginTop: {
|
||||
initial: '0',
|
||||
sm: '20vh'
|
||||
},
|
||||
maxWidth: '640px',
|
||||
maxHeight: {
|
||||
initial: '100%',
|
||||
sm: '320px'
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
backgroundColor: {
|
||||
initial: '{color.gray.300}',
|
||||
dark: '{color.gray.700}'
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
color: 'white',
|
||||
backgroundColor: '{color.primary.500}'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
typography: {
|
||||
color: {
|
||||
primary: {
|
||||
50: '{color.primary.50}',
|
||||
100: '{color.primary.100}',
|
||||
200: '{color.primary.200}',
|
||||
300: '{color.primary.300}',
|
||||
400: '{color.primary.400}',
|
||||
500: '{color.primary.500}',
|
||||
600: '{color.primary.600}',
|
||||
700: '{color.primary.700}',
|
||||
800: '{color.primary.800}',
|
||||
900: '{color.primary.900}'
|
||||
},
|
||||
secondary: {
|
||||
50: '{color.gray.50}',
|
||||
100: '{color.gray.100}',
|
||||
200: '{color.gray.200}',
|
||||
300: '{color.gray.300}',
|
||||
400: '{color.gray.400}',
|
||||
500: '{color.gray.500}',
|
||||
600: '{color.gray.600}',
|
||||
700: '{color.gray.700}',
|
||||
800: '{color.gray.800}',
|
||||
900: '{color.gray.900}'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
// https://v3.nuxtjs.org/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
// "extends": "./.nuxt/tsconfig.json",
|
||||
"ignoreConfigErrors": true
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"typescript": "5.4.5",
|
||||
"undici": "*",
|
||||
"valibot": "^0.42.1",
|
||||
"vite": "6.1.6",
|
||||
"vite": "6.0.15",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"wrangler": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -72,7 +72,7 @@ export default component$(() => {
|
||||
});
|
||||
|
||||
const lockPlay = $(async () => {
|
||||
if (!canvas.value || !playState.hasStream || playState.nestriLock) return;
|
||||
if (!canvas.value || !playState.hasStream) return;
|
||||
|
||||
try {
|
||||
await canvas.value.requestPointerLock();
|
||||
@@ -156,22 +156,18 @@ export default component$(() => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => canvas.value);
|
||||
if (!canvas.value) return; // Ensure canvas is available
|
||||
// Get query parameter "peerURL" from the URL
|
||||
let peerURL = new URLSearchParams(window.location.search).get("peerURL");
|
||||
if (!peerURL || peerURL.length <= 0) {
|
||||
peerURL = "/dnsaddr/relay.dathorse.com/p2p/12D3KooWPK4v5wKYNYx9oXWjqLM8Xix6nm13o91j1Feqq98fLBsw";
|
||||
}
|
||||
|
||||
setupPointerLockListener();
|
||||
try {
|
||||
if (!playState.video) {
|
||||
playState.video = document.createElement("video") as HTMLVideoElement;
|
||||
playState.video = document.createElement("video") as HTMLVideoElement
|
||||
playState.video.style.visibility = "hidden";
|
||||
playState.webrtc = noSerialize(new WebRTCStream(peerURL, id, async (mediaStream) => {
|
||||
playState.webrtc = noSerialize(new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => {
|
||||
if (playState.video && mediaStream && playState.video.srcObject === null) {
|
||||
console.log("Setting mediastream");
|
||||
playState.video.srcObject = mediaStream;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
relay.example.com {
|
||||
@ws {
|
||||
header Connection Upgrade
|
||||
header Upgrade websocket
|
||||
}
|
||||
tls you@example.com
|
||||
reverse_proxy @ws relay:8088
|
||||
reverse_proxy relay:8088
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: caddy
|
||||
ports:
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile # your caddyfile
|
||||
- ./cert:/etc/caddy/certs
|
||||
depends_on:
|
||||
- relay
|
||||
networks:
|
||||
- relay_network
|
||||
restart: unless-stopped
|
||||
|
||||
relay:
|
||||
#image: ghcr.io/nestrilabs/nestri/relay:nightly # Offical relay image
|
||||
image: ghcr.io/datcaptainhorse/nestri-relay:latest # Most current relay image
|
||||
container_name: relay
|
||||
environment:
|
||||
#- AUTO_ADD_LOCAL_IP=false # use with WEBRTC_NAT_IPS
|
||||
#- WEBRTC_NAT_IPS=1.2.3.4 # Add the LAN IP of your container here if connections fail
|
||||
- VERBOSE=true
|
||||
- DEBUG=true
|
||||
ports:
|
||||
- "8088:8088/udp"
|
||||
networks:
|
||||
- relay_network
|
||||
restart:
|
||||
unless-stopped
|
||||
networks:
|
||||
relay_network:
|
||||
driver: bridge
|
||||
@@ -1,52 +0,0 @@
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.3"
|
||||
restart: always
|
||||
container_name: "traefik"
|
||||
networks:
|
||||
- traefik
|
||||
command:
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.network=traefik"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.to=web-secure"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||
- "--entrypoints.web-secure.address=:443"
|
||||
- "--certificatesresolvers.default.acme.tlschallenge=true"
|
||||
- "--certificatesresolvers.default.acme.email=foo@example.com" # Your email for tls challenge
|
||||
- "--certificatesresolvers.default.acme.storage=/letsencrypt/acme.json"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- "./letsencrypt:/letsencrypt" # Your letsencrypt folder for certificate persistence
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
restart:
|
||||
unless-stopped
|
||||
relay:
|
||||
#image: ghcr.io/nestrilabs/nestri/relay:nightly # Offical relay image
|
||||
image: ghcr.io/datcaptainhorse/nestri-relay:latest # Most current relay image
|
||||
container_name: relay
|
||||
environment:
|
||||
- AUTO_ADD_LOCAL_IP=false # Use with WEBRTC_NAT_IPS
|
||||
#- WEBRTC_NAT_IPS=1.2.3.4 # Add the LAN IP of your container here if connections fail
|
||||
- VERBOSE=true
|
||||
- DEBUG=true
|
||||
ports:
|
||||
- "8088:8088/udp"
|
||||
networks:
|
||||
- traefik
|
||||
restart:
|
||||
unless-stopped
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.relay.rule=Host(`relay.example.com`) # Your domain for tls challenge
|
||||
- traefik.http.routers.relay.tls=true
|
||||
- traefik.http.routers.relay.tls.certresolver=default
|
||||
- traefik.http.routers.relay.entrypoints=web-secure
|
||||
- traefik.http.services.relay.loadbalancer.server.port=8088
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
@@ -10,19 +10,21 @@ WORKDIR /relay
|
||||
# TODO: Switch running layer to just alpine (doesn't need golang dev stack)
|
||||
|
||||
# ENV flags
|
||||
ENV REGEN_IDENTITY=false
|
||||
ENV VERBOSE=false
|
||||
ENV DEBUG=false
|
||||
ENV ENDPOINT_PORT=8088
|
||||
ENV WEBRTC_UDP_START=0
|
||||
ENV WEBRTC_UDP_END=0
|
||||
ENV MESH_PORT=8089
|
||||
ENV WEBRTC_UDP_START=10000
|
||||
ENV WEBRTC_UDP_END=20000
|
||||
ENV STUN_SERVER="stun.l.google.com:19302"
|
||||
ENV WEBRTC_UDP_MUX=8088
|
||||
ENV WEBRTC_NAT_IPS=""
|
||||
ENV AUTO_ADD_LOCAL_IP=true
|
||||
ENV PERSIST_DIR="./persist-data"
|
||||
ENV TLS_CERT=""
|
||||
ENV TLS_KEY=""
|
||||
|
||||
EXPOSE $ENDPOINT_PORT
|
||||
EXPOSE $MESH_PORT
|
||||
EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp
|
||||
EXPOSE $WEBRTC_UDP_MUX/udp
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 -b "dev-dmabuf" https://github.com/DatCaptainHorse/gst-wayland-display.git
|
||||
RUN git clone -b dev-dmabuf https://github.com/DatCaptainHorse/gst-wayland-display.git
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM gst-wayland-deps AS gst-wayland-planner
|
||||
@@ -135,12 +135,11 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
|
||||
vulkan-radeon lib32-vulkan-radeon \
|
||||
mesa \
|
||||
steam steam-native-runtime proton-cachyos gtk3 lib32-gtk3 \
|
||||
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
|
||||
steam steam-native-runtime gtk3 lib32-gtk3 \
|
||||
sudo xorg-xwayland seatd libinput gamescope mangohud \
|
||||
libssh2 curl wget \
|
||||
pipewire pipewire-pulse pipewire-alsa wireplumber \
|
||||
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib \
|
||||
openssh && \
|
||||
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib && \
|
||||
# GStreamer stack
|
||||
pacman -Sy --needed --noconfirm \
|
||||
gstreamer gst-plugins-base gst-plugins-good \
|
||||
@@ -154,6 +153,14 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
paccache -rk1 && \
|
||||
rm -rf /usr/share/{info,man,doc}/*
|
||||
|
||||
### Application Installation ###
|
||||
ARG LUDUSAVI_VERSION="0.28.0"
|
||||
RUN curl -fsSL -o ludusavi.tar.gz \
|
||||
"https://github.com/mtkennerly/ludusavi/releases/download/v${LUDUSAVI_VERSION}/ludusavi-v${LUDUSAVI_VERSION}-linux.tar.gz" && \
|
||||
tar -xzvf ludusavi.tar.gz && \
|
||||
mv ludusavi /usr/bin/ && \
|
||||
rm ludusavi.tar.gz
|
||||
|
||||
### User Configuration ###
|
||||
ENV USER="nestri" \
|
||||
UID=1000 \
|
||||
|
||||
93
infra/api.ts
@@ -1 +1,94 @@
|
||||
import { bus } from "./bus";
|
||||
import { auth } from "./auth";
|
||||
import { domain } from "./dns";
|
||||
import { secret } from "./secret";
|
||||
import { cluster } from "./cluster";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
export const apiService = new sst.aws.Service("Api", {
|
||||
cluster,
|
||||
cpu: $app.stage === "production" ? "2 vCPU" : undefined,
|
||||
memory: $app.stage === "production" ? "4 GB" : undefined,
|
||||
link: [
|
||||
bus,
|
||||
auth,
|
||||
postgres,
|
||||
secret.SteamApiKey,
|
||||
secret.PolarSecret,
|
||||
secret.PolarWebhookSecret,
|
||||
secret.NestriFamilyMonthly,
|
||||
secret.NestriFamilyYearly,
|
||||
secret.NestriFreeMonthly,
|
||||
secret.NestriProMonthly,
|
||||
secret.NestriProYearly,
|
||||
],
|
||||
command: ["bun", "run", "./src/api/index.ts"],
|
||||
image: {
|
||||
dockerfile: "packages/functions/Containerfile",
|
||||
},
|
||||
loadBalancer: {
|
||||
rules: [
|
||||
{
|
||||
listen: "80/http",
|
||||
forward: "3001/http",
|
||||
},
|
||||
],
|
||||
},
|
||||
dev: {
|
||||
url: "http://localhost:3001",
|
||||
command: "bun dev:api",
|
||||
directory: "packages/functions",
|
||||
},
|
||||
scaling:
|
||||
$app.stage === "production"
|
||||
? {
|
||||
min: 2,
|
||||
max: 10,
|
||||
}
|
||||
: undefined,
|
||||
// For persisting actor state
|
||||
transform: {
|
||||
taskDefinition: (args) => {
|
||||
const volumes = $output(args.volumes).apply(v => {
|
||||
const next = [...(v || []), {
|
||||
name: "shared-tmp",
|
||||
dockerVolumeConfiguration: {
|
||||
scope: "shared",
|
||||
driver: "local"
|
||||
}
|
||||
}];
|
||||
|
||||
return next;
|
||||
})
|
||||
|
||||
// "containerDefinitions" is a JSON string, parse first
|
||||
let containers = $jsonParse(args.containerDefinitions);
|
||||
|
||||
containers = containers.apply((containerDefinitions) => {
|
||||
containerDefinitions[0].mountPoints = [
|
||||
...(containerDefinitions[0].mountPoints ?? []),
|
||||
{
|
||||
sourceVolume: "shared-tmp",
|
||||
containerPath: "/tmp"
|
||||
},
|
||||
]
|
||||
return containerDefinitions;
|
||||
});
|
||||
|
||||
args.volumes = volumes
|
||||
args.containerDefinitions = $jsonStringify(containers);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const api = !$dev ? new sst.aws.Router("ApiRoute", {
|
||||
routes: {
|
||||
// I think api.url should work all the same
|
||||
"/*": apiService.nodes.loadBalancer.dnsName,
|
||||
},
|
||||
domain: {
|
||||
name: "api." + domain,
|
||||
dns: sst.cloudflare.dns(),
|
||||
},
|
||||
}) : apiService
|
||||
98
infra/auth.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { bus } from "./bus";
|
||||
import { domain } from "./dns";
|
||||
import { secret } from "./secret";
|
||||
import { cluster } from "./cluster";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
export const authService = new sst.aws.Service("Auth", {
|
||||
cluster,
|
||||
cpu: $app.stage === "production" ? "1 vCPU" : undefined,
|
||||
memory: $app.stage === "production" ? "2 GB" : undefined,
|
||||
command: ["bun", "run", "./src/auth/index.ts"],
|
||||
link: [
|
||||
bus,
|
||||
postgres,
|
||||
secret.PolarSecret,
|
||||
secret.GithubClientID,
|
||||
secret.DiscordClientID,
|
||||
secret.GithubClientSecret,
|
||||
secret.DiscordClientSecret,
|
||||
],
|
||||
image: {
|
||||
dockerfile: "packages/functions/Containerfile",
|
||||
},
|
||||
environment: {
|
||||
NO_COLOR: "1",
|
||||
STORAGE: "/tmp/persist.json"
|
||||
},
|
||||
loadBalancer: {
|
||||
rules: [
|
||||
{
|
||||
listen: "80/http",
|
||||
forward: "3002/http",
|
||||
},
|
||||
],
|
||||
},
|
||||
permissions: [
|
||||
{
|
||||
actions: ["ses:SendEmail"],
|
||||
resources: ["*"],
|
||||
},
|
||||
],
|
||||
dev: {
|
||||
command: "bun dev:auth",
|
||||
directory: "packages/functions",
|
||||
url: "http://localhost:3002",
|
||||
},
|
||||
scaling:
|
||||
$app.stage === "production"
|
||||
? {
|
||||
min: 2,
|
||||
max: 10,
|
||||
}
|
||||
: undefined,
|
||||
//For temporarily persisting the persist.json
|
||||
transform: {
|
||||
taskDefinition: (args) => {
|
||||
const volumes = $output(args.volumes).apply(v => {
|
||||
const next = [...(v || []), {
|
||||
name: "shared-tmp",
|
||||
dockerVolumeConfiguration: {
|
||||
scope: "shared",
|
||||
driver: "local"
|
||||
}
|
||||
}];
|
||||
|
||||
return next;
|
||||
})
|
||||
|
||||
// "containerDefinitions" is a JSON string, parse first
|
||||
let containers = $jsonParse(args.containerDefinitions);
|
||||
|
||||
containers = containers.apply((containerDefinitions) => {
|
||||
containerDefinitions[0].mountPoints = [
|
||||
...(containerDefinitions[0].mountPoints ?? []),
|
||||
{
|
||||
sourceVolume: "shared-tmp",
|
||||
containerPath: "/tmp"
|
||||
}
|
||||
]
|
||||
return containerDefinitions;
|
||||
});
|
||||
|
||||
args.volumes = volumes
|
||||
args.containerDefinitions = $jsonStringify(containers);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const auth = !$dev ? new sst.aws.Router("AuthRoute", {
|
||||
routes: {
|
||||
// I think auth.url should work all the same
|
||||
"/*": authService.nodes.loadBalancer.dnsName,
|
||||
},
|
||||
domain: {
|
||||
name: "auth." + domain,
|
||||
dns: sst.cloudflare.dns(),
|
||||
},
|
||||
}) : authService
|
||||
70
infra/bus.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { vpc } from "./vpc";
|
||||
import { secret } from "./secret";
|
||||
import { storage } from "./storage";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
export const dlq = new sst.aws.Queue("Dlq");
|
||||
|
||||
export const retryQueue = new sst.aws.Queue("RetryQueue");
|
||||
|
||||
export const bus = new sst.aws.Bus("Bus");
|
||||
|
||||
export const eventSub = bus.subscribe("Event", {
|
||||
vpc,
|
||||
handler: "packages/functions/src/events/index.handler",
|
||||
link: [
|
||||
// email,
|
||||
bus,
|
||||
storage,
|
||||
postgres,
|
||||
retryQueue,
|
||||
secret.PolarSecret,
|
||||
secret.SteamApiKey
|
||||
],
|
||||
environment: {
|
||||
RETRIES: "2",
|
||||
},
|
||||
memory: "3002 MB",// For faster processing of large(r) images
|
||||
timeout: "10 minutes",
|
||||
});
|
||||
|
||||
new aws.lambda.FunctionEventInvokeConfig("EventConfig", {
|
||||
functionName: $resolve([eventSub.nodes.function.name]).apply(
|
||||
([name]) => name,
|
||||
),
|
||||
maximumRetryAttempts: 1,
|
||||
destinationConfig: {
|
||||
onFailure: {
|
||||
destination: retryQueue.arn,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
retryQueue.subscribe({
|
||||
vpc,
|
||||
handler: "packages/functions/src/queues/retry.handler",
|
||||
timeout: "30 seconds",
|
||||
environment: {
|
||||
RETRIER_QUEUE_URL: retryQueue.url,
|
||||
},
|
||||
link: [
|
||||
dlq,
|
||||
retryQueue,
|
||||
eventSub.nodes.function,
|
||||
],
|
||||
permissions: [
|
||||
{
|
||||
actions: ["lambda:GetFunction", "lambda:InvokeFunction"],
|
||||
resources: [
|
||||
$interpolate`arn:aws:lambda:${aws.getRegionOutput().name}:${aws.getCallerIdentityOutput().accountId}:function:*`,
|
||||
],
|
||||
},
|
||||
],
|
||||
transform: {
|
||||
function: {
|
||||
deadLetterConfig: {
|
||||
targetArn: dlq.arn,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
6
infra/cluster.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { vpc } from "./vpc";
|
||||
|
||||
export const cluster = new sst.aws.Cluster("Cluster", {
|
||||
vpc,
|
||||
forceUpgrade: "v2"
|
||||
});
|
||||
14
infra/dns.ts
@@ -1,5 +1,9 @@
|
||||
export const domain = (() => {
|
||||
if ($app.stage === "production") return "nestri.io"
|
||||
if ($app.stage === "dev") return "nestri.io"
|
||||
return `${$app.stage}.dev.nestri.io`
|
||||
})()
|
||||
export const domain =
|
||||
{
|
||||
production: "nestri.io",
|
||||
dev: "dev.nestri.io",
|
||||
}[$app.stage] || $app.stage + ".dev.nestri.io";
|
||||
|
||||
export const zone = cloudflare.getZoneOutput({
|
||||
name: "nestri.io",
|
||||
});
|
||||
6
infra/email.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { domain } from "./dns";
|
||||
|
||||
export const email = new sst.aws.Email("Email",{
|
||||
sender: domain,
|
||||
dns: sst.cloudflare.dns(),
|
||||
})
|
||||
57
infra/images.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { domain } from "./dns";
|
||||
import { storage } from "./storage";
|
||||
|
||||
sst.Linkable.wrap(aws.iam.AccessKey, (resource) => ({
|
||||
properties: {
|
||||
key: resource.id,
|
||||
secret: resource.secret,
|
||||
},
|
||||
}))
|
||||
|
||||
const cache = new sst.cloudflare.Kv("ImageCache");
|
||||
|
||||
const bucket = new sst.cloudflare.Bucket("ImageBucket");
|
||||
|
||||
const lambdaInvokerUser = new aws.iam.User("ImageIAMUser", {
|
||||
name: `${$app.name}-${$app.stage}-ImageIAMUser`,
|
||||
forceDestroy: true
|
||||
});
|
||||
|
||||
const imageProcessorFunction = new sst.aws.Function("ImageProcessor",
|
||||
{
|
||||
memory: "1024 MB",
|
||||
link: [storage],
|
||||
timeout: "30 seconds",
|
||||
nodejs: { install: ["sharp"] },
|
||||
handler: "packages/functions/src/images/processor.handler",
|
||||
},
|
||||
);
|
||||
|
||||
new aws.iam.UserPolicy("InvokeLambdaPolicy", {
|
||||
user: lambdaInvokerUser.name,
|
||||
policy: $output({
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
Effect: "Allow",
|
||||
Action: ["lambda:InvokeFunction"],
|
||||
Resource: imageProcessorFunction.arn,
|
||||
},
|
||||
],
|
||||
}).apply(JSON.stringify),
|
||||
});
|
||||
|
||||
const accessKey = new aws.iam.AccessKey("ImageInvokerAccessKey", {
|
||||
user: lambdaInvokerUser.name,
|
||||
});
|
||||
|
||||
export const imageCdn = new sst.cloudflare.Worker("ImageCDN", {
|
||||
url: true,
|
||||
domain: "cdn." + domain,
|
||||
link: [bucket, cache, imageProcessorFunction, accessKey],
|
||||
handler: "packages/functions/src/images/index.ts",
|
||||
});
|
||||
|
||||
export const outputs = {
|
||||
cdn: imageCdn.url
|
||||
}
|
||||
64
infra/postgres.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { vpc } from "./vpc";
|
||||
|
||||
export const postgres = new sst.aws.Aurora("Database", {
|
||||
vpc,
|
||||
engine: "postgres",
|
||||
scaling: {
|
||||
min: "0 ACU",
|
||||
max: "1 ACU",
|
||||
},
|
||||
transform: {
|
||||
clusterParameterGroup: {
|
||||
parameters: [
|
||||
{
|
||||
name: "rds.logical_replication",
|
||||
value: "1",
|
||||
applyMethod: "pending-reboot",
|
||||
},
|
||||
{
|
||||
name: "max_slot_wal_keep_size",
|
||||
value: "10240",
|
||||
applyMethod: "pending-reboot",
|
||||
},
|
||||
{
|
||||
name: "rds.force_ssl",
|
||||
value: "0",
|
||||
applyMethod: "pending-reboot",
|
||||
},
|
||||
{
|
||||
name: "max_connections",
|
||||
value: "1000",
|
||||
applyMethod: "pending-reboot",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
new sst.x.DevCommand("Studio", {
|
||||
link: [postgres],
|
||||
dev: {
|
||||
command: "bun db:dev studio",
|
||||
directory: "packages/core",
|
||||
autostart: true,
|
||||
},
|
||||
});
|
||||
|
||||
// const migrator = new sst.aws.Function("DatabaseMigrator", {
|
||||
// handler: "packages/functions/src/migrator.handler",
|
||||
// link: [postgres],
|
||||
// copyFiles: [
|
||||
// {
|
||||
// from: "packages/core/migrations",
|
||||
// to: "./migrations",
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
|
||||
// if (!$dev) {
|
||||
// new aws.lambda.Invocation("DatabaseMigratorInvocation", {
|
||||
// input: Date.now().toString(),
|
||||
// functionName: migrator.name,
|
||||
// });
|
||||
// }
|
||||
9
infra/realtime.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { auth } from "./auth";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
export const device = new sst.aws.Realtime("Realtime", {
|
||||
authorizer: {
|
||||
link: [auth, postgres],
|
||||
handler: "packages/functions/src/realtime/authorizer.handler"
|
||||
}
|
||||
})
|
||||
18
infra/secret.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const secret = {
|
||||
PolarSecret: new sst.Secret("PolarSecret", process.env.POLAR_API_KEY),
|
||||
SteamApiKey: new sst.Secret("SteamApiKey"),
|
||||
GithubClientID: new sst.Secret("GithubClientID"),
|
||||
DiscordClientID: new sst.Secret("DiscordClientID"),
|
||||
PolarWebhookSecret: new sst.Secret("PolarWebhookSecret"),
|
||||
GithubClientSecret: new sst.Secret("GithubClientSecret"),
|
||||
DiscordClientSecret: new sst.Secret("DiscordClientSecret"),
|
||||
|
||||
// Pricing
|
||||
NestriFreeMonthly: new sst.Secret("NestriFreeMonthly"),
|
||||
NestriProMonthly: new sst.Secret("NestriProMonthly"),
|
||||
NestriProYearly: new sst.Secret("NestriProYearly"),
|
||||
NestriFamilyMonthly: new sst.Secret("NestriFamilyMonthly"),
|
||||
NestriFamilyYearly: new sst.Secret("NestriFamilyYearly"),
|
||||
};
|
||||
|
||||
export const allSecrets = Object.values(secret);
|
||||
2
infra/stage.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const isPermanentStage =
|
||||
$app.stage === "production" || $app.stage === "dev";
|
||||
1
infra/storage.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const storage = new sst.aws.Bucket("Storage");
|
||||
11
infra/vpc.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { isPermanentStage } from "./stage";
|
||||
|
||||
export const vpc = isPermanentStage
|
||||
? new sst.aws.Vpc("VPC", {
|
||||
az: 2,
|
||||
// For lambdas to work in this VPC
|
||||
nat: "ec2",
|
||||
// For SST tunnel to work
|
||||
bastion: true,
|
||||
})
|
||||
: sst.aws.Vpc.get("VPC", "vpc-0beb1cdc21a725748");
|
||||
22
infra/www.ts
@@ -1,9 +1,23 @@
|
||||
// This is the website part where people play and connect
|
||||
import { api } from "./api";
|
||||
import { auth } from "./auth";
|
||||
import { zero } from "./zero";
|
||||
import { domain } from "./dns";
|
||||
|
||||
new sst.cloudflare.x.Astro("Web", {
|
||||
domain,
|
||||
path: "packages/web",
|
||||
new sst.aws.StaticSite("Web", {
|
||||
path: "packages/www",
|
||||
build: {
|
||||
output: "./dist",
|
||||
command: "bun run build",
|
||||
},
|
||||
domain: {
|
||||
dns: sst.cloudflare.dns(),
|
||||
name: "console." + domain
|
||||
},
|
||||
environment: {
|
||||
SST_STAGE: $app.stage,
|
||||
VITE_API_URL: api.url,
|
||||
VITE_STAGE: $app.stage,
|
||||
VITE_AUTH_URL: auth.url,
|
||||
VITE_ZERO_URL: zero.url,
|
||||
},
|
||||
})
|
||||
197
infra/zero.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { vpc } from "./vpc";
|
||||
import { auth } from "./auth";
|
||||
import { domain } from "./dns";
|
||||
import { readFileSync } from "fs";
|
||||
import { cluster } from "./cluster";
|
||||
import { storage } from "./storage";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
// const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}/${postgres.database}`
|
||||
const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}:${postgres.port}/${postgres.database}`;
|
||||
|
||||
const tag = $dev
|
||||
? `latest`
|
||||
: JSON.parse(
|
||||
readFileSync("./node_modules/@rocicorp/zero/package.json").toString(),
|
||||
).version.replace("+", "-");
|
||||
|
||||
const zeroEnv = {
|
||||
FORCE: "1",
|
||||
NO_COLOR: "1",
|
||||
ZERO_LOG_LEVEL: "info",
|
||||
ZERO_LITESTREAM_LOG_LEVEL: "info",
|
||||
ZERO_UPSTREAM_DB: connectionString,
|
||||
ZERO_IMAGE_URL: `rocicorp/zero:${tag}`,
|
||||
ZERO_CVR_DB: connectionString,
|
||||
ZERO_CHANGE_DB: connectionString,
|
||||
ZERO_REPLICA_FILE: "/tmp/nestri.db",
|
||||
ZERO_LITESTREAM_RESTORE_PARALLELISM: "64",
|
||||
ZERO_APP_ID: $app.stage,
|
||||
ZERO_AUTH_JWKS_URL: $interpolate`${auth.url}/.well-known/jwks.json`,
|
||||
ZERO_INITIAL_SYNC_ROW_BATCH_SIZE: "30000",
|
||||
NODE_OPTIONS: "--max-old-space-size=8192",
|
||||
...($dev
|
||||
? {
|
||||
}
|
||||
: {
|
||||
ZERO_LITESTREAM_BACKUP_URL: $interpolate`s3://${storage.name}/zero/0`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Replication Manager Service
|
||||
const replicationManager = !$dev
|
||||
? new sst.aws.Service(`ZeroReplication`, {
|
||||
cluster,
|
||||
wait: true,
|
||||
...($app.stage === "production"
|
||||
? {
|
||||
cpu: "2 vCPU",
|
||||
memory: "4 GB",
|
||||
}
|
||||
: {}),
|
||||
architecture: "arm64",
|
||||
image: zeroEnv.ZERO_IMAGE_URL,
|
||||
link: [storage, postgres],
|
||||
health: {
|
||||
command: ["CMD-SHELL", "curl -f http://localhost:4849/ || exit 1"],
|
||||
interval: "5 seconds",
|
||||
retries: 3,
|
||||
startPeriod: "300 seconds",
|
||||
},
|
||||
environment: {
|
||||
...zeroEnv,
|
||||
ZERO_CHANGE_MAX_CONNS: "3",
|
||||
ZERO_NUM_SYNC_WORKERS: "0",
|
||||
},
|
||||
logging: {
|
||||
retention: "1 month",
|
||||
},
|
||||
loadBalancer: {
|
||||
public: false,
|
||||
ports: [
|
||||
{
|
||||
listen: "80/http",
|
||||
forward: "4849/http",
|
||||
},
|
||||
],
|
||||
},
|
||||
transform: {
|
||||
loadBalancer: {
|
||||
idleTimeout: 3600,
|
||||
},
|
||||
service: {
|
||||
healthCheckGracePeriodSeconds: 900,
|
||||
},
|
||||
},
|
||||
}) : undefined;
|
||||
|
||||
// Permissions deployment
|
||||
// const permissions = new sst.aws.Function(
|
||||
// "ZeroPermissions",
|
||||
// {
|
||||
// vpc,
|
||||
// link: [postgres],
|
||||
// handler: "packages/functions/src/zero.handler",
|
||||
// // environment: { ["ZERO_UPSTREAM_DB"]: connectionString },
|
||||
// copyFiles: [{
|
||||
// from: "packages/zero/permissions.sql",
|
||||
// to: "./.permissions.sql"
|
||||
// }],
|
||||
// }
|
||||
// );
|
||||
|
||||
// if (replicationManager) {
|
||||
// new aws.lambda.Invocation(
|
||||
// "ZeroPermissionsInvocation",
|
||||
// {
|
||||
// input: Date.now().toString(),
|
||||
// functionName: permissions.name,
|
||||
// },
|
||||
// { dependsOn: replicationManager }
|
||||
// );
|
||||
// // new command.local.Command(
|
||||
// // "ZeroPermission",
|
||||
// // {
|
||||
// // dir: process.cwd() + "/packages/zero",
|
||||
// // environment: {
|
||||
// // ZERO_UPSTREAM_DB: connectionString,
|
||||
// // },
|
||||
// // create: "bun run zero-deploy-permissions",
|
||||
// // triggers: [Date.now()],
|
||||
// // },
|
||||
// // {
|
||||
// // dependsOn: [replicationManager],
|
||||
// // },
|
||||
// // );
|
||||
// }
|
||||
|
||||
export const zero = new sst.aws.Service("Zero", {
|
||||
cluster,
|
||||
image: zeroEnv.ZERO_IMAGE_URL,
|
||||
link: [storage, postgres],
|
||||
architecture: "arm64",
|
||||
...($app.stage === "production"
|
||||
? {
|
||||
cpu: "2 vCPU",
|
||||
memory: "4 GB",
|
||||
capacity: "spot"
|
||||
}
|
||||
: {
|
||||
capacity: "spot"
|
||||
}),
|
||||
environment: {
|
||||
...zeroEnv,
|
||||
...($dev
|
||||
? {
|
||||
ZERO_NUM_SYNC_WORKERS: "1",
|
||||
}
|
||||
: {
|
||||
ZERO_CHANGE_STREAMER_URI: replicationManager?.url.apply((val) =>
|
||||
val.replace("http://", "ws://"),
|
||||
) ?? "",
|
||||
ZERO_UPSTREAM_MAX_CONNS: "15",
|
||||
ZERO_CVR_MAX_CONNS: "160",
|
||||
}),
|
||||
},
|
||||
health: {
|
||||
retries: 3,
|
||||
command: ["CMD-SHELL", "curl -f http://localhost:4848/ || exit 1"],
|
||||
interval: "5 seconds",
|
||||
startPeriod: "300 seconds",
|
||||
},
|
||||
loadBalancer: {
|
||||
domain: {
|
||||
name: "zero." + domain,
|
||||
dns: sst.cloudflare.dns()
|
||||
},
|
||||
rules: [
|
||||
{ listen: "443/https", forward: "4848/http" },
|
||||
{ listen: "80/http", forward: "4848/http" },
|
||||
],
|
||||
},
|
||||
scaling: {
|
||||
min: 1,
|
||||
max: 4,
|
||||
},
|
||||
logging: {
|
||||
retention: "1 month",
|
||||
},
|
||||
transform: {
|
||||
service: {
|
||||
healthCheckGracePeriodSeconds: 900,
|
||||
},
|
||||
// taskDefinition: {
|
||||
// ephemeralStorage: {
|
||||
// sizeInGib: 200,
|
||||
// },
|
||||
// },
|
||||
loadBalancer: {
|
||||
idleTimeout: 3600,
|
||||
},
|
||||
},
|
||||
dev: {
|
||||
command: "bun dev",
|
||||
directory: "packages/zero",
|
||||
url: "http://localhost:4848",
|
||||
},
|
||||
});
|
||||
34660
package-lock.json
generated
11
package.json
@@ -11,18 +11,20 @@
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"packageManager": "bun@1.2.18",
|
||||
"packageManager": "bun@1.2.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"sso": "aws sso login --sso-session=nestri --no-browser --use-device-code"
|
||||
},
|
||||
"overrides": {
|
||||
"@openauthjs/openauth": "0.4.3"
|
||||
"@openauthjs/openauth": "0.4.3",
|
||||
"steam-session": "1.9.3"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@macaron-css/solid@1.5.3": "patches/@macaron-css%2Fsolid@1.5.3.patch",
|
||||
"drizzle-orm@0.36.1": "patches/drizzle-orm@0.36.1.patch"
|
||||
"drizzle-orm@0.36.1": "patches/drizzle-orm@0.36.1.patch",
|
||||
"steam-session@1.9.3": "patches/steam-session@1.9.3.patch"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"core-js-pure",
|
||||
@@ -35,6 +37,7 @@
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"sst": "^3.17.8"
|
||||
"sharp": "^0.34.2",
|
||||
"sst": "^3.11.21"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { fn } from "../utils";
|
||||
import { Resource } from "sst";
|
||||
import { Actor } from "../actor";
|
||||
import { bus } from "sst/aws/bus";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import { createEvent } from "../event";
|
||||
import { eq, and, isNull, desc } from "drizzle-orm";
|
||||
import { steamTable, StatusEnum, Limitations } from "./steam.sql";
|
||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
||||
import { createTransaction, useTransaction } from "../drizzle/transaction";
|
||||
|
||||
export namespace Steam {
|
||||
export const Info = z
|
||||
@@ -124,9 +122,9 @@ export namespace Steam {
|
||||
lastSyncedAt: input.lastSyncedAt ?? Common.utc(),
|
||||
})
|
||||
|
||||
await afterTx(async () =>
|
||||
bus.publish(Resource.Bus, Events.Created, { userID, steamID: input.id })
|
||||
);
|
||||
// await afterTx(async () =>
|
||||
// bus.publish(Resource.Bus, Events.Created, { userID, steamID: input.id })
|
||||
// );
|
||||
|
||||
return input.id
|
||||
}),
|
||||
@@ -151,9 +149,9 @@ export namespace Steam {
|
||||
})
|
||||
.where(eq(steamTable.id, input.steamID));
|
||||
|
||||
await afterTx(async () =>
|
||||
bus.publish(Resource.Bus, Events.Updated, { userID, steamID: input.steamID })
|
||||
);
|
||||
// await afterTx(async () =>
|
||||
// bus.publish(Resource.Bus, Events.Updated, { userID, steamID: input.steamID })
|
||||
// );
|
||||
|
||||
return input.steamID
|
||||
})
|
||||
|
||||
17
packages/functions/Containerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM mirror.gcr.io/oven/bun:1.2
|
||||
|
||||
# TODO: Add a way to build C# Steam.exe and start it to run in the container before the API
|
||||
|
||||
ADD ./package.json .
|
||||
ADD ./bun.lock .
|
||||
ADD ./packages/core/package.json ./packages/core/package.json
|
||||
ADD ./packages/functions/package.json ./packages/functions/package.json
|
||||
ADD ./patches ./patches
|
||||
RUN bun install --ignore-scripts
|
||||
|
||||
ADD ./packages/functions ./packages/functions
|
||||
ADD ./packages/core ./packages/core
|
||||
|
||||
WORKDIR ./packages/functions
|
||||
|
||||
CMD ["bun", "run", "./src/api/index.ts"]
|
||||
@@ -7,6 +7,10 @@
|
||||
"@types/bun": "latest",
|
||||
"@types/steamcommunity": "^3.43.8"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:auth": "bun run --watch ./src/auth/index.ts",
|
||||
"dev:api": "bun run --watch ./src/api/index.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
@@ -21,10 +25,8 @@
|
||||
"@aws-sdk/client-sqs": "^3.806.0",
|
||||
"@nestri/core": "workspace:",
|
||||
"actor-core": "^0.8.0",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"hono": "^4.7.8",
|
||||
"hono-openapi": "^0.4.8",
|
||||
"steam-session": "*",
|
||||
"steamcommunity": "^3.48.6",
|
||||
"steamid": "^2.1.0"
|
||||
"hono-openapi": "^0.4.8"
|
||||
}
|
||||
}
|
||||
137
packages/functions/src/api/image.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { z } from "zod"
|
||||
import { Hono } from "hono";
|
||||
import {
|
||||
S3Client,
|
||||
GetObjectCommand,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import Sharp from "sharp";
|
||||
import { Resource } from "sst";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
|
||||
const s3 = new S3Client();
|
||||
|
||||
interface TimingMetrics {
|
||||
download: number;
|
||||
transform: number;
|
||||
upload?: number;
|
||||
}
|
||||
|
||||
const formatTimingHeader = (metrics: TimingMetrics): string => {
|
||||
const timings = [
|
||||
`img-download;dur=${Math.round(metrics.download)}`,
|
||||
`img-transform;dur=${Math.round(metrics.transform)}`,
|
||||
];
|
||||
|
||||
if (metrics.upload !== undefined) {
|
||||
timings.push(`img-upload;dur=${Math.round(metrics.upload)}`);
|
||||
}
|
||||
|
||||
return timings.join(",");
|
||||
};
|
||||
|
||||
|
||||
export namespace ImageApi {
|
||||
export const route = new Hono()
|
||||
.post("/:hash",
|
||||
validator("json",
|
||||
z.object({
|
||||
dpr: z.number().optional(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
quality: z.number().optional(),
|
||||
format: z.enum(["avif", "webp", "jpeg"]),
|
||||
})
|
||||
),
|
||||
// validator("header",
|
||||
// z.object({
|
||||
// secretKey: z.string(),
|
||||
// })
|
||||
// ),
|
||||
validator("param",
|
||||
z.object({
|
||||
hash: z.string(),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const input = c.req.valid("json");
|
||||
const { hash } = c.req.valid("param");
|
||||
// const secret = c.req.valid("header").secretKey
|
||||
|
||||
const metrics: TimingMetrics = {
|
||||
download: 0,
|
||||
transform: 0,
|
||||
};
|
||||
|
||||
const downloadStart = performance.now();
|
||||
let originalImage: Buffer;
|
||||
let contentType: string;
|
||||
try {
|
||||
const getCommand = new GetObjectCommand({
|
||||
Bucket: Resource.Storage.name,
|
||||
Key: hash,
|
||||
});
|
||||
const response = await s3.send(getCommand);
|
||||
|
||||
originalImage = Buffer.from(await response.Body!.transformToByteArray());
|
||||
contentType = response.ContentType || "image/jpeg";
|
||||
metrics.download = performance.now() - downloadStart;
|
||||
} catch (error) {
|
||||
throw new HTTPException(500, { message: `Error downloading original image:${error}` });
|
||||
}
|
||||
|
||||
|
||||
const transformStart = performance.now();
|
||||
let transformedImage: Buffer;
|
||||
|
||||
try {
|
||||
let sharpInstance = Sharp(originalImage, {
|
||||
failOn: "none",
|
||||
animated: true,
|
||||
});
|
||||
|
||||
const metadata = await sharpInstance.metadata();
|
||||
|
||||
// Apply transformations
|
||||
if (input.width || input.height) {
|
||||
sharpInstance = sharpInstance.resize({
|
||||
width: input.width,
|
||||
height: input.height,
|
||||
});
|
||||
}
|
||||
|
||||
if (metadata.orientation) {
|
||||
sharpInstance = sharpInstance.rotate();
|
||||
}
|
||||
|
||||
if (input.format) {
|
||||
const isLossy = ["jpeg", "webp", "avif"].includes(input.format);
|
||||
|
||||
if (isLossy && input.quality) {
|
||||
sharpInstance = sharpInstance.toFormat(input.format, {
|
||||
quality: input.quality,
|
||||
});
|
||||
} else {
|
||||
sharpInstance = sharpInstance.toFormat(input.format);
|
||||
}
|
||||
}
|
||||
|
||||
transformedImage = await sharpInstance.toBuffer();
|
||||
metrics.transform = performance.now() - transformStart;
|
||||
|
||||
contentType = `image/${input.format}`;
|
||||
} catch (error) {
|
||||
throw new HTTPException(500, { message: `Error transforming image:${error}` });
|
||||
}
|
||||
|
||||
return c.newResponse(transformedImage,
|
||||
200,
|
||||
{
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "max-age=31536000",
|
||||
"Server-Timing": formatTimingHeader(metrics),
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import "zod-openapi/extend";
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { GameApi } from "./game";
|
||||
import { SteamApi } from "./steam";
|
||||
import { auth } from "./utils/auth";
|
||||
import { FriendApi } from "./friend";
|
||||
import { logger } from "hono/logger";
|
||||
import { type Env, Hono } from "hono";
|
||||
import { Realtime } from "./realtime";
|
||||
import { AccountApi } from "./account";
|
||||
import { openAPISpecs } from "hono-openapi";
|
||||
import { patchLogger } from "../utils/patch-logger";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { handle, streamHandle } from "hono/aws-lambda";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
patchLogger();
|
||||
@@ -17,6 +18,7 @@ patchLogger();
|
||||
export const app = new Hono();
|
||||
app
|
||||
.use(logger())
|
||||
.use(cors())
|
||||
.use(async (c, next) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
return next();
|
||||
@@ -27,6 +29,7 @@ const routes = app
|
||||
.get("/", (c) => c.text("Hello World!"))
|
||||
.route("/games", GameApi.route)
|
||||
.route("/steam", SteamApi.route)
|
||||
.route("/realtime", Realtime.route)
|
||||
.route("/friends", FriendApi.route)
|
||||
.route("/account", AccountApi.route)
|
||||
.onError((error, c) => {
|
||||
@@ -74,7 +77,7 @@ app.get(
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
},
|
||||
TeamID: {
|
||||
SteamID: {
|
||||
type: "apiKey",
|
||||
description: "The steam ID to use for this query",
|
||||
in: "header",
|
||||
@@ -82,7 +85,7 @@ app.get(
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [{ Bearer: [], TeamID: [] }],
|
||||
security: [{ Bearer: [], SteamID: [] }],
|
||||
servers: [
|
||||
{ description: "Production", url: "https://api.nestri.io" },
|
||||
{ description: "Sandbox", url: "https://api.dev.nestri.io" },
|
||||
@@ -91,6 +94,13 @@ app.get(
|
||||
}),
|
||||
);
|
||||
|
||||
export type Routes = typeof routes;
|
||||
|
||||
export const handler = process.env.SST_LIVE ? handle(app) : streamHandle(app);
|
||||
export default {
|
||||
port: 3001,
|
||||
idleTimeout: 255,
|
||||
webSocketHandler: Realtime.webSocketHandler,
|
||||
fetch: (req: Request, env: Env) =>
|
||||
app.fetch(req, env, {
|
||||
waitUntil: (fn) => fn,
|
||||
passThroughOnException: () => { },
|
||||
}),
|
||||
};
|
||||
@@ -1,15 +1,21 @@
|
||||
import { z } from "zod";
|
||||
import { Hono } from "hono";
|
||||
import { Resource } from "sst";
|
||||
import { bus } from "sst/aws/bus";
|
||||
import { Actor } from "@nestri/core/actor";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { User } from "@nestri/core/user/index";
|
||||
import { Examples } from "@nestri/core/examples";
|
||||
import { Steam } from "@nestri/core/steam/index";
|
||||
import { getCookie, setCookie } from "hono/cookie";
|
||||
import { Client } from "@nestri/core/client/index";
|
||||
import { Friend } from "@nestri/core/friend/index";
|
||||
import { Library } from "@nestri/core/library/index";
|
||||
import { chunkArray } from "@nestri/core/utils/helper";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
import { ErrorResponses, validator, Result, notPublic } from "./utils";
|
||||
|
||||
|
||||
export namespace SteamApi {
|
||||
export const route = new Hono()
|
||||
.get("/",
|
||||
@@ -104,6 +110,89 @@ export namespace SteamApi {
|
||||
await Steam.updateOwner({ userID, steamID })
|
||||
}
|
||||
|
||||
c.executionCtx.waitUntil((async () => {
|
||||
try {
|
||||
// Get friends info
|
||||
const friends = await Client.getFriendsList(steamID);
|
||||
|
||||
const friendSteamIDs = friends.friendslist.friends.map(f => f.steamid);
|
||||
|
||||
// Steam API has a limit of requesting 100 friends at a go
|
||||
const friendChunks = chunkArray(friendSteamIDs, 100);
|
||||
|
||||
const settled = await Promise.allSettled(
|
||||
friendChunks.map(async (friendIDs) => {
|
||||
const friendsInfo = await Client.getUserInfo(friendIDs)
|
||||
|
||||
return await Promise.all(
|
||||
friendsInfo.map(async (friend) => {
|
||||
const wasAdded = await Steam.create(friend);
|
||||
|
||||
if (!wasAdded) {
|
||||
console.log(`Friend ${friend.id} already exists`)
|
||||
}
|
||||
|
||||
await Friend.add({ friendSteamID: friend.id, steamID })
|
||||
|
||||
return friend.id
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
settled
|
||||
.filter(result => result.status === 'rejected')
|
||||
.forEach(result => console.warn('[putFriends] failed:', (result as PromiseRejectedResult).reason))
|
||||
|
||||
const prod = (Resource.App.stage === "production" || Resource.App.stage === "dev")
|
||||
|
||||
const friendIDs = [
|
||||
steamID,
|
||||
...(prod ? settled
|
||||
.filter(result => result.status === "fulfilled")
|
||||
.map(f => f.value)
|
||||
.flat() : [])
|
||||
]
|
||||
|
||||
await Promise.all(
|
||||
friendIDs.map(async (currentSteamID) => {
|
||||
// Get user library
|
||||
const gameLibrary = await Client.getUserLibrary(currentSteamID);
|
||||
|
||||
const queryLib = await Promise.allSettled(
|
||||
gameLibrary.response.games.map(async (game) => {
|
||||
await Actor.provide(
|
||||
"steam",
|
||||
{
|
||||
steamID: currentSteamID,
|
||||
},
|
||||
async () => {
|
||||
|
||||
await bus.publish(
|
||||
Resource.Bus,
|
||||
Library.Events.Add,
|
||||
{
|
||||
appID: game.appid,
|
||||
totalPlaytime: game.playtime_forever,
|
||||
lastPlayed: game.rtime_last_played ? new Date(game.rtime_last_played * 1000) : null,
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
queryLib
|
||||
.filter(i => i.status === "rejected")
|
||||
.forEach(e => console.warn(`[pushUserLib]: Failed to push user library to queue: ${e.reason}`))
|
||||
})
|
||||
)
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to process Steam data for user ${userID}:`, error);
|
||||
}
|
||||
})())
|
||||
|
||||
return c.html(
|
||||
`
|
||||
<script>
|
||||
@@ -147,7 +236,7 @@ export namespace SteamApi {
|
||||
|
||||
setCookie(c, "user_id", user.id);
|
||||
|
||||
const returnUrl = `${new URL(Resource.Urls.api).origin}/steam/callback/${userID}`
|
||||
const returnUrl = `${new URL(c.req.url).origin}/steam/callback/${userID}`
|
||||
|
||||
const params = new URLSearchParams({
|
||||
'openid.ns': 'http://specs.openid.net/auth/2.0',
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { Resource } from "sst";
|
||||
import { type Env } from "hono";
|
||||
import { logger } from "hono/logger";
|
||||
import { subjects } from "../subjects";
|
||||
import { handle } from "hono/aws-lambda";
|
||||
import { PasswordUI, Select } from "./ui";
|
||||
import { issuer } from "@openauthjs/openauth";
|
||||
import { User } from "@nestri/core/user/index";
|
||||
import { Email } from "@nestri/core/email/index";
|
||||
import { patchLogger } from "../utils/patch-logger";
|
||||
import { handleDiscord, handleGithub } from "./utils";
|
||||
import { MemoryStorage } from "@openauthjs/openauth/storage/memory";
|
||||
import { DiscordAdapter, PasswordAdapter, GithubAdapter } from "./adapters";
|
||||
|
||||
patchLogger();
|
||||
|
||||
const app = issuer({
|
||||
//TODO: Create our own Storage (?)
|
||||
select: Select(),
|
||||
storage: MemoryStorage({
|
||||
persist: process.env.STORAGE
|
||||
}),
|
||||
theme: {
|
||||
title: "Nestri | Auth",
|
||||
primary: "#FF4F01",
|
||||
@@ -156,4 +161,13 @@ const app = issuer({
|
||||
},
|
||||
}).use(logger())
|
||||
|
||||
export const handler = handle(app);
|
||||
|
||||
export default {
|
||||
port: 3002,
|
||||
idleTimeout: 255,
|
||||
fetch: (req: Request, env: Env) =>
|
||||
app.fetch(req, env, {
|
||||
waitUntil: (fn) => fn,
|
||||
passThroughOnException: () => { },
|
||||
}),
|
||||
};
|
||||
@@ -5,10 +5,8 @@ import { Actor } from "@nestri/core/actor";
|
||||
import { Game } from "@nestri/core/game/index";
|
||||
import { Steam } from "@nestri/core/steam/index";
|
||||
import { Client } from "@nestri/core/client/index";
|
||||
import { Friend } from "@nestri/core/friend/index";
|
||||
import { Images } from "@nestri/core/images/index";
|
||||
import { Library } from "@nestri/core/library/index";
|
||||
import { chunkArray } from "@nestri/core/utils/index";
|
||||
import { BaseGame } from "@nestri/core/base-game/index";
|
||||
import { Categories } from "@nestri/core/categories/index";
|
||||
import { ImageTypeEnum } from "@nestri/core/images/images.sql";
|
||||
@@ -266,93 +264,6 @@ export const handler = bus.subscriber(
|
||||
|
||||
break;
|
||||
}
|
||||
case "steam_account.created":
|
||||
case "steam_account.updated": {
|
||||
const userID = event.properties.userID;
|
||||
|
||||
try {
|
||||
const steamID = event.properties.steamID;
|
||||
// Get friends info
|
||||
const friends = await Client.getFriendsList(steamID);
|
||||
|
||||
const friendSteamIDs = friends.friendslist.friends.map(f => f.steamid);
|
||||
|
||||
// Steam API has a limit of requesting 100 friends at a go
|
||||
const friendChunks = chunkArray(friendSteamIDs, 100);
|
||||
|
||||
const settled = await Promise.allSettled(
|
||||
friendChunks.map(async (friendIDs) => {
|
||||
const friendsInfo = await Client.getUserInfo(friendIDs)
|
||||
|
||||
return await Promise.all(
|
||||
friendsInfo.map(async (friend) => {
|
||||
const wasAdded = await Steam.create(friend);
|
||||
|
||||
if (!wasAdded) {
|
||||
console.log(`Friend ${friend.id} already exists`)
|
||||
}
|
||||
|
||||
await Friend.add({ friendSteamID: friend.id, steamID })
|
||||
|
||||
return friend.id
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
settled
|
||||
.filter(result => result.status === 'rejected')
|
||||
.forEach(result => console.warn('[putFriends] failed:', (result as PromiseRejectedResult).reason))
|
||||
|
||||
const prod = (Resource.App.stage === "production" || Resource.App.stage === "dev")
|
||||
|
||||
const friendIDs = [
|
||||
steamID,
|
||||
...(prod ? settled
|
||||
.filter(result => result.status === "fulfilled")
|
||||
.map(f => f.value)
|
||||
.flat() : [])
|
||||
]
|
||||
|
||||
await Promise.all(
|
||||
friendIDs.map(async (currentSteamID) => {
|
||||
// Get user library
|
||||
const gameLibrary = await Client.getUserLibrary(currentSteamID);
|
||||
|
||||
const queryLib = await Promise.allSettled(
|
||||
gameLibrary.response.games.map(async (game) => {
|
||||
await Actor.provide(
|
||||
"steam",
|
||||
{
|
||||
steamID: currentSteamID,
|
||||
},
|
||||
async () => {
|
||||
|
||||
await bus.publish(
|
||||
Resource.Bus,
|
||||
Library.Events.Add,
|
||||
{
|
||||
appID: game.appid,
|
||||
totalPlaytime: game.playtime_forever,
|
||||
lastPlayed: game.rtime_last_played ? new Date(game.rtime_last_played * 1000) : null,
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
queryLib
|
||||
.filter(i => i.status === "rejected")
|
||||
.forEach(e => console.warn(`[pushUserLib]: Failed to push user library to queue: ${e.reason}`))
|
||||
})
|
||||
)
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to process Steam data for user ${userID}:`, error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
91
packages/functions/src/images/image.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Hono } from "hono";
|
||||
import { AwsClient } from 'aws4fetch'
|
||||
import { Resource } from "sst";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
|
||||
|
||||
export namespace ImageRoute {
|
||||
export const route = new Hono()
|
||||
.get(
|
||||
"/:hashWithExt",
|
||||
async (c) => {
|
||||
// const { hashWithExt } = c.req.param();
|
||||
|
||||
const client = new AwsClient({
|
||||
accessKeyId: Resource.ImageInvokerAccessKey.key,
|
||||
secretAccessKey: Resource.ImageInvokerAccessKey.secret,
|
||||
})
|
||||
|
||||
const LAMBDA_URL = `https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/${Resource.ImageProcessor.name}/invocations`
|
||||
|
||||
const lambdaResponse = await client.fetch(LAMBDA_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ world: "hello" }),
|
||||
})
|
||||
|
||||
if (!lambdaResponse.ok) {
|
||||
console.error(await lambdaResponse.text())
|
||||
return c.json({ error: `Lambda API returned ${lambdaResponse.status}` }, { status: 500 })
|
||||
}
|
||||
|
||||
console.log(await lambdaResponse.json())
|
||||
|
||||
// // Validate format
|
||||
// // Split hash and extension
|
||||
// const match = hashWithExt.match(/^([a-zA-Z0-9_-]+)\.(avif|webp)$/);
|
||||
// if (!match) {
|
||||
// throw new HTTPException(400, { message: "Invalid image hash or format" });
|
||||
// }
|
||||
|
||||
// const [, hash, format] = match;
|
||||
|
||||
// const query = c.req.query();
|
||||
// // Parse dimensions
|
||||
// const width = parseInt(query.w || query.width || "");
|
||||
// const height = parseInt(query.h || query.height || "");
|
||||
// const dpr = parseFloat(query.dpr || "1");
|
||||
|
||||
// if (isNaN(width) || width <= 0) {
|
||||
// throw new HTTPException(400, { message: "Invalid width" });
|
||||
// }
|
||||
// if (!isNaN(height) && height < 0) {
|
||||
// throw new HTTPException(400, { message: "Invalid height" });
|
||||
// }
|
||||
// if (dpr < 1 || dpr > 4) {
|
||||
// throw new HTTPException(400, { message: "Invalid dpr" });
|
||||
// }
|
||||
|
||||
// console.log("url",Resource.Api.url)
|
||||
|
||||
// const imageBytes = await fetch(`${Resource.Api.url}/image/${hash}`,{
|
||||
// method:"POST",
|
||||
// body:JSON.stringify({
|
||||
// dpr,
|
||||
// width,
|
||||
// height,
|
||||
// format
|
||||
// })
|
||||
// })
|
||||
|
||||
// console.log("imahe",imageBytes.headers)
|
||||
|
||||
// // Normalize and build cache key
|
||||
// // const cacheKey = `${hash}_${format}_w${width}${height ? `_h${height}` : ""}_dpr${dpr}`;
|
||||
|
||||
// // Add aggressive caching
|
||||
// // c.header("Cache-Control", "public, max-age=315360000, immutable");
|
||||
|
||||
// // Placeholder image response (to be replaced by real logic)
|
||||
// return c.newResponse(await imageBytes.arrayBuffer(),
|
||||
// // {
|
||||
// // headers: {
|
||||
// // ...imageBytes.headers
|
||||
// // }
|
||||
// // }
|
||||
// );
|
||||
|
||||
return c.text("success")
|
||||
}
|
||||
)
|
||||
}
|
||||
18
packages/functions/src/images/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Hono } from "hono";
|
||||
import { logger } from "hono/logger";
|
||||
import { ImageRoute } from "./image";
|
||||
|
||||
const app = new Hono();
|
||||
app
|
||||
.use(logger(), async (c, next) => {
|
||||
// c.header("Cache-Control", "public, max-age=315360000, immutable");
|
||||
return next();
|
||||
})
|
||||
|
||||
const routes = app
|
||||
.get("/", (c) => c.text("Hello World 👋🏾"))
|
||||
.route("/image", ImageRoute.route)
|
||||
|
||||
|
||||
export type Routes = typeof routes;
|
||||
export default app;
|
||||
5
packages/functions/src/images/processor.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export async function handler(event: any) {
|
||||
console.log('Task completion event received:', JSON.stringify(event, null, 2));
|
||||
|
||||
return JSON.stringify({ hello: "world" })
|
||||
}
|
||||
@@ -11,18 +11,6 @@
|
||||
"@bufbuild/protoc-gen-es": "^2.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.3",
|
||||
"@chainsafe/libp2p-noise": "^16.1.3",
|
||||
"@chainsafe/libp2p-yamux": "^7.0.1",
|
||||
"@libp2p/identify": "^3.0.32",
|
||||
"@libp2p/interface": "^2.10.2",
|
||||
"@libp2p/ping": "^2.0.32",
|
||||
"@libp2p/websockets": "^9.2.13",
|
||||
"@multiformats/multiaddr": "^12.4.0",
|
||||
"it-length-prefixed": "^10.0.1",
|
||||
"it-pipe": "^3.0.1",
|
||||
"libp2p": "^2.8.8",
|
||||
"uint8arraylist": "^2.4.8",
|
||||
"uint8arrays": "^5.1.0"
|
||||
"@bufbuild/protobuf": "^2.2.3"
|
||||
}
|
||||
}
|
||||
@@ -1,305 +1,37 @@
|
||||
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 {
|
||||
payload_type: "ice";
|
||||
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 {
|
||||
payload_type: "sdp";
|
||||
sdp: RTCSessionDescriptionInit;
|
||||
}
|
||||
|
||||
export function NewMessageSDP(
|
||||
type: string,
|
||||
sdp: RTCSessionDescriptionInit,
|
||||
): Uint8Array {
|
||||
const msg = {
|
||||
payload_type: type,
|
||||
sdp: sdp,
|
||||
};
|
||||
return new TextEncoder().encode(JSON.stringify(msg));
|
||||
export enum JoinerType {
|
||||
JoinerNode = 0,
|
||||
JoinerClient = 1,
|
||||
}
|
||||
|
||||
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;
|
||||
export interface MessageJoin extends MessageBase {
|
||||
payload_type: "join";
|
||||
joiner_type: JoinerType;
|
||||
}
|
||||
|
||||
// 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 enum AnswerType {
|
||||
AnswerOffline = 0,
|
||||
AnswerInUse,
|
||||
AnswerOK
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
export interface MessageAnswer extends MessageBase {
|
||||
payload_type: "answer";
|
||||
answer_type: AnswerType;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import {
|
||||
NewMessageRaw,
|
||||
NewMessageSDP,
|
||||
NewMessageICE,
|
||||
SafeStream,
|
||||
MessageBase,
|
||||
MessageICE,
|
||||
MessageJoin,
|
||||
MessageSDP,
|
||||
MessageAnswer,
|
||||
JoinerType,
|
||||
AnswerType,
|
||||
} from "./messages";
|
||||
import { webSockets } from "@libp2p/websockets";
|
||||
import { createLibp2p, Libp2p } from "libp2p";
|
||||
import { noise } from "@chainsafe/libp2p-noise";
|
||||
import { yamux } from "@chainsafe/libp2p-yamux";
|
||||
import { identify } from "@libp2p/identify";
|
||||
import { multiaddr } from "@multiformats/multiaddr";
|
||||
import { Connection } from "@libp2p/interface";
|
||||
import { ping } from "@libp2p/ping";
|
||||
|
||||
//FIXME: Sometimes the room will wait to say offline, then appear to be online after retrying :D
|
||||
// This works for me, with my trashy internet, does it work for you as well?
|
||||
|
||||
const NESTRI_PROTOCOL_STREAM_REQUEST = "/nestri-relay/stream-request/1.0.0";
|
||||
|
||||
export class WebRTCStream {
|
||||
private _p2p: Libp2p | undefined = undefined;
|
||||
private _p2pConn: Connection | undefined = undefined;
|
||||
private _p2pSafeStream: SafeStream | undefined = undefined;
|
||||
private _ws: WebSocket | undefined = undefined;
|
||||
private _pc: RTCPeerConnection | undefined = undefined;
|
||||
private _audioTrack: MediaStreamTrack | undefined = undefined;
|
||||
private _videoTrack: MediaStreamTrack | undefined = undefined;
|
||||
@@ -33,11 +24,7 @@ export class WebRTCStream {
|
||||
private _isConnected: boolean = false; // Add flag to track connection state
|
||||
currentFrameRate: number = 60;
|
||||
|
||||
constructor(
|
||||
serverURL: string,
|
||||
roomName: string,
|
||||
connectedCallback: (stream: MediaStream | null) => void,
|
||||
) {
|
||||
constructor(serverURL: string, roomName: string, connectedCallback: (stream: MediaStream | null) => void) {
|
||||
if (roomName.length <= 0) {
|
||||
console.error("Room name not provided");
|
||||
return;
|
||||
@@ -46,114 +33,120 @@ export class WebRTCStream {
|
||||
this._onConnected = connectedCallback;
|
||||
this._serverURL = serverURL;
|
||||
this._roomName = roomName;
|
||||
this._setup(serverURL, roomName).catch(console.error);
|
||||
this._setup(serverURL, roomName);
|
||||
}
|
||||
|
||||
private async _setup(serverURL: string, roomName: string) {
|
||||
private _setup(serverURL: string, roomName: string) {
|
||||
// Don't setup new connection if already connected
|
||||
if (this._isConnected) {
|
||||
console.log("Already connected, skipping setup");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Setting up libp2p");
|
||||
|
||||
this._p2p = await createLibp2p({
|
||||
transports: [webSockets()],
|
||||
connectionEncrypters: [noise()],
|
||||
streamMuxers: [yamux()],
|
||||
connectionGater: {
|
||||
denyDialMultiaddr: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
services: {
|
||||
identify: identify(),
|
||||
ping: ping(),
|
||||
},
|
||||
});
|
||||
|
||||
this._p2p.addEventListener("peer:connect", async (e) => {
|
||||
console.debug("Peer connected:", e.detail);
|
||||
});
|
||||
this._p2p.addEventListener("peer:disconnect", (e) => {
|
||||
console.debug("Peer disconnected:", e.detail);
|
||||
});
|
||||
|
||||
const ma = multiaddr(serverURL);
|
||||
console.debug("Dialing peer at:", ma.toString());
|
||||
this._p2pConn = await this._p2p.dial(ma);
|
||||
|
||||
if (this._p2pConn) {
|
||||
console.log("Stream is being established");
|
||||
let stream = await this._p2pConn
|
||||
.newStream(NESTRI_PROTOCOL_STREAM_REQUEST)
|
||||
.catch(console.error);
|
||||
if (stream) {
|
||||
this._p2pSafeStream = new SafeStream(stream);
|
||||
console.log("Stream opened with peer");
|
||||
console.log("Setting up WebSocket");
|
||||
const wsURL = serverURL.replace(/^http/, "ws");
|
||||
this._ws = new WebSocket(`${wsURL}/api/ws/${roomName}`);
|
||||
this._ws.onopen = async () => {
|
||||
console.log("WebSocket opened");
|
||||
// Send join message
|
||||
const joinMessage: MessageJoin = {
|
||||
payload_type: "join",
|
||||
joiner_type: JoinerType.JoinerClient
|
||||
};
|
||||
this._ws!.send(JSON.stringify(joinMessage));
|
||||
}
|
||||
|
||||
let iceHolder: RTCIceCandidateInit[] = [];
|
||||
this._p2pSafeStream.registerCallback("ice-candidate", (data) => {
|
||||
if (this._pc) {
|
||||
if (this._pc.remoteDescription) {
|
||||
this._pc.addIceCandidate(data.candidate).catch((err) => {
|
||||
console.error("Error adding ICE candidate:", err);
|
||||
});
|
||||
// Add held candidates
|
||||
iceHolder.forEach((candidate) => {
|
||||
this._pc!.addIceCandidate(candidate).catch((err) => {
|
||||
console.error("Error adding held ICE candidate:", err);
|
||||
});
|
||||
});
|
||||
iceHolder = [];
|
||||
} else {
|
||||
iceHolder.push(data.candidate);
|
||||
}
|
||||
} else {
|
||||
iceHolder.push(data.candidate);
|
||||
}
|
||||
});
|
||||
|
||||
this._p2pSafeStream.registerCallback("offer", async (data) => {
|
||||
this._ws.onmessage = async (e) => {
|
||||
// allow only JSON
|
||||
if (typeof e.data === "object") return;
|
||||
if (!e.data) return;
|
||||
const message = JSON.parse(e.data) as MessageBase;
|
||||
switch (message.payload_type) {
|
||||
case "sdp":
|
||||
if (!this._pc) {
|
||||
// Setup peer connection now
|
||||
this._setupPeerConnection();
|
||||
}
|
||||
await this._pc!.setRemoteDescription(data.sdp);
|
||||
console.log("Received SDP: ", (message as MessageSDP).sdp);
|
||||
await this._pc!.setRemoteDescription((message as MessageSDP).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 = NewMessageSDP("answer", answer);
|
||||
await this._p2pSafeStream?.writeMessage(answerMsg);
|
||||
});
|
||||
|
||||
this._p2pSafeStream.registerCallback("request-stream-offline", (data) => {
|
||||
console.warn("Stream is offline for room:", data.roomName);
|
||||
this._onConnected?.(null);
|
||||
});
|
||||
|
||||
// Send stream request
|
||||
// marshal room name into json
|
||||
const request = NewMessageRaw(
|
||||
"request-stream-room",
|
||||
roomName,
|
||||
);
|
||||
await this._p2pSafeStream.writeMessage(request);
|
||||
this._ws!.send(JSON.stringify({
|
||||
payload_type: "sdp",
|
||||
sdp: answer
|
||||
}));
|
||||
break;
|
||||
case "ice":
|
||||
if (!this._pc) break;
|
||||
if (this._pc.remoteDescription) {
|
||||
try {
|
||||
await this._pc.addIceCandidate((message as MessageICE).candidate);
|
||||
// Add held ICE candidates
|
||||
for (const ice of iceHolder) {
|
||||
try {
|
||||
await this._pc.addIceCandidate(ice);
|
||||
} catch (e) {
|
||||
console.error("Error adding held ICE candidate: ", e);
|
||||
}
|
||||
}
|
||||
iceHolder = [];
|
||||
} catch (e) {
|
||||
console.error("Error adding ICE candidate: ", e);
|
||||
}
|
||||
} else {
|
||||
iceHolder.push((message as MessageICE).candidate);
|
||||
}
|
||||
break;
|
||||
case "answer":
|
||||
switch ((message as MessageAnswer).answer_type) {
|
||||
case AnswerType.AnswerOffline:
|
||||
console.log("Room is offline");
|
||||
// Call callback with null stream
|
||||
if (this._onConnected)
|
||||
this._onConnected(null);
|
||||
|
||||
break;
|
||||
case AnswerType.AnswerInUse:
|
||||
console.warn("Room is in use, we shouldn't even be getting this message");
|
||||
break;
|
||||
case AnswerType.AnswerOK:
|
||||
console.log("Joining Room was successful");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown message type: ", message);
|
||||
}
|
||||
}
|
||||
|
||||
this._ws.onclose = () => {
|
||||
console.log("WebSocket closed, reconnecting in 3 seconds");
|
||||
if (this._onConnected)
|
||||
this._onConnected(null);
|
||||
|
||||
// Clear PeerConnection
|
||||
this._cleanupPeerConnection()
|
||||
|
||||
this._handleConnectionFailure()
|
||||
// setTimeout(() => {
|
||||
// this._setup(serverURL, roomName);
|
||||
// }, this._connectionTimeout);
|
||||
}
|
||||
|
||||
this._ws.onerror = (e) => {
|
||||
console.error("WebSocket error: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;"
|
||||
return SDP.replace(
|
||||
/(minptime=10;useinbandfec=1)/,
|
||||
"$1;stereo=1;sprop-stereo=1;",
|
||||
);
|
||||
return SDP.replace(/(minptime=10;useinbandfec=1)/, "$1;stereo=1;sprop-stereo=1;");
|
||||
}
|
||||
|
||||
private _setupPeerConnection() {
|
||||
@@ -165,50 +158,43 @@ export class WebRTCStream {
|
||||
this._pc = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{
|
||||
urls: "stun:stun.l.google.com:19302",
|
||||
},
|
||||
urls: "stun:stun.l.google.com:19302"
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
this._pc.ontrack = (e) => {
|
||||
console.debug("Track received: ", e.track);
|
||||
if (e.track.kind === "audio") this._audioTrack = e.track;
|
||||
else if (e.track.kind === "video") this._videoTrack = e.track;
|
||||
console.log("Track received: ", e.track);
|
||||
if (e.track.kind === "audio")
|
||||
this._audioTrack = e.track;
|
||||
else if (e.track.kind === "video")
|
||||
this._videoTrack = e.track;
|
||||
|
||||
this._checkConnectionState();
|
||||
};
|
||||
|
||||
this._pc.onconnectionstatechange = () => {
|
||||
console.debug("Connection state changed to: ", this._pc!.connectionState);
|
||||
console.log("Connection state changed to: ", this._pc!.connectionState);
|
||||
this._checkConnectionState();
|
||||
};
|
||||
|
||||
this._pc.oniceconnectionstatechange = () => {
|
||||
console.debug(
|
||||
"ICE connection state changed to: ",
|
||||
this._pc!.iceConnectionState,
|
||||
);
|
||||
console.log("ICE connection state changed to: ", this._pc!.iceConnectionState);
|
||||
this._checkConnectionState();
|
||||
};
|
||||
|
||||
this._pc.onicegatheringstatechange = () => {
|
||||
console.debug(
|
||||
"ICE gathering state changed to: ",
|
||||
this._pc!.iceGatheringState,
|
||||
);
|
||||
console.log("ICE gathering state changed to: ", this._pc!.iceGatheringState);
|
||||
this._checkConnectionState();
|
||||
};
|
||||
|
||||
this._pc.onicecandidate = (e) => {
|
||||
if (e.candidate) {
|
||||
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");
|
||||
}
|
||||
const message: MessageICE = {
|
||||
payload_type: "ice",
|
||||
candidate: e.candidate
|
||||
};
|
||||
this._ws!.send(JSON.stringify(message));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -221,35 +207,26 @@ export class WebRTCStream {
|
||||
private _checkConnectionState() {
|
||||
if (!this._pc) return;
|
||||
|
||||
console.debug("Checking connection state:", {
|
||||
console.log("Checking connection state:", {
|
||||
connectionState: this._pc.connectionState,
|
||||
iceConnectionState: this._pc.iceConnectionState,
|
||||
hasAudioTrack: !!this._audioTrack,
|
||||
hasVideoTrack: !!this._videoTrack,
|
||||
isConnected: this._isConnected,
|
||||
isConnected: this._isConnected
|
||||
});
|
||||
|
||||
if (
|
||||
this._pc.connectionState === "connected" &&
|
||||
this._audioTrack !== undefined &&
|
||||
this._videoTrack !== undefined
|
||||
) {
|
||||
if (this._pc.connectionState === "connected" && this._audioTrack !== undefined && this._videoTrack !== undefined) {
|
||||
this._clearConnectionTimer();
|
||||
if (!this._isConnected) {
|
||||
// Only trigger callback if not already connected
|
||||
this._isConnected = true;
|
||||
if (this._onConnected !== undefined) {
|
||||
this._onConnected(
|
||||
new MediaStream([this._audioTrack, this._videoTrack]),
|
||||
);
|
||||
this._onConnected(new MediaStream([this._audioTrack, this._videoTrack]));
|
||||
|
||||
// Continuously set low-latency target
|
||||
this._pc.getReceivers().forEach((receiver: RTCRtpReceiver) => {
|
||||
let intervalLoop = setInterval(async () => {
|
||||
if (
|
||||
receiver.track.readyState !== "live" ||
|
||||
(receiver.transport && receiver.transport.state !== "connected")
|
||||
) {
|
||||
if (receiver.track.readyState !== "live" || (receiver.transport && receiver.transport.state !== "connected")) {
|
||||
clearInterval(intervalLoop);
|
||||
return;
|
||||
} else {
|
||||
@@ -262,11 +239,9 @@ export class WebRTCStream {
|
||||
}
|
||||
|
||||
this._gatherFrameRate();
|
||||
} else if (
|
||||
this._pc.connectionState === "failed" ||
|
||||
} else if (this._pc.connectionState === "failed" ||
|
||||
this._pc.connectionState === "closed" ||
|
||||
this._pc.iceConnectionState === "failed"
|
||||
) {
|
||||
this._pc.iceConnectionState === "failed") {
|
||||
console.log("Connection failed or closed, attempting reconnect");
|
||||
this._isConnected = false; // Reset connected state
|
||||
this._handleConnectionFailure();
|
||||
@@ -275,8 +250,7 @@ export class WebRTCStream {
|
||||
|
||||
private _handleConnectionFailure() {
|
||||
this._clearConnectionTimer();
|
||||
if (this._isConnected) {
|
||||
// Only notify if previously connected
|
||||
if (this._isConnected) { // Only notify if previously connected
|
||||
this._isConnected = false;
|
||||
if (this._onConnected) {
|
||||
this._onConnected(null);
|
||||
@@ -286,7 +260,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,8 +276,10 @@ export class WebRTCStream {
|
||||
|
||||
if (this._audioTrack || this._videoTrack) {
|
||||
try {
|
||||
if (this._audioTrack) this._audioTrack.stop();
|
||||
if (this._videoTrack) this._videoTrack.stop();
|
||||
if (this._audioTrack)
|
||||
this._audioTrack.stop();
|
||||
if (this._videoTrack)
|
||||
this._videoTrack.stop();
|
||||
} catch (err) {
|
||||
console.error("Error stopping media tracks:", err);
|
||||
}
|
||||
@@ -332,16 +308,14 @@ export class WebRTCStream {
|
||||
private _setupDataChannelEvents() {
|
||||
if (!this._dataChannel) return;
|
||||
|
||||
this._dataChannel.onclose = () => console.log("sendChannel has closed");
|
||||
this._dataChannel.onopen = () => console.log("sendChannel has opened");
|
||||
this._dataChannel.onmessage = (e) =>
|
||||
console.log(
|
||||
`Message from DataChannel '${this._dataChannel?.label}' payload '${e.data}'`,
|
||||
);
|
||||
this._dataChannel.onclose = () => console.log('sendChannel has closed')
|
||||
this._dataChannel.onopen = () => console.log('sendChannel has opened')
|
||||
this._dataChannel.onmessage = e => console.log(`Message from DataChannel '${this._dataChannel?.label}' payload '${e.data}'`)
|
||||
}
|
||||
|
||||
private _gatherFrameRate() {
|
||||
if (this._pc === undefined || this._videoTrack === undefined) return;
|
||||
if (this._pc === undefined || this._videoTrack === undefined)
|
||||
return;
|
||||
|
||||
const videoInfoPromise = new Promise<{ fps: number}>((resolve) => {
|
||||
// Keep trying to get fps until it's found
|
||||
@@ -363,25 +337,24 @@ export class WebRTCStream {
|
||||
});
|
||||
|
||||
videoInfoPromise.then((value) => {
|
||||
this.currentFrameRate = value.fps;
|
||||
});
|
||||
this.currentFrameRate = value.fps
|
||||
})
|
||||
}
|
||||
|
||||
// Send binary message through the data channel
|
||||
public sendBinary(data: Uint8Array) {
|
||||
if (this._dataChannel && this._dataChannel.readyState === "open")
|
||||
this._dataChannel.send(data);
|
||||
else console.log("Data channel not open or not established.");
|
||||
else
|
||||
console.log("Data channel not open or not established.");
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this._clearConnectionTimer();
|
||||
this._cleanupPeerConnection();
|
||||
if (this._p2pConn) {
|
||||
this._p2pConn
|
||||
.close()
|
||||
.catch((err) => console.error("Error closing P2P connection:", err));
|
||||
this._p2pConn = undefined;
|
||||
if (this._ws) {
|
||||
this._ws.close();
|
||||
this._ws = undefined;
|
||||
}
|
||||
this._isConnected = false;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ type Resource struct {
|
||||
Auth struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
/*AuthFingerprintKey struct {
|
||||
AuthFingerprintKey struct {
|
||||
Value string `json:"value"`
|
||||
}*/
|
||||
}
|
||||
Realtime struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Authorizer string `json:"authorizer"`
|
||||
|
||||
1
packages/relay/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
persist-data/
|
||||
@@ -3,15 +3,16 @@ module relay
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/libp2p/go-libp2p v0.41.1
|
||||
github.com/libp2p/go-libp2p-pubsub v0.13.1
|
||||
github.com/libp2p/go-reuseport v0.4.0
|
||||
github.com/multiformats/go-multiaddr v0.15.0
|
||||
github.com/oklog/ulid/v2 v2.1.1
|
||||
github.com/pion/ice/v4 v4.0.10
|
||||
github.com/pion/interceptor v0.1.38
|
||||
github.com/pion/rtp v1.8.15
|
||||
github.com/pion/webrtc/v4 v4.1.1
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/pion/ice/v4 v4.0.9
|
||||
github.com/pion/interceptor v0.1.37
|
||||
github.com/pion/rtp v1.8.13
|
||||
github.com/pion/webrtc/v4 v4.0.14
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
@@ -25,35 +26,32 @@ require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/elastic/gosigar v0.14.3 // indirect
|
||||
github.com/filecoin-project/go-clock v0.1.0 // indirect
|
||||
github.com/flynn/noise v1.1.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/ipfs/go-cid v0.5.0 // indirect
|
||||
github.com/ipfs/go-log/v2 v2.6.0 // indirect
|
||||
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/koron/go-ssdp v0.0.6 // indirect
|
||||
github.com/koron/go-ssdp v0.0.5 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.3.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
|
||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.2 // indirect
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
|
||||
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.66 // indirect
|
||||
github.com/miekg/dns v1.1.64 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
@@ -68,7 +66,7 @@ require (
|
||||
github.com/multiformats/go-multistream v0.6.0 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
@@ -78,39 +76,37 @@ require (
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||
github.com/pion/sctp v1.8.37 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.11 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.64.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.52.0 // indirect
|
||||
github.com/quic-go/quic-go v0.50.1 // indirect
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
|
||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/dig v1.19.0 // indirect
|
||||
go.uber.org/fx v1.24.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.uber.org/dig v1.18.1 // indirect
|
||||
go.uber.org/fx v1.23.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
lukechampine.com/blake3 v1.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
@@ -46,8 +47,6 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
|
||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
||||
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
|
||||
github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
||||
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
@@ -86,8 +85,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
@@ -103,8 +102,8 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||
github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg=
|
||||
github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8=
|
||||
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
|
||||
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
@@ -119,8 +118,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU=
|
||||
github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI=
|
||||
github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk=
|
||||
github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -132,8 +131,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||
github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784=
|
||||
github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo=
|
||||
github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw=
|
||||
github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc=
|
||||
github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE=
|
||||
github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI=
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
|
||||
@@ -150,19 +149,17 @@ github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQsc
|
||||
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
|
||||
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
|
||||
github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||
github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ=
|
||||
github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
||||
@@ -204,12 +201,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
||||
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
@@ -224,10 +221,10 @@ github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.38 h1:Mgt3XIIq47uR5vcLLahfRucE6tFPjxHak+z5ZZFEzLU=
|
||||
github.com/pion/interceptor v0.1.38/go.mod h1:HS9X+Ue5LDE6q2C2tuvOuO83XkBdJFgn6MBDtfoJX4Q=
|
||||
github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk=
|
||||
github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
@@ -237,12 +234,12 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.15 h1:MuhuGn1cxpVCPLNY1lI7F1tQ8Spntpgf12ob+pOYT8s=
|
||||
github.com/pion/rtp v1.8.15/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/rtp v1.8.13 h1:8uSUPpjSL4OlwZI8Ygqu7+h2p9NPFB+yAZ461Xn5sNg=
|
||||
github.com/pion/rtp v1.8.13/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs=
|
||||
github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI=
|
||||
github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
@@ -255,39 +252,37 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v4 v4.1.1 h1:PMFPtLg1kpD2pVtun+LGUzA3k54JdFl87WO0Z1+HKug=
|
||||
github.com/pion/webrtc/v4 v4.1.1/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg=
|
||||
github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
|
||||
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
|
||||
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
|
||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
@@ -323,7 +318,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -339,20 +336,23 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
||||
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/dig v1.18.1 h1:rLww6NuajVjeQn+49u5NcezUJEGwd5uXmyoCKW2g5Es=
|
||||
go.uber.org/dig v1.18.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
|
||||
go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
@@ -369,18 +369,20 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
@@ -392,6 +394,7 @@ golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -399,15 +402,15 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -423,8 +426,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -437,10 +440,10 @@ golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -449,8 +452,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -461,30 +464,33 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -514,14 +520,16 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w=
|
||||
lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
|
||||
@@ -2,13 +2,12 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"github.com/libp2p/go-reuseport"
|
||||
"github.com/pion/ice/v4"
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var globalWebRTCAPI *webrtc.API
|
||||
@@ -25,9 +24,17 @@ func InitWebRTCAPI() error {
|
||||
// Media engine
|
||||
mediaEngine := &webrtc.MediaEngine{}
|
||||
|
||||
// Register our extensions
|
||||
if err := RegisterExtensions(mediaEngine); err != nil {
|
||||
return fmt.Errorf("failed to register extensions: %w", err)
|
||||
// Register additional header extensions to reduce latency
|
||||
// Playout Delay
|
||||
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
||||
URI: ExtensionPlayoutDelay,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
||||
URI: ExtensionPlayoutDelay,
|
||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Default codecs cover most of our needs
|
||||
@@ -68,10 +75,9 @@ func InitWebRTCAPI() error {
|
||||
// New in v4, reduces CPU usage and latency when enabled
|
||||
settingEngine.EnableSCTPZeroChecksum(true)
|
||||
|
||||
nat11IP := GetFlags().NAT11IP
|
||||
if len(nat11IP) > 0 {
|
||||
settingEngine.SetNAT1To1IPs([]string{nat11IP}, webrtc.ICECandidateTypeSrflx)
|
||||
slog.Info("Using NAT 1:1 IP for WebRTC", "nat11_ip", nat11IP)
|
||||
nat11IPs := GetFlags().NAT11IPs
|
||||
if len(nat11IPs) > 0 {
|
||||
settingEngine.SetNAT1To1IPs(nat11IPs, webrtc.ICECandidateTypeHost)
|
||||
}
|
||||
|
||||
muxPort := GetFlags().UDPMuxPort
|
||||
@@ -79,7 +85,7 @@ func InitWebRTCAPI() error {
|
||||
// Use reuseport to allow multiple listeners on the same port
|
||||
pktListener, err := reuseport.ListenPacket("udp", ":"+strconv.Itoa(muxPort))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create WebRTC muxed UDP listener: %w", err)
|
||||
return fmt.Errorf("failed to create UDP listener: %w", err)
|
||||
}
|
||||
|
||||
mux := ice.NewMultiUDPMuxDefault(ice.NewUDPMuxDefault(ice.UDPMuxParams{
|
||||
@@ -89,14 +95,11 @@ func InitWebRTCAPI() error {
|
||||
settingEngine.SetICEUDPMux(mux)
|
||||
}
|
||||
|
||||
if flags.WebRTCUDPStart > 0 && flags.WebRTCUDPEnd > 0 && flags.WebRTCUDPStart < flags.WebRTCUDPEnd {
|
||||
// Set the UDP port range used by WebRTC
|
||||
err = settingEngine.SetEphemeralUDPPortRange(uint16(flags.WebRTCUDPStart), uint16(flags.WebRTCUDPEnd))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("Using WebRTC UDP Port Range", "start", flags.WebRTCUDPStart, "end", flags.WebRTCUDPEnd)
|
||||
}
|
||||
|
||||
settingEngine.SetIncludeLoopbackCandidate(true) // Just in case
|
||||
|
||||
@@ -106,6 +109,11 @@ func InitWebRTCAPI() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWebRTCAPI returns the global WebRTC API
|
||||
func GetWebRTCAPI() *webrtc.API {
|
||||
return globalWebRTCAPI
|
||||
}
|
||||
|
||||
// CreatePeerConnection sets up a new peer connection
|
||||
func CreatePeerConnection(onClose func()) (*webrtc.PeerConnection, error) {
|
||||
pc, err := globalWebRTCAPI.NewPeerConnection(globalWebRTCConfig)
|
||||
|
||||
@@ -1,51 +1,19 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"crypto/sha256"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewULID() (ulid.ULID, error) {
|
||||
return ulid.New(ulid.Timestamp(time.Now()), ulid.Monotonic(rand.Reader, 0))
|
||||
}
|
||||
|
||||
// GenerateED25519Key generates a new ED25519 key
|
||||
func GenerateED25519Key() (ed25519.PrivateKey, error) {
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate ED25519 key pair: %w", err)
|
||||
}
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
// SaveED25519Key saves an ED25519 private key to a path as a binary file
|
||||
func SaveED25519Key(privateKey ed25519.PrivateKey, filePath string) error {
|
||||
if privateKey == nil {
|
||||
return errors.New("private key cannot be nil")
|
||||
}
|
||||
if len(privateKey) != ed25519.PrivateKeySize {
|
||||
return errors.New("private key must be exactly 64 bytes for ED25519")
|
||||
}
|
||||
if err := os.WriteFile(filePath, privateKey, 0600); err != nil {
|
||||
return fmt.Errorf("failed to save ED25519 key to %s: %w", filePath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadED25519Key loads an ED25519 private key binary file from a path
|
||||
func LoadED25519Key(filePath string) (ed25519.PrivateKey, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read ED25519 key from %s: %w", filePath, err)
|
||||
}
|
||||
if len(data) != ed25519.PrivateKeySize {
|
||||
return nil, fmt.Errorf("ED25519 key must be exactly %d bytes, got %d", ed25519.PrivateKeySize, len(data))
|
||||
}
|
||||
return data, nil
|
||||
// Helper function to generate PSK from token
|
||||
func GeneratePSKFromToken(token string) ([]byte, error) {
|
||||
// Simple hash-based PSK generation (32 bytes for libp2p)
|
||||
hash := sha256.Sum256([]byte(token))
|
||||
return hash[:], nil
|
||||
}
|
||||
|
||||
@@ -1,45 +1,11 @@
|
||||
package common
|
||||
|
||||
import "github.com/pion/webrtc/v4"
|
||||
|
||||
const (
|
||||
ExtensionPlayoutDelay string = "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"
|
||||
)
|
||||
|
||||
// ExtensionMap maps audio/video extension URIs to their IDs based on registration order
|
||||
var ExtensionMap = map[webrtc.RTPCodecType]map[string]uint8{}
|
||||
|
||||
func RegisterExtensions(mediaEngine *webrtc.MediaEngine) error {
|
||||
// Register additional header extensions to reduce latency
|
||||
// Playout Delay (Video)
|
||||
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
||||
URI: ExtensionPlayoutDelay,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
// Playout Delay (Audio)
|
||||
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
||||
URI: ExtensionPlayoutDelay,
|
||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Register the extension IDs for both audio and video
|
||||
ExtensionMap[webrtc.RTPCodecTypeAudio] = map[string]uint8{
|
||||
// ExtensionMap maps URIs to their IDs based on registration order
|
||||
// IMPORTANT: This must match the order in which extensions are registered in common.go!
|
||||
var ExtensionMap = map[string]uint8{
|
||||
ExtensionPlayoutDelay: 1,
|
||||
}
|
||||
ExtensionMap[webrtc.RTPCodecTypeVideo] = map[string]uint8{
|
||||
ExtensionPlayoutDelay: 1,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetExtension(codecType webrtc.RTPCodecType, extURI string) (uint8, bool) {
|
||||
cType, ok := ExtensionMap[codecType]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
extID, ok := cType[extURI]
|
||||
return extID, ok
|
||||
}
|
||||
|
||||
@@ -2,43 +2,47 @@ package common
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var globalFlags *Flags
|
||||
|
||||
type Flags struct {
|
||||
RegenIdentity bool // Remove old identity on startup and regenerate it
|
||||
Verbose bool // Log everything to console
|
||||
Debug bool // Enable debug mode, implies Verbose
|
||||
EndpointPort int // Port for HTTP/S and WS/S endpoint (TCP)
|
||||
MeshPort int // Port for Mesh connections (TCP)
|
||||
WebRTCUDPStart int // WebRTC UDP port range start - ignored if UDPMuxPort is set
|
||||
WebRTCUDPEnd int // WebRTC UDP port range end - ignored if UDPMuxPort is set
|
||||
STUNServer string // WebRTC STUN server
|
||||
UDPMuxPort int // WebRTC UDP mux port - if set, overrides UDP port range
|
||||
AutoAddLocalIP bool // Automatically add local IP to NAT 1 to 1 IPs
|
||||
NAT11IP string // WebRTC NAT 1 to 1 IP - allows specifying IP of relay if behind NAT
|
||||
PersistDir string // Directory to save persistent data to
|
||||
NAT11IPs []string // WebRTC NAT 1 to 1 IP(s) - allows specifying host IP(s) if behind NAT
|
||||
TLSCert string // Path to TLS certificate
|
||||
TLSKey string // Path to TLS key
|
||||
ControlSecret string // Shared secret for this relay's control endpoint
|
||||
}
|
||||
|
||||
func (flags *Flags) DebugLog() {
|
||||
slog.Debug("Relay flags",
|
||||
"regenIdentity", flags.RegenIdentity,
|
||||
slog.Info("Relay flags",
|
||||
"verbose", flags.Verbose,
|
||||
"debug", flags.Debug,
|
||||
"endpointPort", flags.EndpointPort,
|
||||
"meshPort", flags.MeshPort,
|
||||
"webrtcUDPStart", flags.WebRTCUDPStart,
|
||||
"webrtcUDPEnd", flags.WebRTCUDPEnd,
|
||||
"stunServer", flags.STUNServer,
|
||||
"webrtcUDPMux", flags.UDPMuxPort,
|
||||
"autoAddLocalIP", flags.AutoAddLocalIP,
|
||||
"webrtcNAT11IPs", flags.NAT11IP,
|
||||
"persistDir", flags.PersistDir,
|
||||
"webrtcNAT11IPs", strings.Join(flags.NAT11IPs, ","),
|
||||
"tlsCert", flags.TLSCert,
|
||||
"tlsKey", flags.TLSKey,
|
||||
"controlSecret", flags.ControlSecret,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -72,25 +76,29 @@ func InitFlags() {
|
||||
// Create Flags struct
|
||||
globalFlags = &Flags{}
|
||||
// Get flags
|
||||
flag.BoolVar(&globalFlags.RegenIdentity, "regenIdentity", getEnvAsBool("REGEN_IDENTITY", false), "Regenerate identity on startup")
|
||||
flag.BoolVar(&globalFlags.Verbose, "verbose", getEnvAsBool("VERBOSE", false), "Verbose mode")
|
||||
flag.BoolVar(&globalFlags.Debug, "debug", getEnvAsBool("DEBUG", false), "Debug mode")
|
||||
flag.IntVar(&globalFlags.EndpointPort, "endpointPort", getEnvAsInt("ENDPOINT_PORT", 8088), "HTTP endpoint port")
|
||||
flag.IntVar(&globalFlags.WebRTCUDPStart, "webrtcUDPStart", getEnvAsInt("WEBRTC_UDP_START", 0), "WebRTC UDP port range start")
|
||||
flag.IntVar(&globalFlags.WebRTCUDPEnd, "webrtcUDPEnd", getEnvAsInt("WEBRTC_UDP_END", 0), "WebRTC UDP port range end")
|
||||
flag.IntVar(&globalFlags.MeshPort, "meshPort", getEnvAsInt("MESH_PORT", 8089), "Mesh connections TCP port")
|
||||
flag.IntVar(&globalFlags.WebRTCUDPStart, "webrtcUDPStart", getEnvAsInt("WEBRTC_UDP_START", 10000), "WebRTC UDP port range start")
|
||||
flag.IntVar(&globalFlags.WebRTCUDPEnd, "webrtcUDPEnd", getEnvAsInt("WEBRTC_UDP_END", 20000), "WebRTC UDP port range end")
|
||||
flag.StringVar(&globalFlags.STUNServer, "stunServer", getEnvAsString("STUN_SERVER", "stun.l.google.com:19302"), "WebRTC STUN server")
|
||||
flag.IntVar(&globalFlags.UDPMuxPort, "webrtcUDPMux", getEnvAsInt("WEBRTC_UDP_MUX", 8088), "WebRTC UDP mux port")
|
||||
flag.BoolVar(&globalFlags.AutoAddLocalIP, "autoAddLocalIP", getEnvAsBool("AUTO_ADD_LOCAL_IP", true), "Automatically add local IP to NAT 1 to 1 IPs")
|
||||
// String with comma separated IPs
|
||||
nat11IP := ""
|
||||
flag.StringVar(&nat11IP, "webrtcNAT11IP", getEnvAsString("WEBRTC_NAT_IP", ""), "WebRTC NAT 1 to 1 IP")
|
||||
flag.StringVar(&globalFlags.PersistDir, "persistDir", getEnvAsString("PERSIST_DIR", "./persist-data"), "Directory to save persistent data to")
|
||||
nat11IPs := ""
|
||||
flag.StringVar(&nat11IPs, "webrtcNAT11IPs", getEnvAsString("WEBRTC_NAT_IPS", ""), "WebRTC NAT 1 to 1 IP(s), comma delimited")
|
||||
flag.StringVar(&globalFlags.TLSCert, "tlsCert", getEnvAsString("TLS_CERT", ""), "Path to TLS certificate")
|
||||
flag.StringVar(&globalFlags.TLSKey, "tlsKey", getEnvAsString("TLS_KEY", ""), "Path to TLS key")
|
||||
flag.StringVar(&globalFlags.ControlSecret, "controlSecret", getEnvAsString("CONTROL_SECRET", ""), "Shared secret for control endpoint")
|
||||
// Parse flags
|
||||
flag.Parse()
|
||||
|
||||
// If debug is enabled, verbose is also enabled
|
||||
if globalFlags.Debug {
|
||||
globalFlags.Verbose = true
|
||||
// If Debug is enabled, set ControlSecret to 1234
|
||||
globalFlags.ControlSecret = "1234"
|
||||
}
|
||||
|
||||
// ICE STUN servers
|
||||
@@ -100,11 +108,24 @@ func InitFlags() {
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize NAT 1 to 1 IPs
|
||||
globalFlags.NAT11IPs = []string{}
|
||||
|
||||
// Get local IP
|
||||
if globalFlags.AutoAddLocalIP {
|
||||
globalFlags.NAT11IPs = append(globalFlags.NAT11IPs, getLocalIP())
|
||||
}
|
||||
|
||||
// Parse NAT 1 to 1 IPs from string
|
||||
if len(nat11IP) > 0 {
|
||||
globalFlags.NAT11IP = nat11IP
|
||||
} else if globalFlags.AutoAddLocalIP {
|
||||
globalFlags.NAT11IP = getLocalIP()
|
||||
if len(nat11IPs) > 0 {
|
||||
split := strings.Split(nat11IPs, ",")
|
||||
if len(split) > 0 {
|
||||
for _, ip := range split {
|
||||
globalFlags.NAT11IPs = append(globalFlags.NAT11IPs, ip)
|
||||
}
|
||||
} else {
|
||||
globalFlags.NAT11IPs = append(globalFlags.NAT11IPs, nat11IPs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
gen "relay/internal/proto"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type TimestampEntry struct {
|
||||
|
||||