From 379db1c87bbe080fb2ed859822a40212ce504b23 Mon Sep 17 00:00:00 2001 From: Wanjohi <71614375+wanjohiryan@users.noreply.github.com> Date: Sun, 8 Dec 2024 14:54:56 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20streaming=20support?= =?UTF-8?q?=20(#125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds: - [x] Keyboard and mouse handling on the frontend - [x] Video and audio streaming from the backend to the frontend - [x] Input server that works with Websockets Update - 17/11 - [ ] Master docker container to run this - [ ] Steam runtime - [ ] Entrypoint.sh --------- Co-authored-by: Kristian Ollikainen <14197772+DatCaptainHorse@users.noreply.github.com> Co-authored-by: Kristian Ollikainen --- .gitignore | 2 + .patches/connectcheckskip.patch | 28 - .patches/devicearg.patch | 23 - Cargo.lock | 3750 +++++++++++++++++ Cargo.toml | 16 + Containerfile.master | 206 + Containerfile.relay | 20 + Containerfile.runner | 219 + apps/docs/README.md | 47 - apps/docs/package.json | 2 +- apps/docs/tsconfig.json | 2 +- apps/www/adapters/deno/vite.config.ts | 23 + apps/www/package.json | 9 +- apps/www/src/entry.deno.ts | 45 + apps/www/src/root.tsx | 2 +- .../src/routes/(moq)/moq/checker/index.tsx | 11 +- .../src/routes/(moq)/moq/checker/tester.ts | 312 +- apps/www/src/routes/home/index.tsx | 48 +- apps/www/src/routes/play/[id]/index.tsx | 287 ++ apps/www/tsconfig.json | 2 +- bun.lockb | Bin 768688 -> 803440 bytes docker-compose.yml | 1 + {.certs => packages/certs}/.gitignore | 0 .../certs}/.terraform.lock.hcl | 0 {.certs => packages/certs}/README.md | 0 {.certs => packages/certs}/input.tf | 0 {.certs => packages/certs}/main.tf | 0 {.certs => packages/certs}/terraform.tfvars | 0 packages/eslint-config/qwik.js | 1 + packages/input/package.json | 9 + packages/input/src/codes.ts | 113 + packages/input/src/index.ts | 3 + packages/input/src/keyboard.ts | 96 + packages/input/src/latency.ts | 54 + packages/input/src/messages.ts | 73 + packages/input/src/mouse.ts | 112 + packages/input/src/types.ts | 52 + packages/input/src/webrtc-stream.ts | 166 + packages/master/go.mod | 32 + packages/master/go.sum | 123 + packages/master/main.go | 80 + packages/moq/.eslintrc.cjs | 2 +- packages/moq/common/async.ts | 16 +- packages/moq/common/error.ts | 12 +- packages/moq/common/hex.ts | 11 + packages/moq/common/ring.ts | 20 +- packages/moq/contribute/audio.ts | 69 +- packages/moq/contribute/broadcast.ts | 261 +- packages/moq/contribute/chunk.ts | 7 - packages/moq/contribute/container.ts | 165 - packages/moq/contribute/segment.ts | 18 +- packages/moq/contribute/track.ts | 30 +- packages/moq/contribute/tsconfig.json | 4 +- packages/moq/contribute/video.ts | 70 +- packages/moq/karp/catalog/audio.ts | 20 + packages/moq/karp/catalog/broadcast.ts | 62 + packages/moq/karp/catalog/index.ts | 7 + packages/moq/karp/catalog/track.ts | 15 + packages/moq/karp/catalog/video.ts | 29 + packages/moq/karp/frame.ts | 64 + packages/moq/{media => karp}/tsconfig.json | 6 +- packages/moq/media/catalog/index.ts | 218 - packages/moq/media/mp4/index.ts | 37 - packages/moq/media/mp4/parser.ts | 71 - packages/moq/media/mp4/rename.ts | 13 - packages/moq/package.json | 15 +- packages/moq/playback/audio.ts | 111 +- packages/moq/playback/backend.ts | 114 - packages/moq/playback/broadcast.ts | 148 + packages/moq/playback/index.ts | 192 +- packages/moq/playback/player.ts | 63 + .../moq/playback/{worker => }/timeline.ts | 15 +- packages/moq/playback/tsconfig.json | 4 +- packages/moq/playback/{worker => }/video.ts | 57 +- packages/moq/playback/worker/audio.ts | 73 - packages/moq/playback/worker/index.ts | 119 - packages/moq/playback/worker/message.ts | 98 - packages/moq/playback/worklet/index.ts | 8 +- packages/moq/playback/worklet/message.ts | 3 +- .../moq/{transport => transfork}/client.ts | 42 +- packages/moq/transfork/connection.ts | 158 + packages/moq/transfork/error.ts | 20 + packages/moq/transfork/frame.ts | 45 + .../moq/{transport => transfork}/index.ts | 4 +- packages/moq/transfork/message.ts | 428 ++ packages/moq/transfork/model.ts | 170 + packages/moq/transfork/publisher.ts | 173 + .../moq/{transport => transfork}/stream.ts | 239 +- packages/moq/transfork/subscriber.ts | 169 + .../{transport => transfork}/tsconfig.json | 0 packages/moq/transport/connection.ts | 95 - packages/moq/transport/control.ts | 550 --- packages/moq/transport/objects.ts | 307 -- packages/moq/transport/publisher.ts | 230 - packages/moq/transport/setup.ts | 163 - packages/moq/transport/subscriber.ts | 197 - packages/moq/tsconfig.json | 6 +- packages/moq/types/mp4box.d.ts | 1848 -------- packages/moq/types/tsconfig.json | 4 - packages/relay/dev/Cargo.toml | 16 + packages/relay/dev/Containerfile | 11 + packages/relay/dev/server.sh | 17 + packages/relay/dev/src/main.rs | 11 + packages/relay/dev/src/room.rs | 292 ++ packages/relay/go.mod | 31 + packages/relay/go.sum | 62 + packages/relay/internal/common.go | 100 + packages/relay/internal/datachannel.go | 72 + packages/relay/internal/egress.go | 189 + packages/relay/internal/flags.go | 82 + packages/relay/internal/http.go | 123 + packages/relay/internal/ingest.go | 251 ++ packages/relay/internal/latency.go | 114 + packages/relay/internal/messages.go | 227 + packages/relay/internal/participant.go | 69 + packages/relay/internal/room.go | 179 + packages/relay/internal/websocket.go | 114 + packages/relay/main.go | 32 + packages/scripts/envs.sh | 12 + {.scripts => packages/scripts}/gpu_helpers.sh | 0 packages/server/.gitignore | 1 + packages/server/Cargo.toml | 28 + packages/server/pkg/PKGBUILD | 32 + packages/server/src/args.rs | 205 + packages/server/src/args/app_args.rs | 59 + packages/server/src/args/device_args.rs | 41 + packages/server/src/args/encoding_args.rs | 190 + packages/server/src/enc_helper.rs | 598 +++ packages/server/src/gpu.rs | 233 + packages/server/src/latency.rs | 60 + packages/server/src/main.rs | 526 +++ packages/server/src/messages.rs | 144 + packages/server/src/room.rs | 540 +++ packages/server/src/websocket.rs | 174 + packages/ui/globals.css | 5 + packages/ui/src/home-nav-bar.tsx | 65 +- packages/ui/src/nav-bar.tsx | 2 +- 137 files changed, 12737 insertions(+), 5234 deletions(-) delete mode 100644 .patches/connectcheckskip.patch delete mode 100644 .patches/devicearg.patch create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Containerfile.master create mode 100644 Containerfile.relay create mode 100644 Containerfile.runner delete mode 100644 apps/docs/README.md create mode 100644 apps/www/adapters/deno/vite.config.ts create mode 100644 apps/www/src/entry.deno.ts create mode 100644 apps/www/src/routes/play/[id]/index.tsx create mode 100644 docker-compose.yml rename {.certs => packages/certs}/.gitignore (100%) rename {.certs => packages/certs}/.terraform.lock.hcl (100%) rename {.certs => packages/certs}/README.md (100%) rename {.certs => packages/certs}/input.tf (100%) rename {.certs => packages/certs}/main.tf (100%) rename {.certs => packages/certs}/terraform.tfvars (100%) create mode 100644 packages/input/package.json create mode 100644 packages/input/src/codes.ts create mode 100644 packages/input/src/index.ts create mode 100644 packages/input/src/keyboard.ts create mode 100644 packages/input/src/latency.ts create mode 100644 packages/input/src/messages.ts create mode 100644 packages/input/src/mouse.ts create mode 100644 packages/input/src/types.ts create mode 100644 packages/input/src/webrtc-stream.ts create mode 100644 packages/master/go.mod create mode 100644 packages/master/go.sum create mode 100644 packages/master/main.go create mode 100644 packages/moq/common/hex.ts delete mode 100644 packages/moq/contribute/chunk.ts delete mode 100644 packages/moq/contribute/container.ts create mode 100644 packages/moq/karp/catalog/audio.ts create mode 100644 packages/moq/karp/catalog/broadcast.ts create mode 100644 packages/moq/karp/catalog/index.ts create mode 100644 packages/moq/karp/catalog/track.ts create mode 100644 packages/moq/karp/catalog/video.ts create mode 100644 packages/moq/karp/frame.ts rename packages/moq/{media => karp}/tsconfig.json (60%) delete mode 100644 packages/moq/media/catalog/index.ts delete mode 100644 packages/moq/media/mp4/index.ts delete mode 100644 packages/moq/media/mp4/parser.ts delete mode 100644 packages/moq/media/mp4/rename.ts delete mode 100644 packages/moq/playback/backend.ts create mode 100644 packages/moq/playback/broadcast.ts create mode 100644 packages/moq/playback/player.ts rename packages/moq/playback/{worker => }/timeline.ts (85%) rename packages/moq/playback/{worker => }/video.ts (50%) delete mode 100644 packages/moq/playback/worker/audio.ts delete mode 100644 packages/moq/playback/worker/index.ts delete mode 100644 packages/moq/playback/worker/message.ts rename packages/moq/{transport => transfork}/client.ts (53%) create mode 100644 packages/moq/transfork/connection.ts create mode 100644 packages/moq/transfork/error.ts create mode 100644 packages/moq/transfork/frame.ts rename packages/moq/{transport => transfork}/index.ts (51%) create mode 100644 packages/moq/transfork/message.ts create mode 100644 packages/moq/transfork/model.ts create mode 100644 packages/moq/transfork/publisher.ts rename packages/moq/{transport => transfork}/stream.ts (52%) create mode 100644 packages/moq/transfork/subscriber.ts rename packages/moq/{transport => transfork}/tsconfig.json (100%) delete mode 100644 packages/moq/transport/connection.ts delete mode 100644 packages/moq/transport/control.ts delete mode 100644 packages/moq/transport/objects.ts delete mode 100644 packages/moq/transport/publisher.ts delete mode 100644 packages/moq/transport/setup.ts delete mode 100644 packages/moq/transport/subscriber.ts delete mode 100644 packages/moq/types/mp4box.d.ts delete mode 100644 packages/moq/types/tsconfig.json create mode 100644 packages/relay/dev/Cargo.toml create mode 100644 packages/relay/dev/Containerfile create mode 100755 packages/relay/dev/server.sh create mode 100644 packages/relay/dev/src/main.rs create mode 100644 packages/relay/dev/src/room.rs create mode 100644 packages/relay/go.mod create mode 100644 packages/relay/go.sum create mode 100644 packages/relay/internal/common.go create mode 100644 packages/relay/internal/datachannel.go create mode 100644 packages/relay/internal/egress.go create mode 100644 packages/relay/internal/flags.go create mode 100644 packages/relay/internal/http.go create mode 100644 packages/relay/internal/ingest.go create mode 100644 packages/relay/internal/latency.go create mode 100644 packages/relay/internal/messages.go create mode 100644 packages/relay/internal/participant.go create mode 100644 packages/relay/internal/room.go create mode 100644 packages/relay/internal/websocket.go create mode 100644 packages/relay/main.go create mode 100644 packages/scripts/envs.sh rename {.scripts => packages/scripts}/gpu_helpers.sh (100%) create mode 100644 packages/server/.gitignore create mode 100644 packages/server/Cargo.toml create mode 100644 packages/server/pkg/PKGBUILD create mode 100644 packages/server/src/args.rs create mode 100644 packages/server/src/args/app_args.rs create mode 100644 packages/server/src/args/device_args.rs create mode 100644 packages/server/src/args/encoding_args.rs create mode 100644 packages/server/src/enc_helper.rs create mode 100644 packages/server/src/gpu.rs create mode 100644 packages/server/src/latency.rs create mode 100644 packages/server/src/main.rs create mode 100644 packages/server/src/messages.rs create mode 100644 packages/server/src/room.rs create mode 100644 packages/server/src/websocket.rs diff --git a/.gitignore b/.gitignore index c9a7c427..6103291d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ node_modules .env.test.local .env.production.local +.idea/ + # Testing coverage diff --git a/.patches/connectcheckskip.patch b/.patches/connectcheckskip.patch deleted file mode 100644 index 01b25ad2..00000000 --- a/.patches/connectcheckskip.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/src/utils.c b/src/utils.c -index e00f3c5..4f1f0bf 100644 ---- a/src/utils.c -+++ b/src/utils.c -@@ -71,7 +71,7 @@ void for_each_active_monitor_output_x11(Display *display, active_monitor_callbac - char display_name[256]; - for(int i = 0; i < screen_res->noutput; ++i) { - XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]); -- if(out_info && out_info->crtc && out_info->connection == RR_Connected) { -+ if(out_info && out_info->crtc) { - XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc); - if(crt_info && crt_info->mode) { - const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode); -@@ -218,10 +218,10 @@ static void for_each_active_monitor_output_drm(const gsr_egl *egl, active_monito - if(connector_type) - ++connector_type->count; - -- if(connector->connection != DRM_MODE_CONNECTED) { -- drmModeFreeConnector(connector); -- continue; -- } -+ //if(connector->connection != DRM_MODE_CONNECTED) { -+ // drmModeFreeConnector(connector); -+ // continue; -+ //} - - if(connector_type) - ++connector_type->count_active; diff --git a/.patches/devicearg.patch b/.patches/devicearg.patch deleted file mode 100644 index 12fd803c..00000000 --- a/.patches/devicearg.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/src/main.cpp b/src/main.cpp -index 112a6ac..57bd9bf 100644 ---- a/src/main.cpp -+++ b/src/main.cpp -@@ -1906,6 +1906,7 @@ int main(int argc, char **argv) { - { "-gopm", Arg { {}, true, false } }, // deprecated, used keyint instead - { "-keyint", Arg { {}, true, false } }, - { "-encoder", Arg { {}, true, false } }, -+ { "-device", Arg { {}, true, false } }, - }; - - for(int i = 1; i < argc; i += 2) { -@@ -2226,6 +2227,10 @@ int main(int argc, char **argv) { - overclock = false; - } - -+ const char *dri_device = args["-device"].value(); -+ if (dri_device) -+ egl.dri_card_path = dri_device; -+ - egl.card_path[0] = '\0'; - if(wayland || egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA) { - // TODO: Allow specifying another card, and in other places diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..b28e9ec9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3750 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive 0.4.0", + "asn1-rs-impl 0.1.0", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.66", +] + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl 0.2.0", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.66", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure 0.13.1", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "aws-lc-rs" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7c2840b66236045acd2607d5866e274380afd87ef99d6226e961e2cb47df45" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3a619a9de81e1d7de1f1186dcba4506ed661a0e483d84410fdef0ee87b2f96" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.87", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-expr" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c360837f8f19e2e4468275138f1c0dec1647d1e17bb7c0215fe3cd7530e93c25" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs 0.5.2", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dev" +version = "0.1.0" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "tokio", + "webrtc", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio-sys" +version = "0.21.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=main#c3fffcacfb5c420c12475bbb853318dc54ce531e" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.59.0", +] + +[[package]] +name = "glib" +version = "0.21.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=main#c3fffcacfb5c420c12475bbb853318dc54ce531e" +dependencies = [ + "bitflags 2.6.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.21.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=main#c3fffcacfb5c420c12475bbb853318dc54ce531e" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "glib-sys" +version = "0.21.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=main#c3fffcacfb5c420c12475bbb853318dc54ce531e" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gobject-sys" +version = "0.21.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=main#c3fffcacfb5c420c12475bbb853318dc54ce531e" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "gstreamer" +version = "0.24.0" +source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#36eca7cca935f930a3dfa006688a199084f4197a" +dependencies = [ + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib", + "gstreamer-sys", + "itertools 0.13.0", + "kstring", + "libc", + "muldiv", + "num-integer", + "num-rational", + "option-operations", + "paste", + "pin-project-lite", + "smallvec", + "thiserror 2.0.3", +] + +[[package]] +name = "gstreamer-app" +version = "0.24.0" +source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#36eca7cca935f930a3dfa006688a199084f4197a" +dependencies = [ + "futures-core", + "futures-sink", + "glib", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "libc", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.24.0" +source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#36eca7cca935f930a3dfa006688a199084f4197a" +dependencies = [ + "glib-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-base" +version = "0.24.0" +source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#36eca7cca935f930a3dfa006688a199084f4197a" +dependencies = [ + "atomic_refcell", + "cfg-if", + "glib", + "gstreamer", + "gstreamer-base-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.24.0" +source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#36eca7cca935f930a3dfa006688a199084f4197a" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-sys" +version = "0.24.0" +source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#36eca7cca935f930a3dfa006688a199084f4197a" +dependencies = [ + "cfg-if", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "hyper" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "interceptor" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4705c00485029e738bea8c9505b5ddb1486a8f3627a953e1e77e6abdf5eef90c" +dependencies = [ + "async-trait", + "bytes", + "log", + "portable-atomic", + "rand", + "rtcp", + "rtp", + "thiserror 1.0.66", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "muldiv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nestri-server" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "flate2", + "futures-util", + "gstreamer", + "gstreamer-app", + "log", + "num-derive", + "num-traits", + "rand", + "regex", + "rustls", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite", + "tokio-util", + "webrtc", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-operations" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +dependencies = [ + "paste", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.87", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rcgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rtcp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc9f775ff89c5fe7f0cc0abafb7c57688ae25ce688f1a52dd88e277616c76ab2" +dependencies = [ + "bytes", + "thiserror 1.0.66", + "webrtc-util", +] + +[[package]] +name = "rtp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6870f09b5db96f8b9e7290324673259fd15519ebb7d55acf8e7eb044a9ead6af" +dependencies = [ + "bytes", + "portable-atomic", + "rand", + "serde", + "thiserror 1.0.66", + "webrtc-util", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdp" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13254db766b17451aced321e7397ebf0a446ef0c8d2942b6e67a95815421093f" +dependencies = [ + "rand", + "substring", + "thiserror 1.0.66", + "url", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "stun" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28fad383a1cc63ae141e84e48eaef44a1063e9d9e55bcb8f51a99b886486e01b" +dependencies = [ + "base64 0.21.7", + "crc", + "lazy_static", + "md-5", + "rand", + "ring", + "subtle", + "thiserror 1.0.66", + "tokio", + "url", + "webrtc-util", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +dependencies = [ + "thiserror-impl 1.0.66", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand", + "sha1", + "thiserror 1.0.66", + "utf-8", +] + +[[package]] +name = "turn" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b000cebd930420ac1ed842c8128e3b3412512dfd5b82657eab035a3f5126acc" +dependencies = [ + "async-trait", + "base64 0.21.7", + "futures", + "log", + "md-5", + "portable-atomic", + "rand", + "ring", + "stun", + "thiserror 1.0.66", + "tokio", + "tokio-util", + "webrtc-util", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webrtc" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b3a840e31c969844714f93b5a87e73ee49f3bc2a4094ab9132c69497eb31db" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "cfg-if", + "hex", + "interceptor", + "lazy_static", + "log", + "portable-atomic", + "rand", + "rcgen", + "regex", + "ring", + "rtcp", + "rtp", + "rustls", + "sdp", + "serde", + "serde_json", + "sha2", + "smol_str", + "stun", + "thiserror 1.0.66", + "time", + "tokio", + "turn", + "url", + "waitgroup", + "webrtc-data", + "webrtc-dtls", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b7c550f8d35867b72d511640adf5159729b9692899826fe00ba7fa74f0bf70" +dependencies = [ + "bytes", + "log", + "portable-atomic", + "thiserror 1.0.66", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-dtls" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e5eedbb0375aa04da93fc3a189b49ed3ed9ee844b6997d5aade14fc3e2c26e" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bincode", + "byteorder", + "cbc", + "ccm", + "der-parser 8.2.0", + "hkdf", + "hmac", + "log", + "p256", + "p384", + "portable-atomic", + "rand", + "rand_core", + "rcgen", + "ring", + "rustls", + "sec1", + "serde", + "sha1", + "sha2", + "subtle", + "thiserror 1.0.66", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser", +] + +[[package]] +name = "webrtc-ice" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4f0ca6d4df8d1bdd34eece61b51b62540840b7a000397bcfb53a7bfcf347c8" +dependencies = [ + "arc-swap", + "async-trait", + "crc", + "log", + "portable-atomic", + "rand", + "serde", + "serde_json", + "stun", + "thiserror 1.0.66", + "tokio", + "turn", + "url", + "uuid", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0804694f3b2acfdff48f6df217979b13cb0a00377c63b5effd111daaee7e8c4" +dependencies = [ + "log", + "socket2", + "thiserror 1.0.66", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c15b20e98167b22949abc1c20eca7c6d814307d187068fe7a48f0b87a4f6d46" +dependencies = [ + "byteorder", + "bytes", + "rand", + "rtp", + "thiserror 1.0.66", +] + +[[package]] +name = "webrtc-sctp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d850daa68639b9d7bb16400676e97525d1e52b15b4928240ae2ba0e849817a5" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc", + "log", + "portable-atomic", + "rand", + "thiserror 1.0.66", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbec5da43a62c228d321d93fb12cc9b4d9c03c9b736b0c215be89d8bd0774cfe" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "byteorder", + "bytes", + "ctr", + "hmac", + "log", + "rtcp", + "rtp", + "sha1", + "subtle", + "thiserror 1.0.66", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8d9bc631768958ed97b8d68b5d301e63054ae90b09083d43e2fefb939fd77e" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "libc", + "log", + "nix", + "portable-atomic", + "rand", + "thiserror 1.0.66", + "tokio", + "winapi", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 1.0.66", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure 0.13.1", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure 0.13.1", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..c99fb87b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +resolver = "2" +members = [ + "packages/server", + "packages/relay/dev" +] + +[workspace.package] +version = "0.1.0-alpha.1" +repository = "https://github.com/nestriness/nestri" +edition = "2021" +rust-version = "1.80" + +[workspace.dependencies] +gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", version = "0.24.0" } +gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", version = "0.24.0" } diff --git a/Containerfile.master b/Containerfile.master new file mode 100644 index 00000000..14777233 --- /dev/null +++ b/Containerfile.master @@ -0,0 +1,206 @@ +#! Runs the docker server that handles everything else +#****************************************************************************** +# base +#****************************************************************************** +FROM archlinux:base-20241027.0.273886 AS base +# How to run - docker run -it --rm --device /dev/dri nestri /bin/bash - DO NOT forget the ports +# TODO: Migrate XDG_RUNTIME_DIR to /run/user/1000 +# TODO: Add nestri-server to pulseaudio.conf +# TODO: Add our own entrypoint, with our very own zombie ripper 🧟🏾‍♀️ +# FIXME: Add user root to `pulse-access` group as well :D +# TODO: Test the whole damn thing + +# Update the pacman repo +RUN \ + pacman -Syu --noconfirm + +#****************************************************************************** +# builder +#****************************************************************************** + +FROM base AS builder + +RUN \ + pacman -Su --noconfirm \ + base-devel \ + git \ + sudo \ + vim + +WORKDIR /scratch + +# Allow nobody user to invoke pacman to install packages (as part of makepkg) and modify the system. +# This should never exist in a running image, just used by *-build Docker stages. +RUN \ + echo "nobody ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers; + +ENV ARTIFACTS=/artifacts \ + CARGO_TARGET_DIR=/build + +RUN \ + mkdir -p /artifacts \ + && mkdir -p /build + +RUN \ + chgrp nobody /scratch /artifacts /build \ + && chmod g+ws /scratch /artifacts /build + +#****************************************************************************** +# rust-builder +#****************************************************************************** + +FROM builder AS rust-builder + +RUN \ + pacman -Su --noconfirm \ + rustup + +RUN \ + rustup default stable + +#****************************************************************************** +# nestri-server-builder +#****************************************************************************** +# Builds nestri server binary +FROM rust-builder AS nestri-server-builder + +RUN \ + pacman -Su --noconfirm \ + wayland \ + vpl-gpu-rt \ + gstreamer \ + gst-plugin-va \ + gst-plugins-base \ + gst-plugins-good \ + mesa-utils \ + weston \ + xorg-xwayland + + +#****************************************************************************** +# nestri-server-build +#****************************************************************************** + +FROM nestri-server-builder AS nestri-server-build + +#Allow makepkg to be run as nobody. +RUN chgrp -R nobody /scratch && chmod -R g+ws /scratch + +# USER nobody + +# Perform the server build. +WORKDIR /scratch/server + +RUN \ + git clone https://github.com/nestriness/nestri + +WORKDIR /scratch/server/nestri + +RUN \ + git checkout feat/stream \ + && cargo build -j$(nproc) --release + +# COPY packages/server/build/ /scratch/server/ + +# RUN makepkg && cp *.zst "$ARTIFACTS" +#****************************************************************************** +# runtime_base_pkgs +#****************************************************************************** + +FROM base AS runtime_base_pkgs + +COPY --from=nestri-server-build /build/release/nestri-server /usr/bin/ + +#****************************************************************************** +# runtime_base +#****************************************************************************** + +FROM runtime_base_pkgs AS runtime_base + +RUN \ + pacman -Su --noconfirm \ + weston \ + sudo \ + xorg-xwayland \ + gstreamer \ + gst-plugins-base \ + gst-plugins-good \ + gst-plugin-qsv \ + gst-plugin-va \ + gst-plugin-fmp4 \ + mesa \ + # Grab GPU encoding packages + # Intel (modern VPL + VA-API) + vpl-gpu-rt \ + intel-media-driver \ + # AMD/ATI (VA-API) + libva-mesa-driver \ + # NVIDIA (proprietary) + nvidia-utils \ + # Audio + pulseaudio \ + # Supervisor + supervisor + +RUN \ + # Set up our non-root user $(nestri) + groupadd -g 1000 nestri \ + && useradd -ms /bin/bash nestri -u 1000 -g 1000 \ + && passwd -d nestri \ + # Setup Pulseaudio + && useradd -d /var/run/pulse -s /usr/bin/nologin -G audio pulse \ + && groupadd pulse-access \ + && usermod -aG audio,input,render,video,pulse-access nestri \ + && echo "nestri ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers \ + && echo "Users created" \ + # Create an empty machine-id file + && touch /etc/machine-id + +ENV \ + XDG_RUNTIME_DIR=/tmp + +#****************************************************************************** +# runtime +#****************************************************************************** + +FROM runtime_base AS runtime +# Setup supervisor # +RUN <<-EOF + echo -e " + [supervisord] + user=root + nodaemon=true + loglevel=info + logfile=/tmp/supervisord.log + pidfile=/tmp/supervisord.pid + + [program:dbus] + user=root + command=dbus-daemon --system --nofork + logfile=/tmp/dbus.log + pidfile=/tmp/dbus.pid + stopsignal=INT + autostart=true + autorestart=true + priority=1 + + [program:pulseaudio] + user=root + command=pulseaudio --daemonize=no --system --disallow-module-loading --disallow-exit --exit-idle-time=-1 + logfile=/tmp/pulseaudio.log + pidfile=/tmp/pulseaudio.pid + stopsignal=INT + autostart=true + autorestart=true + priority=10 + " | tee /etc/supervisord.conf +EOF + +RUN \ + chown -R nestri:nestri /tmp /etc/supervisord.conf + +ENV USER=nestri +USER 1000 + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] +# Debug - pactl list \ No newline at end of file diff --git a/Containerfile.relay b/Containerfile.relay new file mode 100644 index 00000000..ee36a64e --- /dev/null +++ b/Containerfile.relay @@ -0,0 +1,20 @@ +FROM docker.io/golang:1.23-alpine AS go-build +WORKDIR /builder +COPY packages/relay/ /builder/ +RUN go build + +FROM docker.io/golang:1.23-alpine +COPY --from=go-build /builder/relay /relay/relay +WORKDIR /relay + +# ENV flags +ENV VERBOSE=false +ENV ENDPOINT_PORT=8088 +ENV WEBRTC_UDP_START=10000 +ENV WEBRTC_UDP_END=20000 +ENV STUN_SERVER="stun.l.google.com:19302" + +EXPOSE $ENDPOINT_PORT +EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp + +ENTRYPOINT ["/relay/relay"] \ No newline at end of file diff --git a/Containerfile.runner b/Containerfile.runner new file mode 100644 index 00000000..db0bfb0b --- /dev/null +++ b/Containerfile.runner @@ -0,0 +1,219 @@ +# Container build arguments # +ARG BASE_IMAGE=docker.io/cachyos/cachyos-v3:latest + +#****************************************************************************** +# gst-builder +#****************************************************************************** +FROM ${BASE_IMAGE} AS gst-builder +WORKDIR /builder/ + +# Grab build and rust packages # +RUN pacman -Syu --noconfirm meson pkgconf cmake git gcc make rustup \ + gstreamer gst-plugins-base gst-plugins-good + +# Setup stable rust toolchain # +RUN rustup default stable +# Clone nestri source # +RUN git clone -b feat/stream https://github.com/nestriness/nestri.git + +# Build nestri # +RUN cd nestri/packages/server/ && \ + cargo build --release + +#****************************************************************************** +# gstwayland-builder +#****************************************************************************** +FROM ${BASE_IMAGE} AS gstwayland-builder +WORKDIR /builder/ + +# Grab build and rust packages # +RUN pacman -Syu --noconfirm meson pkgconf cmake git gcc make rustup \ + libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput + +# Setup stable rust toolchain # +RUN rustup default stable +# Build required cargo-c package # +RUN cargo install cargo-c +# Clone gst plugin source # +RUN git clone https://github.com/games-on-whales/gst-wayland-display.git + +# Build gst plugin # +RUN mkdir plugin && \ + cd gst-wayland-display && \ + cargo cinstall --prefix=/builder/plugin/ + + +#****************************************************************************** +# runtime +#****************************************************************************** +FROM ${BASE_IMAGE} AS runtime + +## Nestri Env Variables ## +ENV NESTRI_PARAMS="" +ENV RESOLUTION="1280x720" + +## Install Graphics, Media, and Audio packages ## +RUN pacman -Syu --noconfirm --needed \ + # Graphics packages + sudo mesa mesa-utils xorg-xwayland labwc wlr-randr mangohud \ + # Vulkan drivers + vulkan-intel vulkan-radeon nvidia-utils \ + # Media encoding packages + vpl-gpu-rt intel-media-driver libva-utils \ + # GStreamer plugins + gstreamer gst-plugins-base gst-plugins-good \ + gst-plugin-va gst-plugins-bad gst-plugin-fmp4 \ + gst-plugin-qsv gst-plugin-pipewire gst-plugin-rswebrtc \ + gst-plugins-ugly gst-plugin-rsrtp \ + # Audio packages + pipewire pipewire-pulse pipewire-alsa wireplumber \ + # Other requirements + supervisor \ + # Custom + umu-launcher && \ + # Clean up pacman cache and unnecessary files + pacman -Scc --noconfirm && \ + rm -rf /var/cache/pacman/pkg/* /tmp/* /var/tmp/* && \ + # Optionally clean documentation, man pages, and locales + find /usr/share/locale -mindepth 1 -maxdepth 1 ! -name "en*" -exec rm -rf {} + && \ + rm -rf /usr/share/doc /usr/share/man /usr/share/info + + +## User ## +# Create and setup user # +ENV USER="nestri" \ + UID=99 \ + GID=100 \ + USER_PASSWORD="nestri1234" \ + USER_HOME="/home/nestri" + +RUN mkdir -p ${USER_HOME} && \ + useradd -d ${USER_HOME} -u ${UID} -s /bin/bash ${USER} && \ + chown -R ${USER} ${USER_HOME} && \ + echo "${USER} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ + echo "${USER}:${USER_PASSWORD}" | chpasswd + +# Run directory # +RUN mkdir -p /run/user/${UID} && \ + chown ${USER}:${USER} /run/user/${UID} + +# Home config directory # +RUN mkdir -p ${USER_HOME}/.config && \ + chown ${USER}:${USER} ${USER_HOME}/.config + +# Groups # +RUN usermod -aG input root && usermod -aG input ${USER} && \ + usermod -aG video root && usermod -aG video ${USER} && \ + usermod -aG render root && usermod -aG render ${USER} + +## Copy files from builders ## +# this is done here at end to not trigger full rebuild on changes to builder +# nestri +COPY --from=gst-builder /builder/nestri/target/release/nestri-server /usr/bin/nestri-server +# gstwayland +COPY --from=gstwayland-builder /builder/plugin/include/libgstwaylanddisplay /usr/include/ +COPY --from=gstwayland-builder /builder/plugin/lib/*libgstwayland* /usr/lib/ +COPY --from=gstwayland-builder /builder/plugin/lib/gstreamer-1.0/libgstwayland* /usr/lib/gstreamer-1.0/ +COPY --from=gstwayland-builder /builder/plugin/lib/pkgconfig/gstwayland* /usr/lib/pkgconfig/ +COPY --from=gstwayland-builder /builder/plugin/lib/pkgconfig/libgstwayland* /usr/lib/pkgconfig/ + +## Copy scripts ## +COPY packages/scripts/ /etc/nestri/ + +## Startup ## +# Setup supervisor # +RUN <<-EOF +echo -e " +[supervisord] +user=root +nodaemon=true +loglevel=info +logfile=/tmp/supervisord.log + +[program:dbus] +user=root +command=dbus-daemon --system --nofork --nopidfile +logfile=/tmp/dbus.log +autoerestart=true +autostart=true +startretries=3 +priority=1 + +[program:seatd] +user=root +command=seatd +logfile=/tmp/seatd.log +autoerestart=true +autostart=true +startretries=3 +priority=2 + +[program:pipewire] +user=nestri +command=dbus-launch pipewire +environment=XDG_RUNTIME_DIR=\"/run/user/${UID}\",HOME=\"${USER_HOME}\" +logfile=/tmp/pipewire.log +autoerestart=true +autostart=true +startretries=3 +priority=10 + +[program:pipewire-pulse] +user=nestri +command=dbus-launch pipewire-pulse +environment=XDG_RUNTIME_DIR=\"/run/user/${UID}\",HOME=\"${USER_HOME}\" +logfile=/tmp/pipewire-pulse.log +autoerestart=true +autostart=true +startretries=3 +priority=20 + +[program:wireplumber] +user=nestri +command=dbus-launch wireplumber +environment=XDG_RUNTIME_DIR=\"/run/user/${UID}\",HOME=\"${USER_HOME}\" +logfile=/tmp/wireplumber.log +autoerestart=true +autostart=true +startretries=3 +priority=30 + +[program:nestri-server] +user=nestri +command=sh -c 'nestri-server \$NESTRI_PARAMS' +environment=XDG_RUNTIME_DIR=\"/run/user/${UID}\",HOME=\"${USER_HOME}\" +logfile=/tmp/nestri-server.log +autoerestart=true +autostart=true +startretries=3 +priority=50 + +[program:labwc] +user=nestri +command=sh -c 'sleep 4 && rm -rf /tmp/.X11-unix && mkdir -p /tmp/.X11-unix && chown nestri:nestri /tmp/.X11-unix && labwc' +environment=XDG_RUNTIME_DIR=\"/run/user/${UID}\",HOME=\"${USER_HOME}\",WAYLAND_DISPLAY=\"wayland-1\",WLR_BACKENDS=\"wayland\",WLR_RENDERER=\"vulkan\" +logfile=/tmp/labwc.log +autoerestart=true +autostart=true +startretries=5 +priority=60 + +[program:wlrrandr] +user=nestri +command=sh -c 'sleep 6 && wlr-randr --output WL-1 --custom-mode \$RESOLUTION && read -n 1' +environment=XDG_RUNTIME_DIR=\"/run/user/${UID}\",HOME=\"${USER_HOME}\",WAYLAND_DISPLAY=\"wayland-0\" +logfile=/tmp/wlrrandr.log +autoerestart=true +autostart=true +startretries=10 +priority=70 +" | tee /etc/supervisord.conf +EOF + +# Wireplumber disable suspend # +# Remove suspend node +RUN sed -z -i 's/{[[:space:]]*name = node\/suspend-node\.lua,[[:space:]]*type = script\/lua[[:space:]]*provides = hooks\.node\.suspend[[:space:]]*}[[:space:]]*//g' /usr/share/wireplumber/wireplumber.conf +# Remove "hooks.node.suspend" want +RUN sed -i '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' /usr/share/wireplumber/wireplumber.conf + +ENTRYPOINT ["supervisord", "-c", "/etc/supervisord.conf"] diff --git a/apps/docs/README.md b/apps/docs/README.md deleted file mode 100644 index 95051065..00000000 --- a/apps/docs/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Docus - -## Setup - -Install dependencies: - -```bash -npm install -``` - -## Development - -```bash -npm run dev -``` - -## Edge Side Rendering - -Can be deployed to Vercel Functions, Netlify Functions, AWS, and most Node-compatible environments. - -Look at all the available presets [here](https://v3.nuxtjs.org/guide/deploy/presets). - -```bash -npm build -``` - -## Static Generation - -Use the `generate` command to build your application. - -The HTML files will be generated in the .output/public directory and ready to be deployed to any static compatible hosting. - -```bash -npm run generate -``` - -## Preview build - -You might want to preview the result of your build locally, to do so, run the following command: - -```bash -yarn preview -``` - ---- - -For a detailed explanation of how things work, check out [Docus](https://docus.dev). diff --git a/apps/docs/package.json b/apps/docs/package.json index 261a83cc..27f773e5 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "nuxi dev", + "nestri.dev": "nuxi dev", "build": "nuxi build", "generate": "nuxi generate", "preview": "nuxi preview", diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index ad2f8d27..59e048dc 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "./.nuxt/tsconfig.json", + // "extends": "./.nuxt/tsconfig.json", "ignoreConfigErrors": true } diff --git a/apps/www/adapters/deno/vite.config.ts b/apps/www/adapters/deno/vite.config.ts new file mode 100644 index 00000000..bc45d6aa --- /dev/null +++ b/apps/www/adapters/deno/vite.config.ts @@ -0,0 +1,23 @@ +import { denoServerAdapter } from "@builder.io/qwik-city/adapters/deno-server/vite"; +import { extendConfig } from "@builder.io/qwik-city/vite"; +import baseConfig from "../../vite.config"; + +export default extendConfig(baseConfig, () => { + return { + build: { + ssr: true, + rollupOptions: { + input: ["src/entry.deno.ts", "@qwik-city-plan"], + }, + minify: false, + }, + plugins: [ + denoServerAdapter({ + ssg: { + include: ["/*"], + origin: "https://yoursite.dev", + }, + }), + ], + }; +}); diff --git a/apps/www/package.json b/apps/www/package.json index 7e56b847..b4d9828e 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -16,6 +16,7 @@ "build.client": "vite build", "build.preview": "vite build --ssr src/entry.preview.tsx", "build.server": "vite build -c adapters/cloudflare-pages/vite.config.ts", + "deno:build.server": "vite build -c adapters/deno/vite.config.ts", "build.types": "tsc --incremental --noEmit", "deploy": "wrangler pages deploy ./dist", "dev": "vite --mode ssr", @@ -25,6 +26,7 @@ "lint": "eslint \"src/**/*.ts*\"", "preview": "qwik build preview && vite preview --open", "serve": "wrangler pages dev ./dist --compatibility-flags=nodejs_als", + "deno:serve": "deno run --allow-net --allow-read --allow-env server/entry.deno.js", "start": "vite --open --mode ssr", "qwik": "qwik" }, @@ -34,7 +36,8 @@ "@builder.io/qwik-react": "0.5.0", "@modular-forms/qwik": "^0.27.0", "@nestri/eslint-config": "*", - "@nestri/moq": "*", + "@nestri/input": "*", + "@nestri/libmoq": "*", "@nestri/typescript-config": "*", "@nestri/ui": "*", "@types/eslint": "8.56.10", @@ -54,5 +57,9 @@ "vite": "5.3.5", "vite-tsconfig-paths": "^4.2.1", "wrangler": "^3.0.0" + }, + "dependencies": { + "@types/pako": "^2.0.3", + "pako": "^2.1.0" } } diff --git a/apps/www/src/entry.deno.ts b/apps/www/src/entry.deno.ts new file mode 100644 index 00000000..96a4d5d2 --- /dev/null +++ b/apps/www/src/entry.deno.ts @@ -0,0 +1,45 @@ +/* + * WHAT IS THIS FILE? + * + * It's the entry point for the Deno HTTP server when building for production. + * + * Learn more about the Deno integration here: + * - https://qwik.dev/docs/deployments/deno/ + * - https://docs.deno.com/runtime/tutorials/http_server + * + */ +import { createQwikCity } from "@builder.io/qwik-city/middleware/deno"; +import qwikCityPlan from "@qwik-city-plan"; +import { manifest } from "@qwik-client-manifest"; +import render from "./entry.ssr"; + +// Create the Qwik City Deno middleware +const { router, notFound, staticFile } = createQwikCity({ + render, + qwikCityPlan, + manifest, +}); + +// Allow for dynamic port +const port = Number(Deno.env.get("PORT") ?? 3009); + +/* eslint-disable */ +console.log(`Server starter: http://localhost:${port}/app/`); + +Deno.serve({ port }, async (request: Request, info: any) => { + const staticResponse = await staticFile(request); + if (staticResponse) { + return staticResponse; + } + + // Server-side render this request with Qwik City + const qwikCityResponse = await router(request, info); + if (qwikCityResponse) { + return qwikCityResponse; + } + + // Path not found + return notFound(request); +}); + +declare const Deno: any; diff --git a/apps/www/src/root.tsx b/apps/www/src/root.tsx index b9f71f3a..b7851bae 100644 --- a/apps/www/src/root.tsx +++ b/apps/www/src/root.tsx @@ -34,7 +34,7 @@ export default component$(() => { {/* {!isDev && } */} diff --git a/apps/www/src/routes/(moq)/moq/checker/index.tsx b/apps/www/src/routes/(moq)/moq/checker/index.tsx index aee03dc5..612f2666 100644 --- a/apps/www/src/routes/(moq)/moq/checker/index.tsx +++ b/apps/www/src/routes/(moq)/moq/checker/index.tsx @@ -1,5 +1,6 @@ import * as v from "valibot" -import { Broadcast } from "./tester"; +//FIXME: Make sure this works +// import { Broadcast } from "./tester"; import { cn } from "@nestri/ui/design"; import { routeLoader$ } from "@builder.io/qwik-city"; import { component$, $, useSignal } from "@builder.io/qwik"; @@ -36,11 +37,11 @@ export default component$(() => { const handleSubmit = $>(async (values) => { const randomNamespace = generateRandomWord(6); - const sub = await Broadcast.init({ url: values.url, fingerprint: undefined, namespace: randomNamespace }) + // const sub = await Broadcast.init({ url: values.url, fingerprint: undefined, namespace: randomNamespace }) - setTimeout(() => { - broadcasterOk.value = sub.isSubscribed() - }, 1000); + // setTimeout(() => { + // broadcasterOk.value = sub.isSubscribed() + // }, 1000); }); return ( diff --git a/apps/www/src/routes/(moq)/moq/checker/tester.ts b/apps/www/src/routes/(moq)/moq/checker/tester.ts index 11446ea8..09cca337 100644 --- a/apps/www/src/routes/(moq)/moq/checker/tester.ts +++ b/apps/www/src/routes/(moq)/moq/checker/tester.ts @@ -1,208 +1,208 @@ -import type { Connection, SubscribeRecv } from "@nestri/moq/transport" -import { asError } from "@nestri/moq/common/error" -import { Client } from "@nestri/moq/transport/client" -import * as Catalog from "@nestri/moq/media/catalog" -import { type GroupWriter } from "@nestri/moq/transport/objects" +// import type { Connection, SubscribeRecv } from "@nestri/libmoq/transport" +// import { asError } from "@nestri/moq/common/error" +// import { Client } from "@nestri/moq/transport/client" +// import * as Catalog from "@nestri/moq/media/catalog" +// import { type GroupWriter } from "@nestri/moq/transport/objects" -export interface BroadcastConfig { - namespace: string - connection: Connection -} -export interface BroadcasterConfig { - url: string - namespace: string - fingerprint?: string // URL to fetch TLS certificate fingerprint -} +// export interface BroadcastConfig { +// namespace: string +// connection: Connection +// } +// export interface BroadcasterConfig { +// url: string +// namespace: string +// fingerprint?: string // URL to fetch TLS certificate fingerprint +// } -export interface BroadcastConfigTrack { - input: string - bitrate: number -} +// export interface BroadcastConfigTrack { +// input: string +// bitrate: number +// } -export class Broadcast { - stream: GroupWriter | null - subscriber: SubscribeRecv | null - subscribed: boolean; +// export class Broadcast { +// stream: GroupWriter | null +// subscriber: SubscribeRecv | null +// subscribed: boolean; - readonly config: BroadcastConfig - readonly catalog: Catalog.Root - readonly connection: Connection - readonly namespace: string +// readonly config: BroadcastConfig +// readonly catalog: Catalog.Root +// readonly connection: Connection +// readonly namespace: string - #running: Promise +// #running: Promise - constructor(config: BroadcastConfig) { - this.subscribed = false - this.namespace = config.namespace - this.connection = config.connection - this.config = config - //Arbitrary values, just to keep TypeScript happy :) - this.catalog = { - version: 1, - streamingFormat: 1, - streamingFormatVersion: "0.2", - supportsDeltaUpdates: false, - commonTrackFields: { - packaging: "loc", - renderGroup: 1, - }, - tracks: [{ - name: "tester", - namespace: "tester", - selectionParams: {} - }], - } - this.stream = null - this.subscriber = null +// constructor(config: BroadcastConfig) { +// this.subscribed = false +// this.namespace = config.namespace +// this.connection = config.connection +// this.config = config +// //Arbitrary values, just to keep TypeScript happy :) +// this.catalog = { +// version: 1, +// streamingFormat: 1, +// streamingFormatVersion: "0.2", +// supportsDeltaUpdates: false, +// commonTrackFields: { +// packaging: "loc", +// renderGroup: 1, +// }, +// tracks: [{ +// name: "tester", +// namespace: "tester", +// selectionParams: {} +// }], +// } +// this.stream = null +// this.subscriber = null - this.#running = this.#run() - } +// this.#running = this.#run() +// } - static async init(config: BroadcasterConfig): Promise { - const client = new Client({ url: config.url, fingerprint: config.fingerprint, role: "publisher" }) - const connection = await client.connect(); +// static async init(config: BroadcasterConfig): Promise { +// const client = new Client({ url: config.url, fingerprint: config.fingerprint, role: "publisher" }) +// const connection = await client.connect(); - return new Broadcast({ connection, namespace: config.namespace }) - } +// return new Broadcast({ connection, namespace: config.namespace }) +// } - async #run() { - try { - await this.connection.announce(this.namespace) - this.subscribed = true - } catch (error) { +// async #run() { +// try { +// await this.connection.announce(this.namespace) +// this.subscribed = true +// } catch (error) { - this.subscribed = false - } +// this.subscribed = false +// } - for (; ;) { - const subscriber = await this.connection.subscribed() +// for (; ;) { +// const subscriber = await this.connection.subscribed() - if (!subscriber) { - this.subscribed = false +// if (!subscriber) { +// this.subscribed = false - break - } +// break +// } - await subscriber.ack() +// await subscriber.ack() - this.subscriber = subscriber +// this.subscriber = subscriber - this.subscribed = true +// this.subscribed = true - const bytes = Catalog.encode(this.catalog); +// const bytes = Catalog.encode(this.catalog); - const stream = await subscriber.group({ group: 0 }); +// const stream = await subscriber.group({ group: 0 }); - await stream.write({ object: 0, payload: bytes }) +// await stream.write({ object: 0, payload: bytes }) - this.stream = stream - } - } +// this.stream = stream +// } +// } - isSubscribed(): boolean { - return this.subscribed; - } +// isSubscribed(): boolean { +// return this.subscribed; +// } - // async #serveSubscribe(subscriber: SubscribeRecv) { - // try { +// // async #serveSubscribe(subscriber: SubscribeRecv) { +// // try { - // // Send a SUBSCRIBE_OK - // await subscriber.ack() +// // // Send a SUBSCRIBE_OK +// // await subscriber.ack() - // console.log("catalog track name:", subscriber.track) +// // console.log("catalog track name:", subscriber.track) - // const stream = await subscriber.group({ group: 0 }); +// // const stream = await subscriber.group({ group: 0 }); - // // const bytes = this.catalog.encode("Hello World") +// // // const bytes = this.catalog.encode("Hello World") - // await stream.write({ object: 0, payload: bytes }) +// // await stream.write({ object: 0, payload: bytes }) - // } catch (e) { - // const err = asError(e) - // await subscriber.close(1n, `failed to process publish: ${err.message}`) - // } finally { - // // TODO we can't close subscribers because there's no support for clean termination - // // await subscriber.close() - // } - // } +// // } catch (e) { +// // const err = asError(e) +// // await subscriber.close(1n, `failed to process publish: ${err.message}`) +// // } finally { +// // // TODO we can't close subscribers because there's no support for clean termination +// // // await subscriber.close() +// // } +// // } - // async mouseUpdatePosition({ x, y }: { x: number, y: number }, stream: GroupWriter) { +// // async mouseUpdatePosition({ x, y }: { x: number, y: number }, stream: GroupWriter) { - // const mouse_move = { - // input_type: "mouse_move", - // delta_y: y, - // delta_x: x, - // } +// // const mouse_move = { +// // input_type: "mouse_move", +// // delta_y: y, +// // delta_x: x, +// // } - // const bytes = Catalog.encode(this.catalog) +// // const bytes = Catalog.encode(this.catalog) - // await stream.write({ object: 0, payload: bytes }); - // } +// // await stream.write({ object: 0, payload: bytes }); +// // } - // async mouseUpdateButtons(e: MouseEvent, stream: GroupWriter) { - // const data: { input_type?: "mouse_key_down" | "mouse_key_up"; button: number; } = { button: e.button }; +// // async mouseUpdateButtons(e: MouseEvent, stream: GroupWriter) { +// // const data: { input_type?: "mouse_key_down" | "mouse_key_up"; button: number; } = { button: e.button }; - // if (e.type === "mousedown") { - // data["input_type"] = "mouse_key_down" - // } else if (e.type === "mouseup") { - // data["input_type"] = "mouse_key_up" - // } +// // if (e.type === "mousedown") { +// // data["input_type"] = "mouse_key_down" +// // } else if (e.type === "mouseup") { +// // data["input_type"] = "mouse_key_up" +// // } - // const bytes = Catalog.encode(this.catalog) +// // const bytes = Catalog.encode(this.catalog) - // await stream.write({ object: 0, payload: bytes }); - // } +// // await stream.write({ object: 0, payload: bytes }); +// // } - // async mouseUpdateWheel(e: WheelEvent, stream: GroupWriter) { - // const data: { input_type?: "mouse_wheel_up" | "mouse_wheel_down" } = {} +// // async mouseUpdateWheel(e: WheelEvent, stream: GroupWriter) { +// // const data: { input_type?: "mouse_wheel_up" | "mouse_wheel_down" } = {} - // if (e.deltaY < 0.0) { - // data["input_type"] = "mouse_wheel_up" - // } else { - // data["input_type"] = "mouse_wheel_down" - // } +// // if (e.deltaY < 0.0) { +// // data["input_type"] = "mouse_wheel_up" +// // } else { +// // data["input_type"] = "mouse_wheel_down" +// // } - // const bytes = Catalog.encode(this.catalog) +// // const bytes = Catalog.encode(this.catalog) - // await stream.write({ object: 0, payload: bytes }); - // } +// // await stream.write({ object: 0, payload: bytes }); +// // } - // async updateKeyUp(e: KeyboardEvent, stream: GroupWriter) { - // const data = { - // input_type: "key_up", - // key_code: e.keyCode - // } +// // async updateKeyUp(e: KeyboardEvent, stream: GroupWriter) { +// // const data = { +// // input_type: "key_up", +// // key_code: e.keyCode +// // } - // const bytes = Catalog.encode(this.catalog) +// // const bytes = Catalog.encode(this.catalog) - // await stream.write({ object: 0, payload: bytes }); - // } +// // await stream.write({ object: 0, payload: bytes }); +// // } - // async updateKeyDown(e: KeyboardEvent, stream: GroupWriter) { - // const data = { - // input_type: "key_down", - // key_code: e.keyCode - // } +// // async updateKeyDown(e: KeyboardEvent, stream: GroupWriter) { +// // const data = { +// // input_type: "key_down", +// // key_code: e.keyCode +// // } - // const bytes = Catalog.encode(this.catalog) +// // const bytes = Catalog.encode(this.catalog) - // await stream.write({ object: 0, payload: bytes }); - // } +// // await stream.write({ object: 0, payload: bytes }); +// // } - close() { - // TODO implement publish close - } +// close() { +// // TODO implement publish close +// } - // Returns the error message when the connection is closed - async closed(): Promise { - try { - await this.#running - return new Error("closed") // clean termination - } catch (e) { - return asError(e) - } - } -} \ No newline at end of file +// // Returns the error message when the connection is closed +// async closed(): Promise { +// try { +// await this.#running +// return new Error("closed") // clean termination +// } catch (e) { +// return asError(e) +// } +// } +// } \ No newline at end of file diff --git a/apps/www/src/routes/home/index.tsx b/apps/www/src/routes/home/index.tsx index 4e772a01..b1e2ba9e 100644 --- a/apps/www/src/routes/home/index.tsx +++ b/apps/www/src/routes/home/index.tsx @@ -12,13 +12,39 @@ export default component$(() => { return ( <> -
+ {/*
*/} + {/*
+ +
*/} + +
+
+ + +
+
+
+ {/* */} +

Red Dead Redemption 2

+
+
+
+ {/*
+ +
+ +
+ +
+ +
*/} + {/*

{getGreeting()}, Wanjohi

What will you play today?

-
-
+
*/} + {/*
  • { />
-
- +
*/} ) }) \ No newline at end of file diff --git a/apps/www/src/routes/play/[id]/index.tsx b/apps/www/src/routes/play/[id]/index.tsx new file mode 100644 index 00000000..03d71add --- /dev/null +++ b/apps/www/src/routes/play/[id]/index.tsx @@ -0,0 +1,287 @@ +import {useLocation} from "@builder.io/qwik-city"; +import {Keyboard, Mouse, WebRTCStream} from "@nestri/input" +import {component$, useSignal, useVisibleTask$} from "@builder.io/qwik"; + +export default component$(() => { + const id = useLocation().params.id; + const canvas = useSignal(); + + useVisibleTask$(({track}) => { + track(() => canvas.value); + + if (!canvas.value) return; // Ensure canvas is available + + // Create video element and make it output to canvas (TODO: improve this) + let video = document.getElementById("webrtc-video-player"); + if (!video) { + video = document.createElement("video"); + video.id = "stream-video-player"; + video.style.visibility = "hidden"; + const webrtc = new WebRTCStream("https://relay.dathorse.com", id, (mediaStream) => { + if (video && mediaStream && (video as HTMLVideoElement).srcObject === null) { + console.log("Setting mediastream"); + (video as HTMLVideoElement).srcObject = mediaStream; + + // @ts-ignore + window.hasstream = true; + // @ts-ignore + window.roomOfflineElement?.remove(); + + const playbtn = document.createElement("button"); + playbtn.style.position = "absolute"; + playbtn.style.left = "50%"; + playbtn.style.top = "50%"; + playbtn.style.transform = "translateX(-50%) translateY(-50%)"; + playbtn.style.width = "12rem"; + playbtn.style.height = "6rem"; + playbtn.style.borderRadius = "1rem"; + playbtn.style.backgroundColor = "rgb(175, 50, 50)"; + playbtn.style.color = "black"; + playbtn.style.fontSize = "1.5em"; + playbtn.textContent = "< Start >"; + + playbtn.onclick = () => { + playbtn.remove(); + (video as HTMLVideoElement).play().then(() => { + if (canvas.value) { + canvas.value.width = (video as HTMLVideoElement).videoWidth; + canvas.value.height = (video as HTMLVideoElement).videoHeight; + + const ctx = canvas.value.getContext("2d"); + const renderer = () => { + // @ts-ignore + if (ctx && window.hasstream) { + ctx.drawImage((video as HTMLVideoElement), 0, 0); + (video as HTMLVideoElement).requestVideoFrameCallback(renderer); + } + } + (video as HTMLVideoElement).requestVideoFrameCallback(renderer); + } + }); + + document.addEventListener("pointerlockchange", () => { + if (!canvas.value) return; // Ensure canvas is available + // @ts-ignore + if (document.pointerLockElement && !window.nestrimouse && !window.nestrikeyboard) { + // @ts-ignore + window.nestrimouse = new Mouse({canvas: canvas.value, webrtc}); + // @ts-ignore + window.nestrikeyboard = new Keyboard({canvas: canvas.value, webrtc}); + // @ts-ignore + } else if (!document.pointerLockElement && window.nestrimouse && window.nestrikeyboard) { + // @ts-ignore + window.nestrimouse.dispose(); + // @ts-ignore + window.nestrimouse = undefined; + // @ts-ignore + window.nestrikeyboard.dispose(); + // @ts-ignore + window.nestrikeyboard = undefined; + } + }); + }; + document.body.append(playbtn); + } else if (mediaStream === null) { + console.log("MediaStream is null, Room is offline"); + // Add a message to the screen + const offline = document.createElement("div"); + offline.style.position = "absolute"; + offline.style.left = "50%"; + offline.style.top = "50%"; + offline.style.transform = "translateX(-50%) translateY(-50%)"; + offline.style.width = "auto"; + offline.style.height = "auto"; + offline.style.color = "lightgray"; + offline.style.fontSize = "2em"; + offline.textContent = "Offline"; + document.body.append(offline); + // @ts-ignore + window.roomOfflineElement = offline; + // @ts-ignore + window.hasstream = false; + // Clear canvas if it has been set + if (canvas.value) { + const ctx = canvas.value.getContext("2d"); + if (ctx) ctx.clearRect(0, 0, canvas.value.width, canvas.value.height); + } + } + }); + } + }) + + return ( + { + // @ts-ignore + if (canvas.value && window.hasstream) { + // Do not use - unadjustedMovement: true - breaks input on linux + await canvas.value.requestPointerLock(); + await canvas.value.requestFullscreen() + if (document.fullscreenElement !== null) { + // @ts-ignore + if ('keyboard' in window.navigator && 'lock' in window.navigator.keyboard) { + const keys = [ + "AltLeft", + "AltRight", + "Tab", + "Escape", + "ContextMenu", + "MetaLeft", + "MetaRight" + ]; + console.log("requesting keyboard lock"); + // @ts-ignore + window.navigator.keyboard.lock(keys).then( + () => { + console.log("keyboard lock success"); + } + ).catch( + (e: any) => { + console.log("keyboard lock failed: ", e); + } + ) + } else { + console.log("keyboard lock not supported, navigator is: ", window.navigator, navigator); + } + } + } + }} + //TODO: go full screen, then lock on "landscape" screen-orientation on mobile + class="aspect-video h-full w-full object-contain max-h-screen"/> + ) +}) + +{/** + .spinningCircleInner_b6db20 { + transform: rotate(280deg); + } + .inner_b6db20 { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + contain: paint; + } */ +} + +{/* */ +} +// .loadingPopout_a8c724 { +// background-color: var(--background-secondary); +// display: flex; +// justify-content: center; +// padding: 8px; +// } + +// .circular_b6db20 { +// animation: spinner-spinning-circle-rotate_b6db20 2s linear infinite; +// height: 100%; +// width: 100%; +// } + +// 100% { +// transform: rotate(360deg); +// } + + +{/* .path3_b6db20 { + animation-delay: .23s; + stroke: var(--text-brand); +} +.path_b6db20 { + animation: spinner-spinning-circle-dash_b6db20 2s ease-in-out infinite; + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + fill: none; + stroke-width: 6; + stroke-miterlimit: 10; + stroke-linecap: round; + stroke: var(--brand-500); +} +circle[Attributes Style] { + cx: 50; + cy: 50; + r: 20; +} +user agent stylesheet +:not(svg) { + transform-origin: 0px 0px; +} */ +} + + +// .path2_b6db20 { +// animation-delay: .15s; +// stroke: var(--text-brand); +// opacity: .6; +// } +// .path_b6db20 { +// animation: spinner-spinning-circle-dash_b6db20 2s ease-in-out infinite; +// stroke-dasharray: 1, 200; +// stroke-dashoffset: 0; +// fill: none; +// stroke-width: 6; +// stroke-miterlimit: 10; +// stroke-linecap: round; +// stroke: var(--brand-500); +// } +// circle[Attributes Style] { +// cx: 50; +// cy: 50; +// r: 20; + + +// function throttle(func, limit) { +// let inThrottle; +// return function(...args) { +// if (!inThrottle) { +// func.apply(this, args); +// inThrottle = true; +// setTimeout(() => inThrottle = false, limit); +// } +// } +// } + +// // Use it like this: +// const throttledMouseMove = throttle((x, y) => { +// websocket.send(JSON.stringify({ +// type: 'mousemove', +// x: x, +// y: y +// })); +// }, 16); // ~60fps + +// use std::time::Instant; + +// // Add these to your AppState +// struct AppState { +// pipeline: Arc>, +// last_mouse_move: Arc>, // Add this +// } + +// // Then in your MouseMove handler: +// InputMessage::MouseMove { x, y } => { +// let mut last_move = state.last_mouse_move.lock().unwrap(); +// let now = Instant::now(); + +// // Only process if coordinates are different or enough time has passed +// if (last_move.0 != x || last_move.1 != y) && +// (now.duration_since(last_move.2).as_millis() > 16) { // ~60fps + +// println!("Mouse moved to x: {}, y: {}", x, y); + +// let structure = gst::Structure::builder("MouseMoveRelative") +// .field("pointer_x", x as f64) +// .field("pointer_y", y as f64) +// .build(); + +// let event = gst::event::CustomUpstream::new(structure); +// pipeline.send_event(event); + +// // Update last position and time +// *last_move = (x, y, now); +// } +// } diff --git a/apps/www/tsconfig.json b/apps/www/tsconfig.json index ab18d603..6a9efe3a 100644 --- a/apps/www/tsconfig.json +++ b/apps/www/tsconfig.json @@ -41,5 +41,5 @@ "./*.config.ts", "./*.config.js", "content-collections.ts" - ] +, "../../packages/input/src/webrtc-stream.ts" ] } \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 0a3752ae00285550372631f67a4c5a72e0e390e8..58c988d8f44b70ed89fc3a63b83f7bea24cedabd 100755 GIT binary patch delta 162587 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,Heq;=HYkLTp;7W-4z^obqN#PO^E; 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 byQXzZZPhaJgo-d52=CRj^(DJ)riK3x7cJqd delta 143188 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

70;?^~$=0K*>S2pZm%8a=ZAivi}G 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

yLLYy~}`wnP=J`~Q0#7FaXV7$wCT2^~ZK;1NY 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 void; + private keyupListener: (e: KeyboardEvent) => void; + + constructor({webrtc, canvas}: Props) { + this.wrtc = webrtc; + this.canvas = canvas; + this.keydownListener = this.createKeyboardListener("keydown", (e: any) => ({ + type: "KeyDown", + key: this.keyToVirtualKeyCode(e.code) + })); + this.keyupListener = this.createKeyboardListener("keyup", (e: any) => ({ + type: "KeyUp", + key: this.keyToVirtualKeyCode(e.code) + })); + this.run() + } + + private run() { + //calls all the other functions + if (!document.pointerLockElement) { + if (this.connected) { + this.stop() + } + return; + } + + if (document.pointerLockElement == this.canvas) { + this.connected = true + document.addEventListener("keydown", this.keydownListener, {passive: false}); + document.addEventListener("keyup", this.keyupListener, {passive: false}); + } else { + if (this.connected) { + this.stop() + } + } + } + + private stop() { + document.removeEventListener("keydown", this.keydownListener); + document.removeEventListener("keyup", this.keyupListener); + this.connected = false; + } + + // Helper function to create and return mouse listeners + private createKeyboardListener(type: string, dataCreator: (e: Event) => Partial): (e: Event) => void { + return (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + // Prevent repeated key events from being sent (important for games) + if ((e as any).repeat) + return; + + const data = dataCreator(e as any); // type assertion because of the way dataCreator is used + const dataString = JSON.stringify({...data, type} as Input); + + // Latency tracking + const tracker = new LatencyTracker("input-keyboard"); + tracker.addTimestamp("client_send"); + const message: MessageInput = { + payload_type: "input", + data: dataString, + latency: tracker, + }; + this.wrtc.sendBinary(encodeMessage(message)); + }; + } + + public dispose() { + document.exitPointerLock(); + this.stop(); + this.connected = false; + } + + private keyToVirtualKeyCode(code: string) { + // Treat Home key as Escape - TODO: Make user-configurable + if (code === "Home") return 1; + return keyCodeToLinuxEventCode[code] || undefined; + } +} \ No newline at end of file diff --git a/packages/input/src/latency.ts b/packages/input/src/latency.ts new file mode 100644 index 00000000..3a556896 --- /dev/null +++ b/packages/input/src/latency.ts @@ -0,0 +1,54 @@ +type TimestampEntry = { + stage: string; + time: Date; +}; + +export class LatencyTracker { + sequence_id: string; + timestamps: TimestampEntry[]; + metadata?: Record; + + constructor(sequence_id: string, timestamps: TimestampEntry[] = [], metadata: Record = {}) { + this.sequence_id = sequence_id; + this.timestamps = timestamps; + this.metadata = metadata; + } + + addTimestamp(stage: string): void { + const timestamp: TimestampEntry = { + stage, + time: new Date(), + }; + this.timestamps.push(timestamp); + } + + // Calculates the total time between the first and last recorded timestamps. + getTotalLatency(): number { + if (this.timestamps.length < 2) return 0; + + const times = this.timestamps.map((entry) => entry.time.getTime()); + const minTime = Math.min(...times); + const maxTime = Math.max(...times); + return maxTime - minTime; + } + + toJSON(): Record { + return { + sequence_id: this.sequence_id, + timestamps: this.timestamps.map((entry) => ({ + stage: entry.stage, + // Fill nanoseconds with zeros to match the expected format + time: entry.time.toISOString().replace(/\.(\d+)Z$/, ".$1000000Z"), + })), + metadata: this.metadata, + }; + } + + static fromJSON(json: any): LatencyTracker { + const timestamps: TimestampEntry[] = json.timestamps.map((ts: any) => ({ + stage: ts.stage, + time: new Date(ts.time), + })); + return new LatencyTracker(json.sequence_id, timestamps, json.metadata); + } +} diff --git a/packages/input/src/messages.ts b/packages/input/src/messages.ts new file mode 100644 index 00000000..5d60bf07 --- /dev/null +++ b/packages/input/src/messages.ts @@ -0,0 +1,73 @@ +import {gzip, ungzip} from "pako"; +import {LatencyTracker} from "./latency"; + +export interface MessageBase { + payload_type: string; +} + +export interface MessageInput extends MessageBase { + payload_type: "input"; + data: string; + latency?: LatencyTracker; +} + +export interface MessageICE extends MessageBase { + payload_type: "ice"; + candidate: RTCIceCandidateInit; +} + +export interface MessageSDP extends MessageBase { + payload_type: "sdp"; + sdp: RTCSessionDescriptionInit; +} + +export enum JoinerType { + JoinerNode = 0, + JoinerClient = 1, +} + +export interface MessageJoin extends MessageBase { + payload_type: "join"; + joiner_type: JoinerType; +} + +export enum AnswerType { + AnswerOffline = 0, + AnswerInUse, + AnswerOK +} + +export interface MessageAnswer extends MessageBase { + payload_type: "answer"; + answer_type: AnswerType; +} + +function blobToUint8Array(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + const arrayBuffer = reader.result as ArrayBuffer; + resolve(new Uint8Array(arrayBuffer)); + }; + reader.onerror = reject; + reader.readAsArrayBuffer(blob); + }); +} + +export function encodeMessage(message: T): Uint8Array { + // Convert the message to JSON string + const json = JSON.stringify(message); + // Compress the JSON string using gzip + return gzip(json); +} + +export async function decodeMessage(data: Blob): Promise { + // Convert the Blob to Uint8Array + const array = await blobToUint8Array(data); + // Decompress the gzip data + const decompressed = ungzip(array); + // Convert the Uint8Array to JSON string + const json = new TextDecoder().decode(decompressed); + // Parse the JSON string + return JSON.parse(json); +} diff --git a/packages/input/src/mouse.ts b/packages/input/src/mouse.ts new file mode 100644 index 00000000..0345a522 --- /dev/null +++ b/packages/input/src/mouse.ts @@ -0,0 +1,112 @@ +import {type Input} from "./types" +import {mouseButtonToLinuxEventCode} from "./codes" +import {MessageInput, encodeMessage} from "./messages"; +import {WebRTCStream} from "./webrtc-stream"; +import {LatencyTracker} from "./latency"; + +interface Props { + webrtc: WebRTCStream; + canvas: HTMLCanvasElement; +} + +export class Mouse { + protected wrtc: WebRTCStream; + protected canvas: HTMLCanvasElement; + protected connected!: boolean; + + // Store references to event listeners + private mousemoveListener: (e: MouseEvent) => void; + private mousedownListener: (e: MouseEvent) => void; + private mouseupListener: (e: MouseEvent) => void; + private mousewheelListener: (e: WheelEvent) => void; + + constructor({webrtc, canvas}: Props) { + this.wrtc = webrtc; + this.canvas = canvas; + + this.mousemoveListener = this.createMouseListener("mousemove", (e: any) => ({ + type: "MouseMove", + x: e.movementX, + y: e.movementY + })); + this.mousedownListener = this.createMouseListener("mousedown", (e: any) => ({ + type: "MouseKeyDown", + key: this.keyToVirtualKeyCode(e.button) + })); + + this.mouseupListener = this.createMouseListener("mouseup", (e: any) => ({ + type: "MouseKeyUp", + key: this.keyToVirtualKeyCode(e.button) + })); + this.mousewheelListener = this.createMouseListener("wheel", (e: any) => ({ + type: "MouseWheel", + x: e.deltaX, + y: e.deltaY + })); + + this.run() + } + + private run() { + //calls all the other functions + if (!document.pointerLockElement) { + console.log("no pointerlock") + if (this.connected) { + this.stop() + } + return; + } + + if (document.pointerLockElement == this.canvas) { + this.connected = true + this.canvas.addEventListener("mousemove", this.mousemoveListener, { passive: false }); + this.canvas.addEventListener("mousedown", this.mousedownListener, { passive: false }); + this.canvas.addEventListener("mouseup", this.mouseupListener, { passive: false }); + this.canvas.addEventListener("wheel", this.mousewheelListener, { passive: false }); + + } else { + if (this.connected) { + this.stop() + } + } + + } + + private stop() { + this.canvas.removeEventListener("mousemove", this.mousemoveListener); + this.canvas.removeEventListener("mousedown", this.mousedownListener); + this.canvas.removeEventListener("mouseup", this.mouseupListener); + this.canvas.removeEventListener("wheel", this.mousewheelListener); + this.connected = false; + } + + // Helper function to create and return mouse listeners + private createMouseListener(type: string, dataCreator: (e: Event) => Partial): (e: Event) => void { + return (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + const data = dataCreator(e as any); // type assertion because of the way dataCreator is used + const dataString = JSON.stringify({...data, type} as Input); + + // Latency tracking + const tracker = new LatencyTracker("input-mouse"); + tracker.addTimestamp("client_send"); + const message: MessageInput = { + payload_type: "input", + data: dataString, + latency: tracker, + }; + this.wrtc.sendBinary(encodeMessage(message)); + }; + } + + public dispose() { + document.exitPointerLock(); + this.stop(); + this.connected = false; + } + + private keyToVirtualKeyCode(code: number) { + return mouseButtonToLinuxEventCode[code] || undefined; + } +} \ No newline at end of file diff --git a/packages/input/src/types.ts b/packages/input/src/types.ts new file mode 100644 index 00000000..b733b499 --- /dev/null +++ b/packages/input/src/types.ts @@ -0,0 +1,52 @@ +interface BaseInput { + timestamp?: number; // Add a timestamp for better context (optional) +} + +interface MouseMove extends BaseInput { + type: "MouseMove"; + x: number; + y: number; +} + +interface MouseMoveAbs extends BaseInput { + type: "MouseMoveAbs"; + x: number; + y: number; +} + +interface MouseWheel extends BaseInput { + type: "MouseWheel"; + x: number; + y: number; +} + +interface MouseKeyDown extends BaseInput { + type: "MouseKeyDown"; + key: number; +} + +interface MouseKeyUp extends BaseInput { + type: "MouseKeyUp"; + key: number; +} + +interface KeyDown extends BaseInput { + type: "KeyDown"; + key: number; +} + +interface KeyUp extends BaseInput { + type: "KeyUp"; + key: number; +} + + +export type Input = + | MouseMove + | MouseMoveAbs + | MouseWheel + | MouseKeyDown + | MouseKeyUp + | KeyDown + | KeyUp; + diff --git a/packages/input/src/webrtc-stream.ts b/packages/input/src/webrtc-stream.ts new file mode 100644 index 00000000..09a404c0 --- /dev/null +++ b/packages/input/src/webrtc-stream.ts @@ -0,0 +1,166 @@ +import { + MessageBase, + MessageICE, + MessageJoin, + MessageSDP, + MessageAnswer, + JoinerType, + AnswerType, + decodeMessage, + encodeMessage +} from "./messages"; + +export class WebRTCStream { + private _ws: WebSocket | undefined = undefined; + private _pc: RTCPeerConnection | undefined = undefined; + private _mediaStream: MediaStream | undefined = undefined; + private _dataChannel: RTCDataChannel | undefined = undefined; + private _onConnected: ((stream: MediaStream | null) => void) | undefined = undefined; + + constructor(serverURL: string, roomName: string, connectedCallback: (stream: MediaStream | null) => void) { + // If roomName is not provided, return + if (roomName.length <= 0) { + console.error("Room name not provided"); + return; + } + + this._onConnected = connectedCallback; + + console.log("Setting up WebSocket"); + // Replace http/https with ws/wss + const wsURL = serverURL.replace(/^http/, "ws"); + this._ws = new WebSocket(`${wsURL}/api/ws/${roomName}`); + this._ws.onopen = async () => { + console.log("WebSocket opened"); + + console.log("Setting up PeerConnection"); + this._pc = new RTCPeerConnection({ + iceServers: [ + { + urls: "stun:stun.l.google.com:19302" + } + ], + }); + + this._pc.ontrack = (e) => { + console.log("Track received: ", e.track); + this._mediaStream = e.streams[e.streams.length - 1]; + }; + + this._pc.onconnectionstatechange = () => { + console.log("Connection state: ", this._pc!.connectionState); + if (this._pc!.connectionState === "connected") { + if (this._onConnected && this._mediaStream) + this._onConnected(this._mediaStream); + } + }; + + this._pc.onicecandidate = (e) => { + if (e.candidate) { + const message: MessageICE = { + payload_type: "ice", + candidate: e.candidate + }; + this._ws!.send(encodeMessage(message)); + } + } + + this._pc.ondatachannel = (e) => { + this._dataChannel = e.channel; + this._setupDataChannelEvents(); + } + + // Send join message + const joinMessage: MessageJoin = { + payload_type: "join", + joiner_type: JoinerType.JoinerClient + }; + this._ws!.send(encodeMessage(joinMessage)); + } + + let iceHolder: RTCIceCandidateInit[] = []; + + this._ws.onmessage = async (e) => { + // allow only binary + if (typeof e.data !== "object") return; + if (!e.data) return; + const message = await decodeMessage(e.data); + switch (message.payload_type) { + case "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); + this._ws!.send(encodeMessage({ + payload_type: "sdp", + sdp: answer + })); + break; + case "ice": + // If remote description is not set yet, hold the ICE candidates + if (this._pc!.remoteDescription) { + await this._pc!.addIceCandidate((message as MessageICE).candidate); + // Add held ICE candidates + for (const ice of iceHolder) { + await this._pc!.addIceCandidate(ice); + } + iceHolder = []; + } 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"); + } + + 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;"); + } + + 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}'`) + } + + // 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."); + } +} diff --git a/packages/master/go.mod b/packages/master/go.mod new file mode 100644 index 00000000..7565b333 --- /dev/null +++ b/packages/master/go.mod @@ -0,0 +1,32 @@ +module master + +go 1.23.3 + +require github.com/docker/docker v27.3.1+incompatible + +require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/time v0.8.0 // indirect + gotest.tools/v3 v3.5.1 // indirect +) diff --git a/packages/master/go.sum b/packages/master/go.sum new file mode 100644 index 00000000..4acbdbf3 --- /dev/null +++ b/packages/master/go.sum @@ -0,0 +1,123 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +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/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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/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= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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/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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/packages/master/main.go b/packages/master/main.go new file mode 100644 index 00000000..13164f8f --- /dev/null +++ b/packages/master/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "io" + "os" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" +) + +func main() { + ctx := context.Background() + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer cli.Close() + + // Try to get the Docker version + _, err = cli.ServerVersion(ctx) + if err != nil { + // If an error occurs (e.g., Docker is not running), return false + panic(err) + } + + // Download the image + containerName := "hello-world" + + reader, err := cli.ImagePull(ctx, containerName, image.PullOptions{}) + if err != nil { + panic(err) + } + + defer reader.Close() + + // cli.ImagePull is asynchronous. + // The reader needs to be read completely for the pull operation to complete. + // If stdout is not required, consider using io.Discard instead of os.Stdout. + io.Copy(os.Stdout, reader) + + resp, err := cli.ContainerCreate(ctx, &container.Config{ + Image: "hello-world", + }, + nil, nil, nil, containerName) + if err != nil { + panic(err) + } + + // Start the container + if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + panic(err) + } + + // Wait for the container to finish and get its logs + statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) + select { + case err := <-errCh: + if err != nil { + panic(err) + } + case <-statusCh: + } + + out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) + if err != nil { + panic(err) + } + + stdcopy.StdCopy(os.Stdout, os.Stderr, out) + + // Remove the container + if err := cli.ContainerRemove(ctx, resp.ID, container.RemoveOptions{}); err != nil { + panic(err) + } + +} diff --git a/packages/moq/.eslintrc.cjs b/packages/moq/.eslintrc.cjs index 89e59ace..49848456 100644 --- a/packages/moq/.eslintrc.cjs +++ b/packages/moq/.eslintrc.cjs @@ -8,7 +8,7 @@ module.exports = { "prettier", ], parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint", "prettier"], + plugins: ["@typescript-eslint", "prettier", "solid"], root: true, env: { browser: true, diff --git a/packages/moq/common/async.ts b/packages/moq/common/async.ts index a0fa0b4e..2f91bbbf 100644 --- a/packages/moq/common/async.ts +++ b/packages/moq/common/async.ts @@ -1,7 +1,7 @@ export class Deferred { promise: Promise resolve!: (value: T | PromiseLike) => void - reject!: (reason: any) => void + reject!: (reason: unknown) => void pending = true constructor() { @@ -35,16 +35,19 @@ export class Watch { update(v: T | ((v: T) => T)) { if (!this.#next.pending) { - throw new Error("already closed") + throw new Error("closed") } // If we're given a function, call it with the current value + let value: T if (v instanceof Function) { - v = v(this.#current[0]) + value = v(this.#current[0]) + } else { + value = v } const next = new Deferred>() - this.#current = [v, next.promise] + this.#current = [value, next.promise] this.#next.resolve(this.#current) this.#next = next } @@ -53,6 +56,10 @@ export class Watch { this.#current[1] = undefined this.#next.resolve(this.#current) } + + closed() { + return !this.#next.pending + } } // Wakes up a multiple consumers. @@ -88,6 +95,7 @@ export class Queue { } async push(v: T) { + if (this.#closed) throw new Error("closed") const w = this.#stream.writable.getWriter() await w.write(v) w.releaseLock() diff --git a/packages/moq/common/error.ts b/packages/moq/common/error.ts index c627a0b9..d4171799 100644 --- a/packages/moq/common/error.ts +++ b/packages/moq/common/error.ts @@ -1,14 +1,14 @@ // I hate javascript -export function asError(e: any): Error { +export function asError(e: unknown): Error { if (e instanceof Error) { return e - } else if (typeof e === "string") { - return new Error(e) - } else { - return new Error(String(e)) } + if (typeof e === "string") { + return new Error(e) + } + return new Error(String(e)) } -export function isError(e: any): e is Error { +export function isError(e: unknown): e is Error { return e instanceof Error } diff --git a/packages/moq/common/hex.ts b/packages/moq/common/hex.ts new file mode 100644 index 00000000..ca833dbc --- /dev/null +++ b/packages/moq/common/hex.ts @@ -0,0 +1,11 @@ +export function decode(str: string): Uint8Array { + const bytes = new Uint8Array(str.length / 2) + for (let i = 0; i < bytes.length; i += 1) { + bytes[i] = Number.parseInt(str.slice(2 * i, 2 * i + 2), 16) + } + return bytes +} + +export function encode(_bytes: Uint8Array): string { + throw "todo" +} diff --git a/packages/moq/common/ring.ts b/packages/moq/common/ring.ts index 97a73e65..8a805059 100644 --- a/packages/moq/common/ring.ts +++ b/packages/moq/common/ring.ts @@ -2,8 +2,8 @@ enum STATE { READ_POS = 0, // The current read position - WRITE_POS, // The current write position - LENGTH, // Clever way of saving the total number of enums values. + WRITE_POS = 1, // The current write position + LENGTH = 2, // Clever way of saving the total number of enums values. } interface FrameCopyToOptions { @@ -62,16 +62,12 @@ export class Ring { const readPos = Atomics.load(this.state, STATE.READ_POS) const writePos = Atomics.load(this.state, STATE.WRITE_POS) - const startPos = writePos - let endPos = writePos + frame.numberOfFrames + const available = this.capacity - (writePos - readPos) + if (available <= 0) return 0 - if (endPos > readPos + this.capacity) { - endPos = readPos + this.capacity - if (endPos <= startPos) { - // No space to write - return 0 - } - } + const toWrite = Math.min(frame.numberOfFrames, available) + const startPos = writePos + const endPos = writePos + toWrite const startIndex = startPos % this.capacity const endIndex = endPos % this.capacity @@ -114,7 +110,7 @@ export class Ring { Atomics.store(this.state, STATE.WRITE_POS, endPos) - return endPos - startPos + return toWrite } read(dst: Float32Array[]): number { diff --git a/packages/moq/contribute/audio.ts b/packages/moq/contribute/audio.ts index d42fe288..e170b59f 100644 --- a/packages/moq/contribute/audio.ts +++ b/packages/moq/contribute/audio.ts @@ -1,15 +1,67 @@ +import { Deferred } from "../common/async" +import type { Frame } from "../karp/frame" +import type { Group, Track } from "../transfork" +import { Closed } from "../transfork/error" + const SUPPORTED = [ // TODO support AAC // "mp4a" "Opus", ] +export class Packer { + #source: MediaStreamTrackProcessor + #encoder: Encoder + + #data: Track + #current?: Group + + constructor(track: MediaStreamAudioTrack, encoder: Encoder, data: Track) { + this.#source = new MediaStreamTrackProcessor({ track }) + this.#encoder = encoder + this.#data = data + } + + async run() { + const output = new WritableStream({ + write: (chunk) => this.#write(chunk), + close: () => this.#close(), + abort: (e) => this.#close(e), + }) + + return this.#source.readable.pipeThrough(this.#encoder.frames).pipeTo(output) + } + + #write(frame: Frame) { + // TODO use a fixed interval instead of keyframes (audio) + // TODO actually just align with video + if (!this.#current || frame.type === "key") { + if (this.#current) { + this.#current.close() + } + + this.#current = this.#data.appendGroup() + } + + this.#current.writeFrame(frame.data) + } + + #close(err?: unknown) { + const closed = Closed.from(err) + if (this.#current) { + this.#current.close(closed) + } + + this.#data.close(closed) + } +} + export class Encoder { #encoder!: AudioEncoder #encoderConfig: AudioEncoderConfig - #decoderConfig?: AudioDecoderConfig + #decoderConfig = new Deferred() - frames: TransformStream + frames: TransformStream constructor(config: AudioEncoderConfig) { this.#encoderConfig = config @@ -21,7 +73,7 @@ export class Encoder { }) } - #start(controller: TransformStreamDefaultController) { + #start(controller: TransformStreamDefaultController) { this.#encoder = new AudioEncoder({ output: (frame, metadata) => { this.#enqueue(controller, frame, metadata) @@ -40,17 +92,16 @@ export class Encoder { } #enqueue( - controller: TransformStreamDefaultController, + controller: TransformStreamDefaultController, frame: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata, ) { const config = metadata?.decoderConfig - if (config && !this.#decoderConfig) { + if (config && !this.#decoderConfig.pending) { const config = metadata.decoderConfig if (!config) throw new Error("missing decoder config") - controller.enqueue(config) - this.#decoderConfig = config + this.#decoderConfig.resolve(config) } controller.enqueue(frame) @@ -72,4 +123,8 @@ export class Encoder { get config() { return this.#encoderConfig } + + async decoderConfig(): Promise { + return await this.#decoderConfig.promise + } } diff --git a/packages/moq/contribute/broadcast.ts b/packages/moq/contribute/broadcast.ts index 2f8224fb..f0b61e46 100644 --- a/packages/moq/contribute/broadcast.ts +++ b/packages/moq/contribute/broadcast.ts @@ -1,15 +1,14 @@ -import { Connection, SubscribeRecv } from "../transport" -import { asError } from "../common/error" -import { Segment } from "./segment" -import { Track } from "./track" -import * as Catalog from "../media/catalog" +import * as Catalog from "../karp/catalog" +import * as Transfork from "../transfork" +import * as Audio from "./audio" +import * as Video from "./video" import { isAudioTrackSettings, isVideoTrackSettings } from "../common/settings" export interface BroadcastConfig { - namespace: string - connection: Connection + path: string[] media: MediaStream + id?: number audio?: AudioEncoderConfig video?: VideoEncoderConfig @@ -21,221 +20,89 @@ export interface BroadcastConfigTrack { } export class Broadcast { - #tracks = new Map() - - readonly config: BroadcastConfig - readonly catalog: Catalog.Root - readonly connection: Connection - readonly namespace: string - - #running: Promise + #config: BroadcastConfig + #path: string[] constructor(config: BroadcastConfig) { - this.connection = config.connection - this.config = config - this.namespace = config.namespace + const id = config.id || new Date().getTime() / 1000 - const tracks: Catalog.Track[] = [] + this.#config = config + this.#path = config.path.concat(id.toString()) + } - for (const media of this.config.media.getTracks()) { - const track = new Track(media, config) - this.#tracks.set(track.name, track) + async publish(connection: Transfork.Connection) { + const broadcast: Catalog.Broadcast = { + path: this.#config.path, + audio: [], + video: [], + } + for (const media of this.#config.media.getTracks()) { const settings = media.getSettings() + const info = { + name: media.id, // TODO way too verbose + priority: media.kind === "video" ? 1 : 2, + } + + const track = new Transfork.Track(this.#config.path.concat(info.name), info.priority) + if (isVideoTrackSettings(settings)) { - if (!config.video) { + if (!this.#config.video) { throw new Error("no video configuration provided") } - const video: Catalog.VideoTrack = { - namespace: this.namespace, - name: `${track.name}.m4s`, - initTrack: `${track.name}.mp4`, - selectionParams: { - mimeType: "video/mp4", - codec: config.video.codec, - width: settings.width, - height: settings.height, - framerate: settings.frameRate, - bitrate: config.video.bitrate, - }, + const encoder = new Video.Encoder(this.#config.video) + const packer = new Video.Packer(media as MediaStreamVideoTrack, encoder, track) + + // TODO handle error + packer.run().catch((err) => console.error("failed to run video packer: ", err)) + + const decoder = await encoder.decoderConfig() + const description = decoder.description ? new Uint8Array(decoder.description as ArrayBuffer) : undefined + + const video: Catalog.Video = { + track: info, + codec: decoder.codec, + description: description, + resolution: { width: settings.width, height: settings.height }, + frame_rate: settings.frameRate, + bitrate: this.#config.video.bitrate, } - tracks.push(video) + broadcast.video.push(video) } else if (isAudioTrackSettings(settings)) { - if (!config.audio) { + if (!this.#config.audio) { throw new Error("no audio configuration provided") } - const audio: Catalog.AudioTrack = { - namespace: this.namespace, - name: `${track.name}.m4s`, - initTrack: `${track.name}.mp4`, - selectionParams: { - mimeType: "audio/ogg", - codec: config.audio.codec, - samplerate: settings.sampleRate, - //sampleSize: settings.sampleSize, - channelConfig: `${settings.channelCount}`, - bitrate: config.audio.bitrate, - }, + const encoder = new Audio.Encoder(this.#config.audio) + const packer = new Audio.Packer(media as MediaStreamAudioTrack, encoder, track) + packer.run().catch((err) => console.error("failed to run audio packer: ", err)) // TODO handle error + + const decoder = await encoder.decoderConfig() + + const audio: Catalog.Audio = { + track: info, + codec: decoder.codec, + sample_rate: settings.sampleRate, + channel_count: settings.channelCount, + bitrate: this.#config.audio.bitrate, } - tracks.push(audio) + broadcast.audio.push(audio) } else { throw new Error(`unknown track type: ${media.kind}`) } + + connection.publish(track.reader()) } - this.catalog = { - version: 1, - streamingFormat: 1, - streamingFormatVersion: "0.2", - supportsDeltaUpdates: false, - commonTrackFields: { - packaging: "cmaf", - renderGroup: 1, - }, - tracks, - } + const track = new Transfork.Track(this.#config.path.concat("catalog.json"), 0) + track.appendGroup().writeFrames(Catalog.encode(broadcast)) - this.#running = this.#run() + connection.publish(track.reader()) } - async #run() { - await this.connection.announce(this.namespace) - - for (;;) { - const subscriber = await this.connection.subscribed() - if (!subscriber) break - - // Run an async task to serve each subscription. - this.#serveSubscribe(subscriber).catch((e) => { - const err = asError(e) - console.warn("failed to serve subscribe", err) - }) - } - } - - async #serveSubscribe(subscriber: SubscribeRecv) { - try { - const [base, ext] = splitExt(subscriber.track) - if (ext === "catalog") { - await this.#serveCatalog(subscriber, base) - } else if (ext === "mp4") { - await this.#serveInit(subscriber, base) - } else if (ext === "m4s") { - await this.#serveTrack(subscriber, base) - } else { - throw new Error(`unknown subscription: ${subscriber.track}`) - } - } catch (e) { - const err = asError(e) - await subscriber.close(1n, `failed to process subscribe: ${err.message}`) - } finally { - // TODO we can't close subscribers because there's no support for clean termination - // await subscriber.close() - } - } - - async #serveCatalog(subscriber: SubscribeRecv, name: string) { - // We only support ".catalog" - if (name !== "") throw new Error(`unknown catalog: ${name}`) - - const bytes = Catalog.encode(this.catalog) - - // Send a SUBSCRIBE_OK - await subscriber.ack() - - const stream = await subscriber.group({ group: 0 }) - await stream.write({ object: 0, payload: bytes }) - await stream.close() - } - - async #serveInit(subscriber: SubscribeRecv, name: string) { - const track = this.#tracks.get(name) - if (!track) throw new Error(`no track with name ${subscriber.track}`) - - // Send a SUBSCRIBE_OK - await subscriber.ack() - - const init = await track.init() - - const stream = await subscriber.group({ group: 0 }) - await stream.write({ object: 0, payload: init }) - await stream.close() - } - - async #serveTrack(subscriber: SubscribeRecv, name: string) { - const track = this.#tracks.get(name) - if (!track) throw new Error(`no track with name ${subscriber.track}`) - - // Send a SUBSCRIBE_OK - await subscriber.ack() - - const segments = track.segments().getReader() - - for (;;) { - const { value: segment, done } = await segments.read() - if (done) break - - // Serve the segment and log any errors that occur. - this.#serveSegment(subscriber, segment).catch((e) => { - const err = asError(e) - console.warn("failed to serve segment", err) - }) - } - } - - async #serveSegment(subscriber: SubscribeRecv, segment: Segment) { - // Create a new stream for each segment. - const stream = await subscriber.group({ - group: segment.id, - priority: 0, // TODO - }) - - let object = 0 - - // Pipe the segment to the stream. - const chunks = segment.chunks().getReader() - for (;;) { - const { value, done } = await chunks.read() - if (done) break - - await stream.write({ - object, - payload: value, - }) - - object += 1 - } - - await stream.close() - } - - // Attach the captured video stream to the given video element. - attach(video: HTMLVideoElement) { - video.srcObject = this.config.media - } - - close() { - // TODO implement publish close - } - - // Returns the error message when the connection is closed - async closed(): Promise { - try { - await this.#running - return new Error("closed") // clean termination - } catch (e) { - return asError(e) - } - } -} - -function splitExt(s: string): [string, string] { - const i = s.lastIndexOf(".") - if (i < 0) throw new Error(`no extension found`) - return [s.substring(0, i), s.substring(i + 1)] + close() {} } diff --git a/packages/moq/contribute/chunk.ts b/packages/moq/contribute/chunk.ts deleted file mode 100644 index 2fcfe334..00000000 --- a/packages/moq/contribute/chunk.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Extends EncodedVideoChunk, allowing a new "init" type -export interface Chunk { - type: "init" | "key" | "delta" - timestamp: number // microseconds - duration: number // microseconds - data: Uint8Array -} diff --git a/packages/moq/contribute/container.ts b/packages/moq/contribute/container.ts deleted file mode 100644 index ab1b4f1d..00000000 --- a/packages/moq/contribute/container.ts +++ /dev/null @@ -1,165 +0,0 @@ -import * as MP4 from "../media/mp4" -import { Chunk } from "./chunk" - -type DecoderConfig = AudioDecoderConfig | VideoDecoderConfig -type EncodedChunk = EncodedAudioChunk | EncodedVideoChunk - -export class Container { - #mp4: MP4.ISOFile - #frame?: EncodedAudioChunk | EncodedVideoChunk // 1 frame buffer - #track?: number - #segment = 0 - - encode: TransformStream - - constructor() { - this.#mp4 = new MP4.ISOFile() - this.#mp4.init() - - this.encode = new TransformStream({ - transform: (frame, controller) => { - if (isDecoderConfig(frame)) { - return this.#init(frame, controller) - } else { - return this.#enqueue(frame, controller) - } - }, - }) - } - - #init(frame: DecoderConfig, controller: TransformStreamDefaultController) { - if (this.#track) throw new Error("duplicate decoder config") - - let codec = frame.codec.substring(0, 4) - if (codec == "opus") { - codec = "Opus" - } - - const options: MP4.TrackOptions = { - type: codec, - timescale: 1_000_000, - } - - if (isVideoConfig(frame)) { - options.width = frame.codedWidth - options.height = frame.codedHeight - } else { - options.channel_count = frame.numberOfChannels - options.samplerate = frame.sampleRate - } - - if (!frame.description) throw new Error("missing frame description") - const desc = frame.description as ArrayBufferLike - - if (codec === "avc1") { - options.avcDecoderConfigRecord = desc - } else if (codec === "hev1") { - options.hevcDecoderConfigRecord = desc - } else if (codec === "Opus") { - // description is an identification header: https://datatracker.ietf.org/doc/html/rfc7845#section-5.1 - // The first 8 bytes are the magic string "OpusHead", followed by what we actually want. - const dops = new MP4.BoxParser.dOpsBox(undefined) - - // Annoyingly, the header is little endian while MP4 is big endian, so we have to parse. - const data = new MP4.Stream(desc, 8, MP4.Stream.LITTLE_ENDIAN) - dops.parse(data) - - dops.Version = 0 - options.description = dops - options.hdlr = "soun" - } else { - throw new Error(`unsupported codec: ${codec}`) - } - - this.#track = this.#mp4.addTrack(options) - if (!this.#track) throw new Error("failed to initialize MP4 track") - - const buffer = MP4.ISOFile.writeInitializationSegment(this.#mp4.ftyp!, this.#mp4.moov!, 0, 0) - const data = new Uint8Array(buffer) - - controller.enqueue({ - type: "init", - timestamp: 0, - duration: 0, - data, - }) - } - - #enqueue(frame: EncodedChunk, controller: TransformStreamDefaultController) { - // Check if we should create a new segment - if (frame.type == "key") { - this.#segment += 1 - } else if (this.#segment == 0) { - throw new Error("must start with keyframe") - } - - // We need a one frame buffer to compute the duration - if (!this.#frame) { - this.#frame = frame - return - } - - const duration = frame.timestamp - this.#frame.timestamp - - // TODO avoid this extra copy by writing to the mdat directly - // ...which means changing mp4box.js to take an offset instead of ArrayBuffer - const buffer = new Uint8Array(this.#frame.byteLength) - this.#frame.copyTo(buffer) - - if (!this.#track) throw new Error("missing decoder config") - - // Add the sample to the container - this.#mp4.addSample(this.#track, buffer, { - duration, - dts: this.#frame.timestamp, - cts: this.#frame.timestamp, - is_sync: this.#frame.type == "key", - }) - - const stream = new MP4.Stream(undefined, 0, MP4.Stream.BIG_ENDIAN) - - // Moof and mdat atoms are written in pairs. - // TODO remove the moof/mdat from the Box to reclaim memory once everything works - for (;;) { - const moof = this.#mp4.moofs.shift() - const mdat = this.#mp4.mdats.shift() - - if (!moof && !mdat) break - if (!moof) throw new Error("moof missing") - if (!mdat) throw new Error("mdat missing") - - moof.write(stream) - mdat.write(stream) - } - - // TODO avoid this extra copy by writing to the buffer provided in copyTo - const data = new Uint8Array(stream.buffer) - - controller.enqueue({ - type: this.#frame.type, - timestamp: this.#frame.timestamp, - duration: this.#frame.duration ?? 0, - data, - }) - - this.#frame = frame - } - - /* TODO flush the last frame - #flush(controller: TransformStreamDefaultController) { - if (this.#frame) { - // TODO guess the duration - this.#enqueue(this.#frame, 0, controller) - } - } - */ -} - -function isDecoderConfig(frame: DecoderConfig | EncodedChunk): frame is DecoderConfig { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return (frame as DecoderConfig).codec !== undefined -} - -function isVideoConfig(frame: DecoderConfig): frame is VideoDecoderConfig { - return (frame as VideoDecoderConfig).codedWidth !== undefined -} diff --git a/packages/moq/contribute/segment.ts b/packages/moq/contribute/segment.ts index f0aa8195..fa5a65e6 100644 --- a/packages/moq/contribute/segment.ts +++ b/packages/moq/contribute/segment.ts @@ -1,10 +1,10 @@ -import { Chunk } from "./chunk" +import type { Frame } from "../karp/frame" export class Segment { id: number - // Take in a stream of chunks - input: WritableStream + // Take in a stream of frames + input: WritableStream // Output a stream of bytes, which we fork for each new subscriber. #cache: ReadableStream @@ -16,16 +16,18 @@ export class Segment { // Set a max size for each segment, dropping the tail if it gets too long. // We tee the reader, so this limit applies to the FASTEST reader. - const backpressure = new ByteLengthQueuingStrategy({ highWaterMark: 8_000_000 }) + const backpressure = new ByteLengthQueuingStrategy({ + highWaterMark: 8_000_000, + }) - const transport = new TransformStream( + const transport = new TransformStream( { - transform: (chunk: Chunk, controller) => { + transform: (frame: Frame, controller) => { // Compute the max timestamp of the segment - this.timestamp = Math.max(chunk.timestamp + chunk.duration) + this.timestamp = Math.max(this.timestamp, frame.timestamp) // Push the chunk to any listeners. - controller.enqueue(chunk.data) + controller.enqueue(frame.data) }, }, undefined, diff --git a/packages/moq/contribute/track.ts b/packages/moq/contribute/track.ts index cec70d81..d2a050ee 100644 --- a/packages/moq/contribute/track.ts +++ b/packages/moq/contribute/track.ts @@ -1,9 +1,8 @@ -import { Segment } from "./segment" import { Notify } from "../common/async" -import { Chunk } from "./chunk" -import { Container } from "./container" -import { BroadcastConfig } from "./broadcast" +import type { BroadcastConfig } from "./broadcast" +import { Segment } from "./segment" +import type { Frame } from "../karp/frame" import * as Audio from "./audio" import * as Video from "./video" @@ -36,7 +35,6 @@ export class Track { async #runAudio(track: MediaStreamAudioTrack, config: AudioEncoderConfig) { const source = new MediaStreamTrackProcessor({ track }) const encoder = new Audio.Encoder(config) - const container = new Container() // Split the container at keyframe boundaries const segments = new WritableStream({ @@ -45,13 +43,12 @@ export class Track { abort: (e) => this.#close(e), }) - return source.readable.pipeThrough(encoder.frames).pipeThrough(container.encode).pipeTo(segments) + return source.readable.pipeThrough(encoder.frames).pipeTo(segments) } async #runVideo(track: MediaStreamVideoTrack, config: VideoEncoderConfig) { const source = new MediaStreamTrackProcessor({ track }) const encoder = new Video.Encoder(config) - const container = new Container() // Split the container at keyframe boundaries const segments = new WritableStream({ @@ -60,18 +57,12 @@ export class Track { abort: (e) => this.#close(e), }) - return source.readable.pipeThrough(encoder.frames).pipeThrough(container.encode).pipeTo(segments) + return source.readable.pipeThrough(encoder.frames).pipeTo(segments) } - async #write(chunk: Chunk) { - if (chunk.type === "init") { - this.#init = chunk.data - this.#notify.wake() - return - } - + async #write(frame: Frame) { let current = this.#segments.at(-1) - if (!current || chunk.type === "key") { + if (!current || frame.type === "key") { if (current) { await current.input.close() } @@ -88,7 +79,7 @@ export class Track { const first = this.#segments[0] // Expire after 10s - if (chunk.timestamp - first.timestamp < 10_000_000) break + if (frame.timestamp - first.timestamp < 10_000_000) break this.#segments.shift() this.#offset += 1 @@ -99,7 +90,7 @@ export class Track { const writer = current.input.getWriter() if ((writer.desiredSize || 0) > 0) { - await writer.write(chunk) + await writer.write(frame) } else { console.warn("dropping chunk", writer.desiredSize) } @@ -147,7 +138,8 @@ export class Track { if (this.#error) { controller.error(this.#error) return - } else if (this.#closed) { + } + if (this.#closed) { controller.close() return } diff --git a/packages/moq/contribute/tsconfig.json b/packages/moq/contribute/tsconfig.json index 70899a7f..693a7071 100644 --- a/packages/moq/contribute/tsconfig.json +++ b/packages/moq/contribute/tsconfig.json @@ -9,10 +9,10 @@ "path": "../common" }, { - "path": "../transport" + "path": "../transfork" }, { - "path": "../media" + "path": "../karp" } ] } diff --git a/packages/moq/contribute/video.ts b/packages/moq/contribute/video.ts index 747c7659..fa25876c 100644 --- a/packages/moq/contribute/video.ts +++ b/packages/moq/contribute/video.ts @@ -1,3 +1,8 @@ +import { Deferred } from "../common/async" +import type { Frame } from "../karp/frame" +import type { Group, Track } from "../transfork" +import { Closed } from "../transfork/error" + const SUPPORTED = [ "avc1", // H.264 "hev1", // HEVC (aka h.265) @@ -8,10 +13,55 @@ export interface EncoderSupported { codecs: string[] } +export class Packer { + #source: MediaStreamTrackProcessor + #encoder: Encoder + + #data: Track + #current?: Group + + constructor(track: MediaStreamVideoTrack, encoder: Encoder, data: Track) { + this.#source = new MediaStreamTrackProcessor({ track }) + this.#encoder = encoder + this.#data = data + } + + async run() { + const output = new WritableStream({ + write: (chunk) => this.#write(chunk), + close: () => this.#close(), + abort: (e) => this.#close(e), + }) + + return this.#source.readable.pipeThrough(this.#encoder.frames).pipeTo(output) + } + + #write(frame: Frame) { + if (!this.#current || frame.type === "key") { + if (this.#current) { + this.#current.close() + } + + this.#current = this.#data.appendGroup() + } + + frame.encode(this.#current) + } + + #close(err?: unknown) { + const closed = Closed.from(err) + if (this.#current) { + this.#current.close(closed) + } + + this.#data.close(closed) + } +} + export class Encoder { #encoder!: VideoEncoder #encoderConfig: VideoEncoderConfig - #decoderConfig?: VideoDecoderConfig + #decoderConfig = new Deferred() // true if we should insert a keyframe, undefined when the encoder should decide #keyframeNext: true | undefined = true @@ -20,7 +70,7 @@ export class Encoder { #keyframeCounter = 0 // Converts raw rames to encoded frames. - frames: TransformStream + frames: TransformStream constructor(config: VideoEncoderConfig) { config.bitrateMode ??= "constant" @@ -53,12 +103,17 @@ export class Encoder { return !!res.supported } + async decoderConfig(): Promise { + return await this.#decoderConfig.promise + } + #start(controller: TransformStreamDefaultController) { this.#encoder = new VideoEncoder({ output: (frame, metadata) => { this.#enqueue(controller, frame, metadata) }, error: (err) => { + this.#decoderConfig.reject(err) throw err }, }) @@ -77,23 +132,22 @@ export class Encoder { } #enqueue( - controller: TransformStreamDefaultController, + controller: TransformStreamDefaultController, frame: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata, ) { - if (!this.#decoderConfig) { + if (this.#decoderConfig.pending) { const config = metadata?.decoderConfig if (!config) throw new Error("missing decoder config") - - controller.enqueue(config) - this.#decoderConfig = config + this.#decoderConfig.resolve(config) } if (frame.type === "key") { this.#keyframeCounter = 0 } else { this.#keyframeCounter += 1 - if (this.#keyframeCounter + this.#encoder.encodeQueueSize >= 2 * this.#encoderConfig.framerate!) { + const framesPerGop = this.#encoderConfig.framerate ? 2 * this.#encoderConfig.framerate : 60 + if (this.#keyframeCounter + this.#encoder.encodeQueueSize >= framesPerGop) { this.#keyframeNext = true } } diff --git a/packages/moq/karp/catalog/audio.ts b/packages/moq/karp/catalog/audio.ts new file mode 100644 index 00000000..00738479 --- /dev/null +++ b/packages/moq/karp/catalog/audio.ts @@ -0,0 +1,20 @@ +import { type Track, decodeTrack } from "./track" + +export interface Audio { + track: Track + codec: string + sample_rate: number + channel_count: number + bitrate?: number +} + +export function decodeAudio(o: unknown): o is Audio { + if (typeof o !== "object" || o === null) return false + + const obj = o as Partial