Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f3466546e | |||
| 8170383278 | |||
| 2e86f68a3c | |||
| 35b2a06e64 | |||
| 09985e6031 | |||
| 056a673bc0 | |||
| 411d923c2a | |||
| ff14f577fe | |||
| 083a11274e | |||
| 2fcb853c30 | |||
| feaa9f2bda | |||
| aa3fcf09cf | |||
| a30082eebe | |||
| 7c75e64a64 | |||
| 65883917b0 | |||
| c8119d0efa | |||
| d7b88874df | |||
| f001a192e1 | |||
| 37c0bce57e |
@@ -0,0 +1,27 @@
|
||||
name: Build android container
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 4 * * *"
|
||||
|
||||
jobs:
|
||||
android-release-builder-container-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.ohea.xyz
|
||||
username: ${{ secrets.CI_REGISTRY_USER }}
|
||||
password: ${{ secrets.CI_REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Android builder image
|
||||
shell: bash
|
||||
run: |
|
||||
docker pull "$(grep -m1 '^FROM' ./docker/android-release-builder.Dockerfile | awk '{print $2}')"
|
||||
docker build -t git.ohea.xyz/mumble/mumble-web2/android-release-builder:latest -f ./docker/android-release-builder.Dockerfile .
|
||||
docker push git.ohea.xyz/mumble/mumble-web2/android-release-builder:latest
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||
|
||||
- name: Install dioxus-cli
|
||||
run: cargo binstall dioxus-cli --version 0.7.2
|
||||
run: cargo binstall dioxus-cli --version 0.7.3
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -83,3 +83,26 @@ jobs:
|
||||
name: mumble-web2-gui-windows
|
||||
path: gui/dist
|
||||
retention-days: 5
|
||||
|
||||
android_build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: git.ohea.xyz/mumble/mumble-web2/android-release-builder:latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build dioxus project (x86_64-linux-android)
|
||||
run: dx build --platform android --target x86_64-linux-android --release -p mumble-web2-gui
|
||||
|
||||
- name: Build dioxus project (aarch64-linux-android)
|
||||
run: dx build --platform android --target aarch64-linux-android --release -p mumble-web2-gui
|
||||
|
||||
- name: Upload mumble-web2-gui Android Artifact
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: mumble-web2-android
|
||||
path: target/dx/mumble-web2-gui/release/android/app/app/build/outputs/apk/debug/app-debug.apk
|
||||
retention-days: 5
|
||||
|
||||
Generated
+166
-403
@@ -127,6 +127,37 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-permissions"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3b4eabf57cddc2cd9934aabb1d17f3c9f1f1cfcac48746944fcff76ed0f62bb"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"jni",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_log-sys"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
||||
|
||||
[[package]]
|
||||
name = "android_logger"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f"
|
||||
dependencies = [
|
||||
"android_log-sys",
|
||||
"env_logger",
|
||||
"log",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -157,50 +188,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.2",
|
||||
"raw-window-handle 0.6.2",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
@@ -867,15 +854,6 @@ dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
@@ -918,7 +896,17 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad7154afa56de2f290e3c82c2c6dc4f5b282b6870903f56ef3509aba95866edc"
|
||||
dependencies = [
|
||||
"const-serialize-macro",
|
||||
"const-serialize-macro 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-serialize"
|
||||
version = "0.8.0-alpha.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e42cd5aabba86f128b3763da1fec1491c0f728ce99245062cd49b6f9e6d235b"
|
||||
dependencies = [
|
||||
"const-serialize 0.7.2",
|
||||
"const-serialize-macro 0.8.0-alpha.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -933,6 +921,17 @@ dependencies = [
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-serialize-macro"
|
||||
version = "0.8.0-alpha.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42571ed01eb46d2e1adcf99c8ca576f081e46f2623d13500eba70d1d99a4c439"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-str"
|
||||
version = "0.7.0"
|
||||
@@ -1479,9 +1478,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a115f9dbe5900c6044ee6a791e1b160c29989c6a8721eec099e01a964e5dae4"
|
||||
checksum = "92b583b48ac77158495e6678fe3a2b5954fc8866fc04cb9695dd146e88bc329d"
|
||||
dependencies = [
|
||||
"dioxus-asset-resolver",
|
||||
"dioxus-cli-config",
|
||||
@@ -1507,9 +1506,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-asset-resolver"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6851ae49ba3988f1b77f6ef826eb142e811602129841c24bf5a4e103708d9844"
|
||||
checksum = "c0161af1d3cfc8ff31503ff1b7ee0068c97771fc38d0cc6566e23483142ddf4f"
|
||||
dependencies = [
|
||||
"dioxus-cli-config",
|
||||
"http",
|
||||
@@ -1528,18 +1527,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-cli-config"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c59e9d9da2e7334fdae5d77e3989207aa549062f74ff1ca2171393bbdd7fda90"
|
||||
checksum = "ccd67ab405e1915a47df9769cd5408545d1b559d5c01ce7a0f442caef520d1f3"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-config-macro"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bd56be5ea6c9f416b25e9e3adc910c02127be75b6d1ecd567661f31920b27ba"
|
||||
checksum = "f040ec7c41aa5428283f56bb0670afba9631bfe3ffd885f4814807f12c8c9d91"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1547,15 +1546,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-config-macros"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c49327465c2d434d00fb4c86bd35ae72155b479622e09352b950d9ab4807bf23"
|
||||
checksum = "10c41b47b55a433b61f7c12327c85ba650572bacbcc42c342ba2e87a57975264"
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-core"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7400cbd21a98e585a13f8c29574da9b8afb2fd343f712618042b6c71761f0933"
|
||||
checksum = "b389b0e3cc01c7da292ad9b884b088835fdd1671d45fbd2f737506152b22eef0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -1575,9 +1574,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-core-macro"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51c0eb7eb76dd5a0b9a116d94d29ca78924a1ed1fcb7ea072eda5045d3ac056"
|
||||
checksum = "6a82d65f0024fc86f01911a16156d280eea583be5a82a3bed85e7e8e4194302d"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"dioxus-rsx",
|
||||
@@ -1588,15 +1587,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-core-types"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0652ab5f9c2c32261d44a3155debbfd909ed03d03434d7f70f5a796bf255c519"
|
||||
checksum = "bfc4b8cdc440a55c17355542fc2089d97949bba674255d84cac77805e1db8c9f"
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-desktop"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b24aa7e4aa87fce202c5e67d560cddd9ed67ad533f16b7d922916c04993766ff"
|
||||
checksum = "7e6ec66749d1556636c5b4f661495565c155a7f78a46d4d007d7478c6bdc288c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
@@ -1630,7 +1629,7 @@ dependencies = [
|
||||
"objc_id",
|
||||
"percent-encoding",
|
||||
"rand 0.9.2",
|
||||
"rfd 0.15.4",
|
||||
"rfd 0.17.2",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1649,9 +1648,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-devtools"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9748128bcd102b10e58c765939807053ccab542206a939b8bab228077455c259"
|
||||
checksum = "dcf89488bad8fb0f18b9086ee2db01f95f709801c10c68be42691a36378a0f2d"
|
||||
dependencies = [
|
||||
"dioxus-cli-config",
|
||||
"dioxus-core",
|
||||
@@ -1667,9 +1666,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-devtools-types"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48540ca8a0ab1ec81cd4db35f0c9713d43b158647fc1dcb0d79965fc3b41d96c"
|
||||
checksum = "6e7381d9d7d0a0f66b9d5082d584853c3d53be21d34007073daca98ddf26fc4d"
|
||||
dependencies = [
|
||||
"dioxus-core",
|
||||
"serde",
|
||||
@@ -1678,9 +1677,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-document"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501a189b391d091c9aa02c05f5b25f5d0d17fa0e1016e000b0fdbb073d77cd6a"
|
||||
checksum = "6ba0aeeff26d9d06441f59fd8d7f4f76098ba30ca9728e047c94486161185ceb"
|
||||
dependencies = [
|
||||
"dioxus-core",
|
||||
"dioxus-core-macro",
|
||||
@@ -1697,9 +1696,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-fullstack"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54150804265defdb21a6f2d8914a45316a1e7fb70ab22c30cf836e8fe2f8081b"
|
||||
checksum = "7db1f8b70338072ec408b48d09c96559cf071f87847465d8161294197504c498"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -1754,9 +1753,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-fullstack-core"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a9be2ef4d701520eefef284d218fb35b159dccd6bccc02b5bad42945e2599d"
|
||||
checksum = "cda8b152e85121243741b9d5f2a3d8cb3c47a7b2299e902f98b6a7719915b0a2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum-core",
|
||||
@@ -1782,9 +1781,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-fullstack-macro"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31ea4451fe8c9d2af24fb718a94966d5fd7e11325777e5b5a59085c5c85e5fb"
|
||||
checksum = "255104d4a4f278f1a8482fa30536c91d22260c561c954b753e72987df8d65b2e"
|
||||
dependencies = [
|
||||
"const_format",
|
||||
"convert_case 0.8.0",
|
||||
@@ -1796,9 +1795,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-history"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55d704b3ba9504cb3c9cde49499b75546d1faaff2736f4c368aca6c061c48ac3"
|
||||
checksum = "8d00ba43bfe6e5ca226fef6128f240ca970bea73cac0462416188026360ccdcf"
|
||||
dependencies = [
|
||||
"dioxus-core",
|
||||
"tracing",
|
||||
@@ -1806,9 +1805,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-hooks"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79c6d68be372eca8186a1c57ec49be67a6ea46022150b5e85ab6a6acde52d272"
|
||||
checksum = "dab2da4f038c33cb38caa37ffc3f5d6dfbc018f05da35b238210a533bb075823"
|
||||
dependencies = [
|
||||
"dioxus-core",
|
||||
"dioxus-signals",
|
||||
@@ -1822,9 +1821,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-html"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aa87ecfa0f38ec286be25789a7f2d6c30778111f1fbff563da4bae41d171496"
|
||||
checksum = "eded5fa6d2e677b7442a93f4228bf3c0ad2597a8bd3292cae50c869d015f3a99"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -1849,9 +1848,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-html-internal-macro"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49301d0e389378e8070b8b704110339a0d3358efad9f5ad483ffab3a8d406dae"
|
||||
checksum = "45462ab85fe059a36841508d40545109fd0e25855012d22583a61908eb5cd02a"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro2",
|
||||
@@ -1861,9 +1860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-interpreter-js"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5437a89d3ef7edfebc0f10acb065f1709cb7ffb678e3a4bb1416706d71f7c67"
|
||||
checksum = "a42a7f73ad32a5054bd8c1014f4ac78cca3b7f6889210ee2b57ea31b33b6d32f"
|
||||
dependencies = [
|
||||
"dioxus-core",
|
||||
"dioxus-core-types",
|
||||
@@ -1881,9 +1880,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-logger"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b25ebfbc193cebcf5af5e19b8ee7c6adee486fbd1c12f11aea058b464da16f9"
|
||||
checksum = "f1eeab114cb009d9e6b85ea10639a18cfc54bb342f3b837770b004c4daeb89c2"
|
||||
dependencies = [
|
||||
"dioxus-cli-config",
|
||||
"tracing",
|
||||
@@ -1893,9 +1892,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-rsx"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d97c02689beff55767ba5f6e185ffd204c6a193e372f0fead8a3722c6f7eea"
|
||||
checksum = "53128858f0ccca9de54292a4d48409fda1df75fd5012c6243f664042f0225d68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
@@ -1906,9 +1905,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-signals"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27fc4df7a31a7f02e5a0b40884bb66ee165226a05d75fce03baa44029e438762"
|
||||
checksum = "2f48020bc23bc9766e7cce986c0fd6de9af0b8cbfd432652ec6b1094439c1ec6"
|
||||
dependencies = [
|
||||
"dioxus-core",
|
||||
"futures-channel",
|
||||
@@ -1922,9 +1921,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-stores"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2dec3cd677078824a733de25ddbe8e987cfc8d98aec29b7d199e1fdb8452b96"
|
||||
checksum = "77aaa9ac56d781bb506cf3c0d23bea96b768064b89fe50d3b4d4659cc6bd8058"
|
||||
dependencies = [
|
||||
"dioxus-core",
|
||||
"dioxus-signals",
|
||||
@@ -1934,9 +1933,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-stores-macro"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9b7f085e374aaaa78403227b9bd83675c4078388d41a41b67dfbe4aa0bb64d5"
|
||||
checksum = "5b1a728622e7b63db45774f75e71504335dd4e6115b235bbcff272980499493a"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro2",
|
||||
@@ -1946,9 +1945,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-web"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "315009f3a77c3c813415b3b8a8ea62a4d7a32dde9a666664b30862d4386e8456"
|
||||
checksum = "3b33fe739fed4e8143dac222a9153593f8e2451662ce8fc4c9d167a9d6ec0923"
|
||||
dependencies = [
|
||||
"dioxus-cli-config",
|
||||
"dioxus-core",
|
||||
@@ -2027,15 +2026,6 @@ dependencies = [
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlib"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlopen2"
|
||||
version = "0.8.0"
|
||||
@@ -2196,12 +2186,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
@@ -2209,7 +2193,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2244,6 +2227,16 @@ dependencies = [
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -2291,27 +2284,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.12"
|
||||
@@ -2524,19 +2496,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
@@ -2674,9 +2633,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generational-box"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e658d10252a15200ca4a1c67c7180fc0baffa3f92869bbd903025daf6f70fd65"
|
||||
checksum = "cc4ed190b9de8e734d47a70be59b1e7588b9e8e0d0036e332f4c014e8aed1bc5"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
"tracing",
|
||||
@@ -3688,9 +3647,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lazy-js-bundle"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21972afec4627b7ba0de60b5269585b5ac2f56d559b0696f57eee6daf8a51b68"
|
||||
checksum = "c7b88b715ab1496c6e6b8f5e927be961c4235196121b6ae59bcb51077a21dd36"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
@@ -3993,32 +3952,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "manganis"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97c63ae68d25457a579b7714806088c5cb44c536cf624a53a17184878f9f0bcd"
|
||||
checksum = "6cce7d688848bf9d034168513b9a2ffbfe5f61df2ff14ae15e6cfc866efdd344"
|
||||
dependencies = [
|
||||
"const-serialize",
|
||||
"const-serialize 0.7.2",
|
||||
"const-serialize 0.8.0-alpha.0",
|
||||
"manganis-core",
|
||||
"manganis-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "manganis-core"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88d071660b149f985cbab8b23f2004ea6dd5cf947b63a0843f0e2f46e6af7229"
|
||||
checksum = "84ce917b978268fe8a7db49e216343ec7c8f471f7e686feb70940d67293f19d4"
|
||||
dependencies = [
|
||||
"const-serialize",
|
||||
"const-serialize 0.7.2",
|
||||
"const-serialize 0.8.0-alpha.0",
|
||||
"dioxus-cli-config",
|
||||
"dioxus-core-types",
|
||||
"serde",
|
||||
"winnow 0.7.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "manganis-macro"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9793d1d33778245b4240c330a8f575d208ce077c7e7bab1c79064252ddd4a162"
|
||||
checksum = "ad513e990f7c0bca86aa68659a7a3dc4c705572ed4c22fd6af32ccf261334cc2"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"macro-string",
|
||||
@@ -4268,6 +4230,7 @@ dependencies = [
|
||||
name = "mumble-web2-gui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"android-permissions",
|
||||
"async_cell",
|
||||
"asynchronous-codec",
|
||||
"base64",
|
||||
@@ -4286,6 +4249,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"gloo-timers",
|
||||
"html-purifier",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"lol_html 2.7.0",
|
||||
"markdown",
|
||||
@@ -4293,6 +4257,7 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"mumble-protocol-2x",
|
||||
"mumble-web2-common",
|
||||
"ndk-context",
|
||||
"ogg 0.9.2",
|
||||
"once_cell",
|
||||
"opus",
|
||||
@@ -4437,7 +4402,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4818,16 +4782,6 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordermap"
|
||||
version = "0.5.12"
|
||||
@@ -4892,12 +4846,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -5470,15 +5418,6 @@ dependencies = [
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@@ -5834,30 +5773,6 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"pollster",
|
||||
"raw-window-handle 0.6.2",
|
||||
"urlencoding",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
@@ -5880,6 +5795,30 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"pollster",
|
||||
"raw-window-handle 0.6.2",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -7015,9 +6954,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "subsecond"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c09bc2c9ef0381b403ab8b58122961cb83266d16b1f55f9486d5857ba4a9ae26"
|
||||
checksum = "8438668e545834d795d04c4335aafc332ce046106521a29f0a5c6501de34187c"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
@@ -7034,9 +6973,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "subsecond-types"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07aa455c66ddfdbb51507537402b961e027846468954ef8d974bce65dff9eb0"
|
||||
checksum = "1e72f747606fc19fe81d6c59e491af93ed7dcbcb6aad9d1d18b05129914ec298"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -7356,7 +7295,6 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -7480,7 +7418,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 0.7.3",
|
||||
"toml_parser",
|
||||
"winnow 0.7.13",
|
||||
"winnow 0.7.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7489,7 +7427,7 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
|
||||
dependencies = [
|
||||
"winnow 0.7.13",
|
||||
"winnow 0.7.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7866,17 +7804,6 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ulid"
|
||||
version = "1.2.1"
|
||||
@@ -7954,12 +7881,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
@@ -7979,7 +7900,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -8140,66 +8060,6 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"rustix",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"rustix",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.82"
|
||||
@@ -8764,9 +8624,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.13"
|
||||
version = "0.7.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -8940,62 +8800,6 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"nix",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow 0.7.13",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"winnow 0.7.13",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.27"
|
||||
@@ -9103,44 +8907,3 @@ dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"url",
|
||||
"winnow 0.7.13",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.108",
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
+4
-3
@@ -1,9 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct ProxyOverides {
|
||||
pub proxy_url: String,
|
||||
pub cert_hash: Vec<u8>,
|
||||
pub struct ClientConfig {
|
||||
pub proxy_url: Option<String>,
|
||||
pub cert_hash: Option<Vec<u8>>,
|
||||
pub any_server: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
FROM rust:trixie
|
||||
|
||||
ARG ANDROID_CLI_TOOLS_VERSION=13114758
|
||||
|
||||
# Install android rust toolchains
|
||||
RUN rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
|
||||
|
||||
# Install debian dependencies
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
ca-certificates \
|
||||
curl \
|
||||
unzip \
|
||||
default-jdk
|
||||
|
||||
# Install android commandline tools (required to install the sdk)
|
||||
RUN cd /tmp && \
|
||||
curl -o commandlinetools-linux.zip "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CLI_TOOLS_VERSION}_latest.zip" && \
|
||||
unzip commandlinetools-linux.zip && \
|
||||
mkdir -p /opt/android-tools/cmdline-tools && \
|
||||
cp -r cmdline-tools /opt/android-tools/cmdline-tools/latest
|
||||
|
||||
|
||||
# Install required android tools
|
||||
RUN yes | /opt/android-tools/cmdline-tools/latest/bin/sdkmanager --install "platform-tools" "platforms;android-36.1" "build-tools;36.1.0" "ndk;29.0.14206865" "cmake;3.31.6"
|
||||
|
||||
# Install cargo binstall
|
||||
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||
|
||||
# Install dioxus-cli
|
||||
RUN cargo binstall dioxus-cli@0.7.3
|
||||
|
||||
# Install bindgen-cli
|
||||
RUN cargo binstall bindgen-cli
|
||||
|
||||
# Set required env vars
|
||||
ENV ANDROID_HOME="/opt/android-tools/"
|
||||
ENV NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865"
|
||||
ENV PATH="$PATH:$ANDROID_HOME/platform-tools"
|
||||
ENV PATH="$PATH:/opt/android-tools/cmake/3.31.6/bin/"
|
||||
ENV LLVM_CONFIG_PATH="/opt/android-tools/ndk/29.0.14206865/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-config"
|
||||
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
- "64444:64444/tcp"
|
||||
- "64444:64444/udp"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:z
|
||||
#- caddy_data:/data
|
||||
#- caddy_config:/config
|
||||
depends_on:
|
||||
@@ -35,8 +35,8 @@ services:
|
||||
image: rust:latest
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ..:/app
|
||||
- ./proxy-config.toml:/app/config.toml
|
||||
- ..:/app:z
|
||||
- ./proxy-config.toml:/app/config.toml:z
|
||||
ports:
|
||||
- "4433:4433/tcp"
|
||||
- "4433:4433/udp"
|
||||
|
||||
@@ -44,14 +44,12 @@ RUN choco install rustup.install -y --no-progress
|
||||
RUN rustup toolchain install stable-x86_64-pc-windows-msvc
|
||||
RUN rustup default stable-x86_64-pc-windows-msvc
|
||||
|
||||
# Install carog binstall
|
||||
# Install cargo binstall
|
||||
RUN Set-ExecutionPolicy Unrestricted -Scope Process; `
|
||||
iex (Invoke-WebRequest "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.ps1" -UseBasicParsing).Content
|
||||
|
||||
SHELL ["C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat", "&&", "powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
|
||||
# Install dioxus-cli from git HEAD with cargo
|
||||
# This is to work around a bug in the windows builder upstream.
|
||||
# Dioxus has released 0.7.2, but it seems to be broken for now.
|
||||
RUN cargo binstall dioxus-cli
|
||||
# Install dioxus-cli
|
||||
RUN cargo binstall dioxus-cli@0.7.3
|
||||
|
||||
ENTRYPOINT ["C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat", "&&", "powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
|
||||
|
||||
+23
-1
@@ -88,13 +88,13 @@ tracing = "^0.1.40"
|
||||
color-eyre = "^0.6.3"
|
||||
crossbeam-queue = "^0.3.11"
|
||||
lol_html = "^2.2.0"
|
||||
rfd = { git = "https://github.com/PolyMeilex/rfd.git", version = "^0.16.0", default-features = false }
|
||||
base64 = "^0.22"
|
||||
mime_guess = "^2.0.5"
|
||||
async_cell = "^0.2.3"
|
||||
reqwest = { version = "^0.12.22", features = ["json"] }
|
||||
dioxus-asset-resolver = "0.7.2"
|
||||
|
||||
|
||||
# Denoising
|
||||
# =========
|
||||
deep_filter = { git = "https://github.com/Rikorose/DeepFilterNet.git", rev = "d375b2d8309e0935d165700c91da9de862a99c31", features = [
|
||||
@@ -102,6 +102,18 @@ deep_filter = { git = "https://github.com/Rikorose/DeepFilterNet.git", rev = "d3
|
||||
] }
|
||||
crossbeam = "0.8.4"
|
||||
|
||||
# Platform Integration
|
||||
# ====================
|
||||
# rfd only supports windows, macos, linux, and wasm32. No support for Android or iOS
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos", target_arch = "wasm32"))'.dependencies]
|
||||
rfd = { git = "https://github.com/PolyMeilex/rfd.git", version = "^0.16.0", default-features = false, optional = true }
|
||||
|
||||
# Android dependencies for requesting permissions
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android-permissions = "0.1.2"
|
||||
jni = "0.21.1"
|
||||
ndk-context = "0.1.1"
|
||||
|
||||
[patch.crates-io]
|
||||
tract-hir = "=0.12.4"
|
||||
tract-core = "=0.12.4"
|
||||
@@ -121,6 +133,7 @@ web = [
|
||||
"gloo-timers",
|
||||
"tracing-web",
|
||||
"deep_filter/wasm",
|
||||
"rfd",
|
||||
]
|
||||
desktop = [
|
||||
"dioxus/desktop",
|
||||
@@ -133,3 +146,12 @@ desktop = [
|
||||
"rfd/xdg-portal",
|
||||
"etcetera",
|
||||
]
|
||||
mobile = [
|
||||
"dioxus/mobile",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing-subscriber/env-filter",
|
||||
"opus",
|
||||
"cpal",
|
||||
"dasp_ring_buffer",
|
||||
]
|
||||
|
||||
+3
-1
@@ -8,6 +8,8 @@ out_dir = "dist"
|
||||
# resource (public) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
android_manifest = "build/AndroidManifest.xml"
|
||||
|
||||
[web.app]
|
||||
# HTML title tag content
|
||||
title = "Mumble Web 2"
|
||||
@@ -33,7 +35,7 @@ style = []
|
||||
script = []
|
||||
|
||||
[bundle]
|
||||
identifier = "xyz.ohea.mumble-web-2"
|
||||
identifier = "xyz.ohea.mumble_web_2"
|
||||
publisher = "OheaCorp"
|
||||
icon = [
|
||||
"icons/32x32.png",
|
||||
|
||||
+134
-11
@@ -83,6 +83,44 @@ a:visited {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.channel_header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.channel_arrow {
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.channel_arrow--placeholder {
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* The whole right side of the row is the dblclick target */
|
||||
.channel_row_click {
|
||||
flex: 1;
|
||||
padding: 0.1rem 0.25rem 0.1rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Hover highlight for whole row area (title + blank space) */
|
||||
.channel_row_click:hover {
|
||||
background-color: var(--channel-hover-bg, #222); /* pick your color */
|
||||
}
|
||||
|
||||
|
||||
/* still keep text non-selectable if desired */
|
||||
.channel_details {
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.channel {
|
||||
&_details {
|
||||
flex: 0 0 100%;
|
||||
@@ -168,26 +206,68 @@ a:visited {
|
||||
background-color: oklch(0.53 0.1431 264.18);
|
||||
border-radius: 50%;
|
||||
aspect-ratio: 1 / 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
}
|
||||
}
|
||||
|
||||
.button_row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: clamp(4px, 1vw, 10px);
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
min-height: 0;
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.connection_status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.user_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
flex-shrink: 1;
|
||||
|
||||
.user_name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user_data {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle_button {
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
padding: clamp(4px, 0.5vw, 8px);
|
||||
aspect-ratio: 1 / 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
background-color: unset;
|
||||
|
||||
border: solid rgb(255 255 255 / 0.1) 3px;
|
||||
border-radius: 10px;
|
||||
border: solid rgb(255 255 255 / 0.1) clamp(1px, 0.3vw, 3px);
|
||||
border-radius: clamp(4px, 0.8vw, 10px);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
|
||||
transition: all 0.5s ease-in-out;
|
||||
@@ -200,7 +280,6 @@ a:visited {
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
vertical-align: middle;
|
||||
font-size: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,16 +324,60 @@ a:visited {
|
||||
}
|
||||
|
||||
&_control_box {
|
||||
padding: 16px;
|
||||
margin: 16px;
|
||||
padding: clamp(6px, 0.8vw, 12px);
|
||||
margin: clamp(6px, 0.8vw, 12px);
|
||||
background-color: var(--light-bg-color);
|
||||
border-radius: 10px;
|
||||
border-radius: clamp(6px, 1vw, 10px);
|
||||
overflow: hidden;
|
||||
grid-area: control;
|
||||
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: clamp(4px, 0.8vw, 8px);
|
||||
flex-direction: column;
|
||||
|
||||
// Dynamic font sizing for control elements
|
||||
--control-icon-size: clamp(16px, 2.5vw, 30px);
|
||||
--control-text-size: clamp(12px, 2vw, 25px);
|
||||
--control-small-text-size: clamp(10px, 1.5vw, 20px);
|
||||
--user-icon-size: clamp(24px, 4vw, 45px);
|
||||
--toggle-icon-size: clamp(18px, 3vw, 35px);
|
||||
|
||||
.connection_status {
|
||||
.material-symbols-outlined {
|
||||
font-size: var(--control-icon-size);
|
||||
}
|
||||
.status_text {
|
||||
font-size: var(--control-text-size);
|
||||
}
|
||||
.channel_text {
|
||||
font-size: var(--control-small-text-size);
|
||||
}
|
||||
}
|
||||
|
||||
.user_edit_button {
|
||||
.material-symbols-outlined {
|
||||
font-size: var(--user-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
.user_info {
|
||||
.user_name {
|
||||
font-size: var(--control-text-size);
|
||||
}
|
||||
.user_data {
|
||||
font-size: var(--control-small-text-size);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle_button {
|
||||
.material-symbols-outlined {
|
||||
font-size: var(--toggle-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,4 +430,4 @@ a:visited {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
|
||||
<!--
|
||||
Borrowed from https://github.com/irh/audio-app/blob/main/apps/dioxus/AndroidManifest.xml
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<uses-feature android:name="android.hardware.audio.low_latency" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.audio.output" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.audio.pro" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="false" />
|
||||
|
||||
<application android:hasCode="true" android:supportsRtl="true" android:icon="@mipmap/ic_launcher"
|
||||
android:extractNativeLibs="true"
|
||||
android:allowNativeHeapPointerTagging="false"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity android:configChanges="orientation|screenLayout|screenSize|keyboardHidden" android:exported="true"
|
||||
android:label="@string/app_name" android:name="dev.dioxus.main.MainActivity">
|
||||
<meta-data android:name="android.app.lib_name" android:value="dioxusmain" />
|
||||
<meta-data android:name="android.app.func_name" android:value="ANativeActivity_onCreate" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
+243
-110
@@ -2,11 +2,11 @@
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use mime_guess::Mime;
|
||||
use mumble_web2_common::{ProxyOverides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use ordermap::OrderSet;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::imp;
|
||||
use crate::imp::{Platform, PlatformInterface as _};
|
||||
|
||||
pub type ChannelId = u32;
|
||||
pub type UserId = u32;
|
||||
@@ -23,7 +23,7 @@ pub enum Command {
|
||||
Connect {
|
||||
address: String,
|
||||
username: String,
|
||||
config: ProxyOverides,
|
||||
config: ClientConfig,
|
||||
},
|
||||
SendChat {
|
||||
markdown: String,
|
||||
@@ -54,30 +54,27 @@ pub enum Command {
|
||||
use Command::*;
|
||||
use ConnectionState::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChannelState {
|
||||
pub name: String,
|
||||
pub children: OrderSet<ChannelId>,
|
||||
pub users: OrderSet<UserId>,
|
||||
pub parent: Option<ChannelId>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserState {
|
||||
pub name: String,
|
||||
pub channel: ChannelId,
|
||||
pub deaf: bool,
|
||||
pub mute: bool,
|
||||
pub suppress: bool,
|
||||
pub self_deaf: bool,
|
||||
pub self_mute: bool,
|
||||
}
|
||||
|
||||
impl UserState {
|
||||
pub fn icon(&self) -> UserIcon {
|
||||
match (self.mute || self.self_mute, self.deaf || self.self_deaf) {
|
||||
(false, false) => UserIcon::Normal,
|
||||
(true, false) => UserIcon::Muted,
|
||||
(_, true) => UserIcon::Deafened,
|
||||
if self.deaf || self.self_deaf {
|
||||
UserIcon::Deafened
|
||||
} else if self.mute || self.self_mute {
|
||||
UserIcon::Muted
|
||||
} else if self.suppress {
|
||||
UserIcon::Suppressed
|
||||
} else {
|
||||
UserIcon::Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,8 +86,121 @@ pub struct Chat {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ServerState {
|
||||
pub struct ChannelState {
|
||||
pub name: String,
|
||||
pub children: OrderSet<ChannelId>,
|
||||
pub users: OrderSet<UserId>,
|
||||
pub parent: Option<ChannelId>,
|
||||
pub position: i32,
|
||||
}
|
||||
|
||||
impl ChannelState {
|
||||
pub fn update_from_channel_state(
|
||||
&mut self,
|
||||
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
||||
) {
|
||||
if channel_state.has_position() {
|
||||
self.position = channel_state.get_position();
|
||||
}
|
||||
if channel_state.has_parent() {
|
||||
self.parent = Some(channel_state.get_parent());
|
||||
}
|
||||
if channel_state.has_name() {
|
||||
self.name = channel_state.get_name().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChannelsState {
|
||||
pub channels: HashMap<ChannelId, ChannelState>,
|
||||
}
|
||||
|
||||
impl ChannelsState {
|
||||
pub fn update_from_channel_state(
|
||||
&mut self,
|
||||
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
||||
) {
|
||||
self.channels
|
||||
.entry(channel_state.get_channel_id())
|
||||
.or_default()
|
||||
.update_from_channel_state(channel_state);
|
||||
|
||||
self.update_channel_parents();
|
||||
}
|
||||
|
||||
pub fn update_from_channel_remove(
|
||||
&mut self,
|
||||
channel_remove: &mumble_protocol::control::msgs::ChannelRemove,
|
||||
) {
|
||||
self.channels.remove(&channel_remove.get_channel_id());
|
||||
self.update_channel_parents();
|
||||
}
|
||||
|
||||
pub fn update_channel_parents(&mut self) {
|
||||
// Zero out existing children
|
||||
for state in self.channels.values_mut() {
|
||||
state.children.clear();
|
||||
}
|
||||
|
||||
let mut to_sort: Vec<(ChannelId, Option<ChannelId>, i32, String)> = Vec::new();
|
||||
for (id, state) in self.channels.iter() {
|
||||
// Handle channels with no parent (the root channel)
|
||||
let Some(parent_id) = state.parent else {
|
||||
to_sort.push((*id, None, 0, state.name.clone()));
|
||||
continue;
|
||||
};
|
||||
|
||||
// If a channel has a parent that we haven't gotten a channel
|
||||
// state packet for, ignore it
|
||||
if !self.channels.contains_key(&parent_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
to_sort.push((*id, Some(parent_id), state.position, state.name.clone()));
|
||||
}
|
||||
|
||||
let pos_name: HashMap<ChannelId, (i32, String)> = self
|
||||
.channels
|
||||
.iter()
|
||||
.map(|(&id, state)| (id, (state.position, state.name.clone())))
|
||||
.collect();
|
||||
|
||||
let mut updated: HashSet<ChannelId> = HashSet::new();
|
||||
|
||||
while updated.len() < to_sort.len() {
|
||||
for &(id, ref parent_id, position, ref name) in &to_sort {
|
||||
let Some(parent_id) = parent_id else {
|
||||
updated.insert(id);
|
||||
continue;
|
||||
};
|
||||
|
||||
if updated.contains(&id) || !updated.contains(&parent_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unwrap should never fail here since we pre filter
|
||||
let parent = self.channels.get_mut(&parent_id).unwrap();
|
||||
|
||||
let mut insert_index = parent.children.len();
|
||||
for (i, &child) in parent.children.iter().enumerate() {
|
||||
let (p, ref n) = pos_name[&child];
|
||||
if (position == p && name < n) || p > position {
|
||||
insert_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
parent.children.insert_before(insert_index, id);
|
||||
updated.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ServerState {
|
||||
pub channels_state: ChannelsState,
|
||||
pub users: HashMap<UserId, UserState>,
|
||||
pub chat: Vec<Chat>,
|
||||
pub session: Option<UserId>,
|
||||
@@ -117,6 +227,7 @@ pub enum UserIcon {
|
||||
Normal,
|
||||
Muted,
|
||||
Deafened,
|
||||
Suppressed,
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -128,7 +239,7 @@ impl UserIcon {
|
||||
use UserIcon::*;
|
||||
Some(match self {
|
||||
Normal => asset!("assets/mic-svgrepo-com.svg"),
|
||||
Muted => asset!("assets/mic-off-svgrepo-com.svg"),
|
||||
Muted | Suppressed => asset!("assets/mic-off-svgrepo-com.svg"),
|
||||
Deafened => asset!("assets/speaker-muted-svgrepo-com.svg"),
|
||||
None => return Option::None,
|
||||
})
|
||||
@@ -140,7 +251,7 @@ pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
|
||||
let color = match icon {
|
||||
UserIcon::Normal => "var(--accent-normal)",
|
||||
UserIcon::Muted => "var(--accent-muted)",
|
||||
UserIcon::Deafened => "var(--accent-deafened)",
|
||||
UserIcon::Suppressed | UserIcon::Deafened => "var(--accent-deafened)",
|
||||
UserIcon::None => "var(--accent-normal)",
|
||||
};
|
||||
|
||||
@@ -176,26 +287,57 @@ pub fn Channel(id: ChannelId) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
let server = STATE.server.read();
|
||||
let user = server.session.unwrap();
|
||||
let Some(state) = server.channels.get(&id) else {
|
||||
let Some(state) = server.channels_state.channels.get(&id) else {
|
||||
return rsx!("missing channel {id}");
|
||||
};
|
||||
|
||||
let mut open = use_signal(|| true);
|
||||
|
||||
let has_children = !state.users.is_empty() || !state.children.is_empty();
|
||||
|
||||
rsx!(
|
||||
details {
|
||||
div {
|
||||
class: "channel_details",
|
||||
open: true,
|
||||
summary {
|
||||
span {
|
||||
role: "button",
|
||||
ondoubleclick: move |evt| {
|
||||
|
||||
div {
|
||||
class: "channel_header",
|
||||
// Arrow: only toggles open
|
||||
if has_children {
|
||||
span {
|
||||
class: "channel_arrow",
|
||||
onclick: move |evt| {
|
||||
evt.stop_propagation();
|
||||
evt.prevent_default();
|
||||
let mut w = open.write();
|
||||
*w = !*w;
|
||||
},
|
||||
if *open.read() { "▾" } else { "▸" }
|
||||
}
|
||||
} else {
|
||||
span {
|
||||
class: "channel_arrow channel_arrow--placeholder",
|
||||
" "
|
||||
}
|
||||
}
|
||||
|
||||
// Clickable row area (everything except the arrow)
|
||||
div {
|
||||
class: "channel_row_click",
|
||||
ondblclick: move |evt| {
|
||||
evt.stop_propagation();
|
||||
evt.prevent_default();
|
||||
net.send(EnterChannel { channel: id, user })
|
||||
},
|
||||
"{state.name}"
|
||||
// remove dblclick from the inner span
|
||||
span {
|
||||
class: "channel_title",
|
||||
"{state.name}"
|
||||
}
|
||||
// if you add icons/badges later, put them here too
|
||||
}
|
||||
}
|
||||
if state.users.len() + state.children.len() > 0 {
|
||||
|
||||
if *open.read() && has_children {
|
||||
div {
|
||||
class: "channel_children",
|
||||
for id in state.users.iter() {
|
||||
@@ -210,6 +352,7 @@ pub fn Channel(id: ChannelId) -> Element {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "desktop", feature = "web"))]
|
||||
pub fn pick_and_send_file(net: &Coroutine<Command>) {
|
||||
let channels = if let Some(user) = STATE.server.read().this_user() {
|
||||
vec![user.channel]
|
||||
@@ -231,6 +374,8 @@ pub fn pick_and_send_file(net: &Coroutine<Command>) {
|
||||
});
|
||||
});
|
||||
}
|
||||
#[cfg(not(any(feature = "desktop", feature = "web")))]
|
||||
pub fn pick_and_send_file(net: &Coroutine<Command>) {}
|
||||
|
||||
#[component]
|
||||
pub fn ChatView() -> Element {
|
||||
@@ -309,7 +454,7 @@ pub fn ChatView() -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ControlView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
let status = &STATE.status;
|
||||
let server = STATE.server.read();
|
||||
@@ -317,6 +462,7 @@ pub fn ControlView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
deaf,
|
||||
self_deaf,
|
||||
mute,
|
||||
suppress,
|
||||
self_mute,
|
||||
ref name,
|
||||
channel,
|
||||
@@ -326,13 +472,12 @@ pub fn ControlView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
return rsx!();
|
||||
};
|
||||
|
||||
let current_channel_name = server.channels[&channel].name.clone();
|
||||
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
||||
|
||||
let proxy_url = config
|
||||
.read_unchecked()
|
||||
.as_ref()
|
||||
.and_then(|opt| opt.as_ref())
|
||||
.map(|gui_config| gui_config.proxy_url.clone());
|
||||
.and_then(|gui_config| gui_config.proxy_url.clone());
|
||||
|
||||
let connecting_color = "yellow";
|
||||
let connected_color = "oklch(0.55 0.1184 141.35)";
|
||||
@@ -342,79 +487,69 @@ pub fn ControlView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
let connection_status = match &*status.read() {
|
||||
Connecting => rsx! {
|
||||
div {
|
||||
style: "color: \"{connecting_color}\";",
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
style: "vertical-align: middle; font-size: 30px;",
|
||||
"signal_cellular_alt_2_bar"
|
||||
}
|
||||
span {
|
||||
style: "width: 5px; display: inline-block;"
|
||||
}
|
||||
span {
|
||||
style: "vertical-align: middle; font-size: 30px;",
|
||||
"Connecting"
|
||||
class: "connection_status",
|
||||
style: "color: {connecting_color};",
|
||||
div {
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_cellular_alt_2_bar"
|
||||
}
|
||||
span {
|
||||
class: "status_text",
|
||||
" Connecting"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Connected => rsx! {
|
||||
div {
|
||||
class: "connection_status",
|
||||
div {
|
||||
style: "color: \"{connected_color}\";",
|
||||
style: "color: {connected_color};",
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
style: "vertical-align: middle; font-size: 30px;",
|
||||
"signal_cellular_alt"
|
||||
}
|
||||
span {
|
||||
style: "width: 5px; display: inline-block;"
|
||||
}
|
||||
span {
|
||||
style: "vertical-align: middle; font-size: 25px;",
|
||||
"Connected"
|
||||
class: "status_text",
|
||||
" Connected"
|
||||
}
|
||||
}
|
||||
div {
|
||||
span { style: "width: 3px; display: inline-block;"}
|
||||
class: "channel_text",
|
||||
span { "{current_channel_name}" }
|
||||
if let Some(proxy_url) = proxy_url {
|
||||
span { " — " }
|
||||
span { "{proxy_url}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Disconnected => rsx! {
|
||||
div {
|
||||
style: "color: \"{disconnected_color}\";",
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
style: "vertical-align: middle;",
|
||||
"signal_disconnected"
|
||||
}
|
||||
span {
|
||||
style: "width: 5px; display: inline-block;"
|
||||
}
|
||||
span {
|
||||
style: "vertical-align: middle;",
|
||||
"Disconnected"
|
||||
class: "connection_status",
|
||||
style: "color: {disconnected_color};",
|
||||
div {
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_disconnected"
|
||||
}
|
||||
span {
|
||||
class: "status_text",
|
||||
" Disconnected"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Failed(_) => rsx! {
|
||||
div {
|
||||
style: "color: \"{failed_color}\";",
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
style: "vertical-align: middle;",
|
||||
"error"
|
||||
}
|
||||
span {
|
||||
style: "width: 5px; display: inline-block;"
|
||||
}
|
||||
span {
|
||||
style: "vertical-align: middle;",
|
||||
"Failed"
|
||||
class: "connection_status",
|
||||
style: "color: {failed_color};",
|
||||
div {
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"error"
|
||||
}
|
||||
span {
|
||||
class: "status_text",
|
||||
" Failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -446,16 +581,17 @@ pub fn ControlView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
class: "user_edit_button",
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
style: "color: oklch(0.65 0.2245 28.06); font-size: 45px; font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;",
|
||||
style: "color: oklch(0.65 0.2245 28.06);",
|
||||
"person_edit"
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "user_info",
|
||||
div {
|
||||
span { style: "font-size: 25px;", "{name}" }
|
||||
span { class: "user_name", "{name}" }
|
||||
}
|
||||
div {
|
||||
span { style: "font-size: 20px; color: gray;", "some data" }
|
||||
span { class: "user_data", "some data" }
|
||||
}
|
||||
}
|
||||
span { class: "spacer" }
|
||||
@@ -477,15 +613,15 @@ pub fn ControlView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: match mute || self_mute {
|
||||
class: match mute || suppress || self_mute {
|
||||
true => "toggle_button is_on",
|
||||
false => "toggle_button",
|
||||
},
|
||||
role: "switch",
|
||||
aria_checked: mute || self_mute,
|
||||
disabled: mute,
|
||||
aria_checked: mute || suppress || self_mute,
|
||||
disabled: mute || suppress,
|
||||
onclick: move |_| net.send(SetMute { mute: !self_mute }),
|
||||
match mute || self_mute {
|
||||
match mute || suppress || self_mute {
|
||||
true => rsx!(span { class: "material-symbols-outlined", "mic_off"}),
|
||||
false => rsx!(span { class: "material-symbols-outlined", "mic"}),
|
||||
}
|
||||
@@ -509,7 +645,7 @@ pub fn ControlView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ServerView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
let server = STATE.server.read();
|
||||
let Some(&UserState {
|
||||
@@ -528,7 +664,7 @@ pub fn ServerView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
class: "server_grid",
|
||||
div {
|
||||
class: "server_channel_box",
|
||||
for (id, state) in server.channels.iter() {
|
||||
for (id, state) in server.channels_state.channels.iter() {
|
||||
if state.parent.is_none() {
|
||||
Channel { id: *id }
|
||||
}
|
||||
@@ -547,43 +683,42 @@ pub fn ServerView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoginView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
|
||||
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
||||
use_resource(move || async move {
|
||||
let client = reqwest::Client::new();
|
||||
loop {
|
||||
*last_status.write_unchecked() = Some(imp::get_status(&client).await);
|
||||
imp::sleep(std::time::Duration::from_secs_f32(1.0)).await;
|
||||
*last_status.write_unchecked() = Some(Platform::get_status(&client).await);
|
||||
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
|
||||
}
|
||||
});
|
||||
|
||||
let mut address_input = use_signal(|| imp::load_server_url());
|
||||
let mut address_input = use_signal(|| Platform::load_server_url());
|
||||
let address = use_memo(move || {
|
||||
if let Some(addr) = address_input() {
|
||||
addr.clone()
|
||||
} else {
|
||||
config()
|
||||
.flatten()
|
||||
.map(|c| c.proxy_url.clone())
|
||||
.and_then(|c| c.proxy_url.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
});
|
||||
|
||||
let previous_username = imp::load_username();
|
||||
let previous_username = Platform::load_username();
|
||||
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
|
||||
|
||||
let do_connect = move |_| {
|
||||
//let _ = set_default_username(&username.read());
|
||||
let _ = imp::set_default_username(&username.read());
|
||||
if config.read().as_ref().is_some_and(|opt| opt.is_none()) {
|
||||
imp::set_default_server(&address.read());
|
||||
let _ = Platform::set_default_username(&username.read());
|
||||
if config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||
Platform::set_default_server(&address.read());
|
||||
}
|
||||
net.send(Connect {
|
||||
address: address.read().clone(),
|
||||
username: username.read().clone(),
|
||||
config: config.read().clone().flatten().unwrap_or_default(),
|
||||
config: config.read().clone().unwrap_or_default(),
|
||||
})
|
||||
};
|
||||
let status = &STATE.status;
|
||||
@@ -628,7 +763,7 @@ pub fn LoginView(config: Resource<Option<ProxyOverides>>) -> Element {
|
||||
None => rsx!(),
|
||||
}
|
||||
}
|
||||
if config.read().as_ref().is_some_and(|opt| opt.is_none()) {
|
||||
if config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||
div {
|
||||
label {
|
||||
for: "address-entry",
|
||||
@@ -725,24 +860,22 @@ pub fn app() -> Element {
|
||||
|
||||
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
|
||||
let config = use_resource(|| async move {
|
||||
match imp::load_proxy_overides().await {
|
||||
match Platform::load_config().await {
|
||||
Ok(config) => config,
|
||||
Err(_) => None,
|
||||
Err(_) => ClientConfig::default(),
|
||||
}
|
||||
});
|
||||
|
||||
Platform::request_permissions();
|
||||
|
||||
rsx!(
|
||||
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" }
|
||||
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" }
|
||||
document::Link{ rel: "stylesheet", href: STYLE }
|
||||
|
||||
if config.read().is_none() {
|
||||
div { class: "loading", "Loading..." }
|
||||
} else {
|
||||
match *STATE.status.read() {
|
||||
Connected => rsx!(ServerView { config }),
|
||||
_ => rsx!(LoginView { config }),
|
||||
}
|
||||
match *STATE.status.read() {
|
||||
Connected => rsx!(ServerView { config }),
|
||||
_ => rsx!(LoginView { config }),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+69
-9
@@ -7,9 +7,24 @@ use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::imp;
|
||||
use crate::imp::SpawnHandle;
|
||||
|
||||
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
|
||||
// TODO: make this user configurable.
|
||||
static DEFAULT_NOISE_FLOOR: f32 = 0.001;
|
||||
// 200ms hold at 48kHz sample rate
|
||||
static HOLD_SAMPLES_MAX: usize = 48000 / 5; // 9600 samples = 200ms
|
||||
|
||||
/// Indicates the transmission state after processing audio.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TransmitState {
|
||||
/// Audio is above threshold, or below but within hold period - transmit normally
|
||||
Transmitting,
|
||||
/// Hold period expired - send this frame as terminator (end_bit = true)
|
||||
Terminator,
|
||||
/// Silent and not transmitting - don't send anything
|
||||
Silent,
|
||||
}
|
||||
|
||||
enum DenoisingModelState {
|
||||
Nothing,
|
||||
@@ -17,10 +32,7 @@ enum DenoisingModelState {
|
||||
Availible(Box<DfTract>),
|
||||
}
|
||||
|
||||
fn with_denoising_model<O>(
|
||||
spawn: &imp::SpawnHandle,
|
||||
func: impl FnOnce(&mut DfTract) -> O,
|
||||
) -> Option<O> {
|
||||
fn with_denoising_model<O>(spawn: &SpawnHandle, func: impl FnOnce(&mut DfTract) -> O) -> Option<O> {
|
||||
// Using a thread local is super gross, but DfTract is not Send (so it can never leave the current
|
||||
// thread) while AudioProcessing itself might change threads whenever.
|
||||
thread_local! {
|
||||
@@ -74,30 +86,46 @@ fn with_denoising_model<O>(
|
||||
|
||||
pub struct AudioProcessor {
|
||||
denoise: bool,
|
||||
spawn: imp::SpawnHandle,
|
||||
spawn: SpawnHandle,
|
||||
buffer: Vec<f32>,
|
||||
noise_floor: f32,
|
||||
/// Whether we were transmitting in the previous frame
|
||||
was_transmitting: bool,
|
||||
/// Number of samples we've been below threshold (for hold period)
|
||||
hold_samples: usize,
|
||||
}
|
||||
|
||||
impl AudioProcessor {
|
||||
pub fn new_plain() -> Self {
|
||||
AudioProcessor {
|
||||
denoise: false,
|
||||
spawn: imp::SpawnHandle::current(),
|
||||
spawn: SpawnHandle::current(),
|
||||
buffer: Vec::new(),
|
||||
noise_floor: DEFAULT_NOISE_FLOOR,
|
||||
was_transmitting: false,
|
||||
hold_samples: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_denoising() -> Self {
|
||||
AudioProcessor {
|
||||
denoise: true,
|
||||
spawn: imp::SpawnHandle::current(),
|
||||
spawn: SpawnHandle::current(),
|
||||
buffer: Vec::new(),
|
||||
noise_floor: DEFAULT_NOISE_FLOOR,
|
||||
was_transmitting: false,
|
||||
hold_samples: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioProcessor {
|
||||
pub fn process(&mut self, audio: &[f32], channels: usize, output: &mut Vec<f32>) {
|
||||
pub fn process(
|
||||
&mut self,
|
||||
audio: &[f32],
|
||||
channels: usize,
|
||||
output: &mut Vec<f32>,
|
||||
) -> TransmitState {
|
||||
let mut include_raw = true;
|
||||
if self.denoise {
|
||||
with_denoising_model(&self.spawn, |df| {
|
||||
@@ -132,6 +160,38 @@ impl AudioProcessor {
|
||||
if include_raw {
|
||||
output.extend(audio.iter().step_by(channels).copied());
|
||||
}
|
||||
|
||||
// Calculate average amplitude for VAD
|
||||
let avg: f32 = if output.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
output.iter().map(|x| x.abs()).sum::<f32>() / output.len() as f32
|
||||
};
|
||||
|
||||
let above_threshold = avg >= self.noise_floor;
|
||||
let samples_in_frame = output.len();
|
||||
|
||||
let state = if above_threshold {
|
||||
// Above threshold - reset hold counter and transmit
|
||||
self.hold_samples = 0;
|
||||
self.was_transmitting = true;
|
||||
TransmitState::Transmitting
|
||||
} else if self.was_transmitting && self.hold_samples < HOLD_SAMPLES_MAX {
|
||||
// Below threshold but in hold period - keep transmitting
|
||||
self.hold_samples += samples_in_frame;
|
||||
TransmitState::Transmitting
|
||||
} else if self.was_transmitting {
|
||||
// Hold period expired - send terminator
|
||||
self.was_transmitting = false;
|
||||
self.hold_samples = 0;
|
||||
TransmitState::Terminator
|
||||
} else {
|
||||
// Not transmitting and below threshold - stay silent
|
||||
output.clear(); // Don't accumulate stale audio during silence
|
||||
TransmitState::Silent
|
||||
};
|
||||
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
use crate::app::Command;
|
||||
use color_eyre::eyre::{bail, Error};
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_protocol::control::ClientControlCodec;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::rustls;
|
||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use tokio_rustls::rustls::ClientConfig as RlsClientConfig;
|
||||
use tokio_rustls::rustls::DigitallySignedStruct;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoCertificateVerification;
|
||||
|
||||
impl ServerCertVerifier for NoCertificateVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA1,
|
||||
rustls::SignatureScheme::ECDSA_SHA1_Legacy,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
||||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA384,
|
||||
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA512,
|
||||
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA256,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA384,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA512,
|
||||
rustls::SignatureScheme::ED25519,
|
||||
rustls::SignatureScheme::ED448,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
let config = RlsClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||
.with_no_client_auth();
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
|
||||
let addr = format!("{}:{}", address, 64738)
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
let server_tcp = TcpStream::connect(addr).await?;
|
||||
let server_stream = connector
|
||||
//.connect("127.0.0.1".try_into()?, server_tcp)
|
||||
.connect(address.try_into()?, server_tcp)
|
||||
.await?;
|
||||
let (read_server, write_server) = tokio::io::split(server_stream);
|
||||
|
||||
let read_codec = ClientControlCodec::new();
|
||||
let write_codec = ClientControlCodec::new();
|
||||
|
||||
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
||||
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
||||
|
||||
crate::network_loop(username, event_rx, reader, writer).await
|
||||
}
|
||||
|
||||
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
bail!("status not supported on desktop yet")
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub use tokio::spawn;
|
||||
#[allow(unused)]
|
||||
pub type SpawnHandle = tokio::runtime::Handle;
|
||||
+56
-338
@@ -1,319 +1,82 @@
|
||||
use crate::app::Command;
|
||||
use crate::effects::{AudioProcessor, AudioProcessorSender};
|
||||
use color_eyre::eyre::{bail, eyre, Error};
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _};
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||
use futures::io::{AsyncRead, AsyncWrite};
|
||||
use mumble_protocol::control::ClientControlCodec;
|
||||
use mumble_web2_common::{ProxyOverides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use std::collections::HashMap;
|
||||
use std::mem::replace;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::rustls;
|
||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use tokio_rustls::rustls::ClientConfig as RlsClientConfig;
|
||||
use tokio_rustls::rustls::DigitallySignedStruct;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
||||
use tracing::{error, info, instrument, warn};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use tokio::runtime::Handle as SpawnHandle;
|
||||
pub use tokio::task::spawn;
|
||||
pub use tokio::time::sleep;
|
||||
/// Desktop platform implementation using Tokio and native audio.
|
||||
pub struct DesktopPlatform;
|
||||
|
||||
pub trait ImpRead: AsyncRead + Unpin + Send + 'static {}
|
||||
impl<T: AsyncRead + Unpin + Send + 'static> ImpRead for T {}
|
||||
impl super::PlatformInterface for DesktopPlatform {
|
||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||
|
||||
pub trait ImpWrite: AsyncWrite + Unpin + Send + 'static {}
|
||||
impl<T: AsyncWrite + Unpin + Send + 'static> ImpWrite for T {}
|
||||
async fn sleep(duration: Duration) {
|
||||
tokio::time::sleep(duration).await;
|
||||
}
|
||||
|
||||
pub struct AudioSystem {
|
||||
output: cpal::Device,
|
||||
input: cpal::Device,
|
||||
processors: AudioProcessorSender,
|
||||
recording_stream: Option<cpal::Stream>,
|
||||
}
|
||||
|
||||
const SAMPLE_RATE: u32 = 48_000;
|
||||
const PACKET_SAMPLES: u32 = 960;
|
||||
|
||||
type Buffer = Arc<Mutex<dasp_ring_buffer::Bounded<Vec<i16>>>>;
|
||||
|
||||
impl AudioSystem {
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
// TODO
|
||||
let host = cpal::default_host();
|
||||
let name = host.id();
|
||||
let processors = AudioProcessorSender::default();
|
||||
Ok(AudioSystem {
|
||||
output: host
|
||||
.default_output_device()
|
||||
.ok_or(eyre!("no output devices from {name:?}"))?,
|
||||
input: host
|
||||
.default_input_device()
|
||||
.ok_or(eyre!("no input devices from {name:?}"))?,
|
||||
processors,
|
||||
recording_stream: None,
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
Ok(ClientConfig {
|
||||
proxy_url: None,
|
||||
cert_hash: None,
|
||||
any_server: true,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_processor(&self, processor: AudioProcessor) {
|
||||
self.processors.store(Some(processor))
|
||||
fn load_username() -> Option<String> {
|
||||
let config = load_config_map();
|
||||
config.get("username").cloned()
|
||||
}
|
||||
|
||||
fn choose_config(
|
||||
&self,
|
||||
configs: impl Iterator<Item = cpal::SupportedStreamConfigRange>,
|
||||
) -> Result<cpal::StreamConfig, Error> {
|
||||
let mut supported_configs: Vec<_> = configs
|
||||
.filter_map(|cfg| cfg.try_with_sample_rate(cpal::SampleRate(SAMPLE_RATE)))
|
||||
.filter(|cfg| cfg.sample_format() == cpal::SampleFormat::I16)
|
||||
.map(|cfg| cpal::StreamConfig {
|
||||
buffer_size: cpal::BufferSize::Fixed(match *cfg.buffer_size() {
|
||||
cpal::SupportedBufferSize::Range { min, max } => 480.clamp(min, max),
|
||||
cpal::SupportedBufferSize::Unknown => 480,
|
||||
}),
|
||||
..cfg.config()
|
||||
})
|
||||
.collect();
|
||||
supported_configs.sort_by(|a, b| {
|
||||
let cpal::BufferSize::Fixed(a_buf) = a.buffer_size else {
|
||||
unreachable!()
|
||||
};
|
||||
let cpal::BufferSize::Fixed(b_buf) = b.buffer_size else {
|
||||
unreachable!()
|
||||
};
|
||||
Ord::cmp(&a.channels, &b.channels).then(Ord::cmp(&a_buf, &b_buf))
|
||||
});
|
||||
supported_configs
|
||||
.get(0)
|
||||
.cloned()
|
||||
.ok_or(eyre!("no supported stream configs"))
|
||||
fn load_server_url() -> Option<String> {
|
||||
let config = load_config_map();
|
||||
config.get("server").cloned()
|
||||
}
|
||||
|
||||
pub fn start_recording(
|
||||
&mut self,
|
||||
mut each: impl FnMut(Vec<u8>) + Send + 'static,
|
||||
fn set_default_username(username: &str) -> Option<()> {
|
||||
let mut config = load_config_map();
|
||||
config.insert("username".to_string(), username.to_string());
|
||||
save_config_map(&config).ok()
|
||||
}
|
||||
|
||||
fn set_default_server(server: &str) -> Option<()> {
|
||||
let mut config = load_config_map();
|
||||
config.insert("server".to_string(), server.to_string());
|
||||
save_config_map(&config).ok()
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
let config = self.choose_config(self.input.supported_input_configs()?)?;
|
||||
info!(
|
||||
"creating recording on {:?} with {:#?}",
|
||||
self.input.name()?,
|
||||
config
|
||||
);
|
||||
let mut encoder =
|
||||
opus::Encoder::new(SAMPLE_RATE, opus::Channels::Mono, opus::Application::Voip)?;
|
||||
let mut current_processor = AudioProcessor::new_plain();
|
||||
let mut output_buffer = Vec::new();
|
||||
let processors = self.processors.clone();
|
||||
let error_callback = move |e: cpal::StreamError| error!("error recording: {e:?}");
|
||||
let data_callback = move |frame: &[f32], _: &cpal::InputCallbackInfo| {
|
||||
if let Some(new_processor) = processors.take() {
|
||||
current_processor = new_processor;
|
||||
}
|
||||
current_processor.process(frame, config.channels as usize, &mut output_buffer);
|
||||
if output_buffer.len() < PACKET_SAMPLES as usize {
|
||||
return;
|
||||
}
|
||||
let remainder = output_buffer.split_off(PACKET_SAMPLES as usize);
|
||||
let frame = replace(&mut output_buffer, remainder);
|
||||
match encoder.encode_vec_float(&frame, frame.len() * 2) {
|
||||
Ok(buf) => {
|
||||
each(buf);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("error encoding {} samples: {e:?}", frame.len());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.input
|
||||
.build_input_stream(&config, data_callback, error_callback, None)
|
||||
{
|
||||
Ok(stream) => {
|
||||
stream.play()?;
|
||||
self.recording_stream = Some(stream);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.recording_stream = None;
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
super::connect::network_connect(address, username, event_rx, gui_config).await
|
||||
}
|
||||
|
||||
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
|
||||
let config = self.choose_config(self.input.supported_input_configs()?)?;
|
||||
info!(
|
||||
"creating player on {:?} with {:#?}",
|
||||
self.output.name().ok(),
|
||||
&config
|
||||
);
|
||||
let buffer = Arc::new(Mutex::new(dasp_ring_buffer::Bounded::from_raw_parts(
|
||||
0,
|
||||
0,
|
||||
vec![
|
||||
0;
|
||||
SAMPLE_RATE as usize/4 // 250ms of buffer
|
||||
],
|
||||
)));
|
||||
let decoder = opus::Decoder::new(SAMPLE_RATE, opus::Channels::Mono)?;
|
||||
let stream = {
|
||||
let buffer = buffer.clone();
|
||||
self.output.build_output_stream(
|
||||
&config,
|
||||
move |frame, _info| {
|
||||
let mut buffer = buffer.lock().unwrap();
|
||||
for x in frame.chunks_mut(config.channels as usize) {
|
||||
match buffer.pop() {
|
||||
Some(y) => {
|
||||
x.fill(y);
|
||||
}
|
||||
None => {
|
||||
x.fill(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
move |err| error!("could not create output stream {err:?}"),
|
||||
None,
|
||||
)?
|
||||
};
|
||||
stream.play()?;
|
||||
Ok(AudioPlayer {
|
||||
decoder,
|
||||
stream,
|
||||
buffer,
|
||||
tmp: vec![0; 2400],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioPlayer {
|
||||
decoder: opus::Decoder,
|
||||
stream: cpal::Stream,
|
||||
buffer: Buffer,
|
||||
tmp: Vec<i16>,
|
||||
}
|
||||
|
||||
impl AudioPlayer {
|
||||
pub fn play_opus(&mut self, payload: &[u8]) {
|
||||
let len = loop {
|
||||
match self.decoder.decode(payload, &mut self.tmp, false) {
|
||||
Ok(l) => break l,
|
||||
Err(e) => {
|
||||
error!("opus decode error {e:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut buffer = self.buffer.lock().unwrap();
|
||||
let mut overrun = 0;
|
||||
for x in &self.tmp[..len] {
|
||||
if let Some(_) = buffer.push(*x) {
|
||||
overrun += 1;
|
||||
}
|
||||
}
|
||||
if overrun > 0 {
|
||||
warn!("playback overrun by {overrun} samples");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoCertificateVerification;
|
||||
|
||||
impl ServerCertVerifier for NoCertificateVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
super::connect::get_status(client).await
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
fn init_logging() {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
fn request_permissions() {
|
||||
// No-op on desktop
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA1,
|
||||
rustls::SignatureScheme::ECDSA_SHA1_Legacy,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
||||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA384,
|
||||
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA512,
|
||||
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA256,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA384,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA512,
|
||||
rustls::SignatureScheme::ED25519,
|
||||
rustls::SignatureScheme::ED448,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ProxyOverides,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
let config = RlsClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||
.with_no_client_auth();
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
|
||||
let addr = format!("{}:{}", address, 64738)
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
let server_tcp = TcpStream::connect(addr).await?;
|
||||
let server_stream = connector
|
||||
//.connect("127.0.0.1".try_into()?, server_tcp)
|
||||
.connect(address.try_into()?, server_tcp)
|
||||
.await?;
|
||||
let (read_server, write_server) = tokio::io::split(server_stream);
|
||||
|
||||
let read_codec = ClientControlCodec::new();
|
||||
let write_codec = ClientControlCodec::new();
|
||||
|
||||
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
||||
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
||||
|
||||
crate::network_loop(username, event_rx, reader, writer).await
|
||||
}
|
||||
|
||||
fn get_config_path() -> std::path::PathBuf {
|
||||
@@ -343,48 +106,3 @@ fn save_config_map(config: &HashMap<String, String>) -> color_eyre::Result<()> {
|
||||
std::fs::write(&config_path, contents)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_default_username(username: &str) -> Option<()> {
|
||||
let mut config = load_config_map();
|
||||
config.insert("username".to_string(), username.to_string());
|
||||
save_config_map(&config).ok()
|
||||
}
|
||||
|
||||
pub fn set_default_server(server: &str) -> Option<()> {
|
||||
let mut config = load_config_map();
|
||||
config.insert("server".to_string(), server.to_string());
|
||||
save_config_map(&config).ok()
|
||||
}
|
||||
|
||||
pub fn load_username() -> Option<String> {
|
||||
let config = load_config_map();
|
||||
config.get("username").cloned()
|
||||
}
|
||||
|
||||
pub fn load_server_url() -> Option<String> {
|
||||
let config = load_config_map();
|
||||
config.get("server").cloned()
|
||||
}
|
||||
|
||||
pub async fn load_proxy_overides() -> color_eyre::Result<Option<ProxyOverides>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
bail!("status not supported on desktop yet")
|
||||
}
|
||||
|
||||
pub fn init_logging() {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
use crate::app::Command;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
||||
pub struct MobilePlatform;
|
||||
|
||||
impl super::PlatformInterface for MobilePlatform {
|
||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
Ok(ClientConfig {
|
||||
proxy_url: None,
|
||||
cert_hash: None,
|
||||
any_server: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_username(_username: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_server(server: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
super::connect::network_connect(address, username, event_rx, gui_config).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
super::connect::get_status(client).await
|
||||
}
|
||||
|
||||
fn init_logging() {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
}
|
||||
|
||||
fn request_permissions() {
|
||||
request_recording_permission();
|
||||
}
|
||||
|
||||
async fn sleep(duration: Duration) {
|
||||
tokio::time::sleep(duration).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn request_recording_permission() {}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn request_recording_permission() {
|
||||
use android_permissions::{PermissionManager, RECORD_AUDIO};
|
||||
use jni::{objects::JObject, JavaVM};
|
||||
|
||||
let ctx = ndk_context::android_context();
|
||||
let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()).unwrap() };
|
||||
let activity = unsafe { JObject::from_raw(ctx.context().cast()) };
|
||||
|
||||
let manager = PermissionManager::create(vm, activity).unwrap();
|
||||
if !manager.check(&RECORD_AUDIO).unwrap() {
|
||||
manager.request(&[&RECORD_AUDIO]).unwrap();
|
||||
}
|
||||
}
|
||||
+161
-6
@@ -1,11 +1,166 @@
|
||||
//! Platform abstraction layer
|
||||
//!
|
||||
//! This module defines traits that each platform (web, desktop, mobile) must implement.
|
||||
//! The traits make the platform boundary explicit and provide compile-time verification.
|
||||
#![allow(async_fn_in_trait)]
|
||||
|
||||
use crate::{app::Command, effects::AudioProcessor};
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
// ============================================================================
|
||||
// Trait Definitions
|
||||
// ============================================================================
|
||||
|
||||
/// Platform-specific audio subsystem for capturing microphone input and creating playback streams.
|
||||
///
|
||||
/// The audio system handles Opus encoding internally - callers receive encoded frames
|
||||
/// ready for network transmission.
|
||||
pub trait AudioSystemInterface: Sized {
|
||||
/// The player type returned by [`create_player`](Self::create_player).
|
||||
type AudioPlayer: AudioPlayerInterface;
|
||||
|
||||
/// Initialize the audio system.
|
||||
async fn new() -> Result<Self, Error>;
|
||||
|
||||
/// Set the processor for the microphone input, mainly noise cancellation settings.
|
||||
fn set_processor(&self, processor: AudioProcessor);
|
||||
|
||||
/// Begin listening to microphone input, calling the `each` function with
|
||||
/// encoded opus frames.
|
||||
fn start_recording(
|
||||
&mut self,
|
||||
each: impl FnMut(Vec<u8>, bool) + Send + 'static,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// Begin playback of an audio stream, returning an object that can be passed opus frames.
|
||||
fn create_player(&mut self) -> Result<Self::AudioPlayer, Error>;
|
||||
}
|
||||
|
||||
/// A handle to an active audio playback stream for a single remote user.
|
||||
///
|
||||
/// Each connected user gets their own `AudioPlayer` instance, which decodes
|
||||
/// incoming Opus frames and outputs PCM audio to the platform's audio device.
|
||||
/// The player manages its own decoder state and output buffer.
|
||||
pub trait AudioPlayerInterface {
|
||||
/// Decode and play an Opus-encoded audio frame.
|
||||
fn play_opus(&mut self, payload: &[u8]);
|
||||
}
|
||||
|
||||
/// This is the main trait that each platform must implement. It combines all
|
||||
/// platform-specific functionality into a single interface, providing compile-time
|
||||
/// verification that all platforms implement the required functionality.
|
||||
pub trait PlatformInterface {
|
||||
type AudioSystem: AudioSystemInterface;
|
||||
|
||||
/// Initialize logging for the platform.
|
||||
fn init_logging();
|
||||
|
||||
/// Request runtime permissions (Android audio recording, etc.).
|
||||
fn request_permissions();
|
||||
|
||||
/// Establish a connection to the Mumble server and run the network loop.
|
||||
fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
) -> impl Future<Output = Result<(), Error>>;
|
||||
|
||||
/// Get server status (user count, version, etc.).
|
||||
fn get_status(
|
||||
client: &reqwest::Client,
|
||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||
|
||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
|
||||
|
||||
/// Load saved username.
|
||||
fn load_username() -> Option<String>;
|
||||
|
||||
/// Load saved server URL.
|
||||
fn load_server_url() -> Option<String>;
|
||||
|
||||
/// Save the default username.
|
||||
fn set_default_username(username: &str) -> Option<()>;
|
||||
|
||||
/// Save the default server URL.
|
||||
fn set_default_server(server: &str) -> Option<()>;
|
||||
|
||||
/// Async sleep for the given duration.
|
||||
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Platform Modules
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod connect;
|
||||
#[cfg(feature = "desktop")]
|
||||
mod desktop;
|
||||
#[cfg(feature = "mobile")]
|
||||
mod mobile;
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod native_audio;
|
||||
mod stub;
|
||||
#[cfg(feature = "web")]
|
||||
mod web;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
mod desktop;
|
||||
// ============================================================================
|
||||
// Platform Type Alias
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(all(feature = "web", not(feature = "desktop")))]
|
||||
pub use web::*;
|
||||
#[cfg(feature = "web")]
|
||||
pub type Platform = web::WebPlatform;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
pub use desktop::*;
|
||||
#[cfg(all(feature = "desktop", not(feature = "web")))]
|
||||
pub type Platform = desktop::DesktopPlatform;
|
||||
|
||||
#[cfg(all(feature = "mobile", not(feature = "web"), not(feature = "desktop")))]
|
||||
pub type Platform = mobile::MobilePlatform;
|
||||
|
||||
#[cfg(all(
|
||||
not(feature = "mobile"),
|
||||
not(feature = "web"),
|
||||
not(feature = "desktop")
|
||||
))]
|
||||
pub type Platform = stub::StubPlatform;
|
||||
|
||||
pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem;
|
||||
pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer;
|
||||
|
||||
// ========================
|
||||
// Platform Async Runtime
|
||||
// ========================
|
||||
|
||||
// Note: these can not be part of the Platform because they differ in Send requiremets
|
||||
#[cfg(all(any(feature = "desktop", feature = "mobile"), not(feature = "web")))]
|
||||
pub use connect::{spawn, SpawnHandle};
|
||||
#[cfg(all(
|
||||
not(feature = "desktop"),
|
||||
not(feature = "mobile"),
|
||||
not(feature = "web")
|
||||
))]
|
||||
pub use stub::{spawn, SpawnHandle};
|
||||
#[cfg(feature = "web")]
|
||||
pub use web::{spawn, SpawnHandle};
|
||||
|
||||
// =======================
|
||||
// Compile-time Assertions
|
||||
// =======================
|
||||
const _: () = {
|
||||
fn assert_platform<T: PlatformInterface>() {}
|
||||
|
||||
// Check each implementation, and prevent warnings that the implementations are unused.
|
||||
#[cfg(feature = "web")]
|
||||
let _ = assert_platform::<web::WebPlatform>;
|
||||
#[cfg(feature = "desktop")]
|
||||
let _ = assert_platform::<desktop::DesktopPlatform>;
|
||||
#[cfg(feature = "mobile")]
|
||||
let _ = assert_platform::<mobile::MobilePlatform>;
|
||||
let _ = assert_platform::<stub::StubPlatform>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
||||
use color_eyre::eyre::{eyre, Error};
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _};
|
||||
use std::mem::replace;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
pub struct NativeAudioSystem {
|
||||
output: cpal::Device,
|
||||
input: cpal::Device,
|
||||
processors: AudioProcessorSender,
|
||||
recording_stream: Option<cpal::Stream>,
|
||||
}
|
||||
|
||||
const SAMPLE_RATE: u32 = 48_000;
|
||||
const PACKET_SAMPLES: u32 = 960;
|
||||
// Divide by 1000 to get samples per ms, then multiply by 60ms for max Opus frame size.
|
||||
const MAX_DECODE_SAMPLES: usize = SAMPLE_RATE as usize / 1000 * 60;
|
||||
|
||||
fn encode_and_send(
|
||||
state: TransmitState,
|
||||
output_buffer: &mut Vec<f32>,
|
||||
encoder: &mut opus::Encoder,
|
||||
each: &mut impl FnMut(Vec<u8>, bool),
|
||||
) {
|
||||
let (is_terminator, should_encode) = match state {
|
||||
TransmitState::Silent => return,
|
||||
TransmitState::Transmitting => (false, output_buffer.len() >= PACKET_SAMPLES as usize),
|
||||
TransmitState::Terminator => {
|
||||
output_buffer.resize(PACKET_SAMPLES as usize, 0.0);
|
||||
(true, true)
|
||||
}
|
||||
};
|
||||
|
||||
if should_encode {
|
||||
let remainder = output_buffer.split_off(PACKET_SAMPLES as usize);
|
||||
let frame = replace(output_buffer, remainder);
|
||||
match encoder.encode_vec_float(&frame, frame.len() * 2) {
|
||||
Ok(encoded) => each(encoded, is_terminator),
|
||||
Err(e) => error!("error encoding {} samples: {e:?}", frame.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Buffer = Arc<Mutex<dasp_ring_buffer::Bounded<Vec<i16>>>>;
|
||||
|
||||
impl NativeAudioSystem {
|
||||
fn choose_config(
|
||||
&self,
|
||||
configs: impl Iterator<Item = cpal::SupportedStreamConfigRange>,
|
||||
) -> Result<cpal::StreamConfig, Error> {
|
||||
let mut supported_configs: Vec<_> = configs
|
||||
.filter_map(|cfg| cfg.try_with_sample_rate(cpal::SampleRate(SAMPLE_RATE)))
|
||||
.filter(|cfg| cfg.sample_format() == cpal::SampleFormat::I16)
|
||||
.map(|cfg| cpal::StreamConfig {
|
||||
buffer_size: cpal::BufferSize::Fixed(match *cfg.buffer_size() {
|
||||
cpal::SupportedBufferSize::Range { min, max } => 480.clamp(min, max),
|
||||
cpal::SupportedBufferSize::Unknown => 480,
|
||||
}),
|
||||
..cfg.config()
|
||||
})
|
||||
.collect();
|
||||
supported_configs.sort_by(|a, b| {
|
||||
let cpal::BufferSize::Fixed(a_buf) = a.buffer_size else {
|
||||
unreachable!()
|
||||
};
|
||||
let cpal::BufferSize::Fixed(b_buf) = b.buffer_size else {
|
||||
unreachable!()
|
||||
};
|
||||
Ord::cmp(&a.channels, &b.channels).then(Ord::cmp(&a_buf, &b_buf))
|
||||
});
|
||||
supported_configs
|
||||
.get(0)
|
||||
.cloned()
|
||||
.ok_or(eyre!("no supported stream configs"))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::AudioSystemInterface for NativeAudioSystem {
|
||||
type AudioPlayer = NativeAudioPlayer;
|
||||
|
||||
async fn new() -> Result<Self, Error> {
|
||||
let host = cpal::default_host();
|
||||
let name = host.id();
|
||||
let processors = AudioProcessorSender::default();
|
||||
Ok(NativeAudioSystem {
|
||||
output: host
|
||||
.default_output_device()
|
||||
.ok_or(eyre!("no output devices from {name:?}"))?,
|
||||
input: host
|
||||
.default_input_device()
|
||||
.ok_or(eyre!("no input devices from {name:?}"))?,
|
||||
processors,
|
||||
recording_stream: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_processor(&self, processor: AudioProcessor) {
|
||||
self.processors.store(Some(processor))
|
||||
}
|
||||
|
||||
fn start_recording(
|
||||
&mut self,
|
||||
mut each: impl FnMut(Vec<u8>, bool) + Send + 'static,
|
||||
) -> Result<(), Error> {
|
||||
let config = self.choose_config(self.input.supported_input_configs()?)?;
|
||||
info!(
|
||||
"creating recording on {:?} with {:#?}",
|
||||
self.input.name()?,
|
||||
config
|
||||
);
|
||||
let mut encoder =
|
||||
opus::Encoder::new(SAMPLE_RATE, opus::Channels::Mono, opus::Application::Voip)?;
|
||||
let mut current_processor = AudioProcessor::new_plain();
|
||||
let mut output_buffer = Vec::new();
|
||||
let processors = self.processors.clone();
|
||||
let error_callback = move |e: cpal::StreamError| error!("error recording: {e:?}");
|
||||
let data_callback = move |frame: &[f32], _: &cpal::InputCallbackInfo| {
|
||||
if let Some(new_processor) = processors.take() {
|
||||
current_processor = new_processor;
|
||||
}
|
||||
let state =
|
||||
current_processor.process(frame, config.channels as usize, &mut output_buffer);
|
||||
encode_and_send(state, &mut output_buffer, &mut encoder, &mut each);
|
||||
};
|
||||
|
||||
match self
|
||||
.input
|
||||
.build_input_stream(&config, data_callback, error_callback, None)
|
||||
{
|
||||
Ok(stream) => {
|
||||
stream.play()?;
|
||||
self.recording_stream = Some(stream);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.recording_stream = None;
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_player(&mut self) -> Result<NativeAudioPlayer, Error> {
|
||||
let config = self.choose_config(self.output.supported_output_configs()?)?;
|
||||
info!(
|
||||
"creating player on {:?} with {:#?}",
|
||||
self.output.name().ok(),
|
||||
&config
|
||||
);
|
||||
let buffer = Arc::new(Mutex::new(dasp_ring_buffer::Bounded::from_raw_parts(
|
||||
0,
|
||||
0,
|
||||
vec![
|
||||
0;
|
||||
SAMPLE_RATE as usize/4 // 250ms of buffer
|
||||
],
|
||||
)));
|
||||
let decoder = opus::Decoder::new(SAMPLE_RATE, opus::Channels::Mono)?;
|
||||
let stream = {
|
||||
let buffer = buffer.clone();
|
||||
self.output.build_output_stream(
|
||||
&config,
|
||||
move |frame, _info| {
|
||||
let mut buffer = buffer.lock().unwrap();
|
||||
for x in frame.chunks_mut(config.channels as usize) {
|
||||
match buffer.pop() {
|
||||
Some(y) => {
|
||||
x.fill(y);
|
||||
}
|
||||
None => {
|
||||
x.fill(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
move |err| error!("could not create output stream {err:?}"),
|
||||
None,
|
||||
)?
|
||||
};
|
||||
stream.play()?;
|
||||
Ok(NativeAudioPlayer {
|
||||
decoder,
|
||||
stream,
|
||||
buffer,
|
||||
tmp: vec![0; MAX_DECODE_SAMPLES],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NativeAudioPlayer {
|
||||
decoder: opus::Decoder,
|
||||
stream: cpal::Stream,
|
||||
buffer: Buffer,
|
||||
tmp: Vec<i16>,
|
||||
}
|
||||
|
||||
impl super::AudioPlayerInterface for NativeAudioPlayer {
|
||||
fn play_opus(&mut self, payload: &[u8]) {
|
||||
let len = match self.decoder.decode(payload, &mut self.tmp, false) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("opus decode error {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut buffer = self.buffer.lock().unwrap();
|
||||
let mut overrun = 0;
|
||||
for x in &self.tmp[..len] {
|
||||
if let Some(_) = buffer.push(*x) {
|
||||
overrun += 1;
|
||||
}
|
||||
}
|
||||
if overrun > 0 {
|
||||
warn!("playback overrun by {overrun} samples");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/// Stub implementation of the platform interface, so that we can
|
||||
/// `cargo check` without any --feature flags.
|
||||
use crate::effects::AudioProcessor;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use std::future::Future;
|
||||
|
||||
pub struct StubPlatform;
|
||||
|
||||
impl super::PlatformInterface for StubPlatform {
|
||||
type AudioSystem = StubAudioSystem;
|
||||
|
||||
fn init_logging() {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn request_permissions() {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn network_connect(
|
||||
_address: String,
|
||||
_username: String,
|
||||
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
||||
_gui_config: &ClientConfig,
|
||||
) -> impl Future<Output = Result<(), Error>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
fn get_status(
|
||||
_client: &reqwest::Client,
|
||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn set_default_username(_username: &str) -> Option<()> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn set_default_server(_server: &str) -> Option<()> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StubAudioSystem;
|
||||
|
||||
impl super::AudioSystemInterface for StubAudioSystem {
|
||||
type AudioPlayer = StubAudioPlayer;
|
||||
|
||||
async fn new() -> Result<Self, Error> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn set_processor(&self, _processor: AudioProcessor) {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn start_recording(
|
||||
&mut self,
|
||||
_each: impl FnMut(Vec<u8>, bool) + Send + 'static,
|
||||
) -> Result<(), Error> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn create_player(&mut self) -> Result<Self::AudioPlayer, Error> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StubAudioPlayer;
|
||||
|
||||
impl super::AudioPlayerInterface for StubAudioPlayer {
|
||||
fn play_opus(&mut self, _payload: &[u8]) {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub struct SpawnHandle;
|
||||
|
||||
impl SpawnHandle {
|
||||
#[allow(unused)]
|
||||
pub fn spawn<F>(&self, _future: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn current() -> Self {
|
||||
SpawnHandle
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn spawn<F>(_future: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
+159
-118
@@ -1,14 +1,15 @@
|
||||
use crate::app::Command;
|
||||
use crate::effects::{AudioProcessor, AudioProcessorSender};
|
||||
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
||||
use color_eyre::eyre::{bail, eyre, Error};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dioxus::prelude::*;
|
||||
use futures::{AsyncRead, AsyncWrite};
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use js_sys::Float32Array;
|
||||
use mumble_protocol::control::ClientControlCodec;
|
||||
use mumble_web2_common::{ProxyOverides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use reqwest::Url;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing::{debug, error, info, instrument};
|
||||
@@ -27,7 +28,6 @@ use web_sys::AudioWorkletNode;
|
||||
use web_sys::EncodedAudioChunk;
|
||||
use web_sys::EncodedAudioChunkInit;
|
||||
use web_sys::EncodedAudioChunkType;
|
||||
use web_sys::MediaStream;
|
||||
use web_sys::MediaStreamConstraints;
|
||||
use web_sys::MessageEvent;
|
||||
use web_sys::WebTransport;
|
||||
@@ -37,16 +37,119 @@ use web_sys::WorkletOptions;
|
||||
use web_sys::{console, window};
|
||||
use web_sys::{AudioContext, AudioDataCopyToOptions};
|
||||
|
||||
#[allow(unused)]
|
||||
pub use wasm_bindgen_futures::spawn_local as spawn;
|
||||
|
||||
pub trait ImpRead: AsyncRead + Unpin + 'static {}
|
||||
impl<T: AsyncRead + Unpin + 'static> ImpRead for T {}
|
||||
#[allow(unused)]
|
||||
#[derive(Clone)]
|
||||
pub struct SpawnHandle;
|
||||
|
||||
pub trait ImpWrite: AsyncWrite + Unpin + 'static {}
|
||||
impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {}
|
||||
impl SpawnHandle {
|
||||
pub fn spawn<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
}
|
||||
|
||||
pub async fn sleep(d: Duration) {
|
||||
TimeoutFuture::new(d.as_millis() as u32).await
|
||||
pub fn current() -> Self {
|
||||
SpawnHandle
|
||||
}
|
||||
}
|
||||
|
||||
/// Web platform implementation using WebTransport and Web Audio API.
|
||||
pub struct WebPlatform;
|
||||
|
||||
impl super::PlatformInterface for WebPlatform {
|
||||
type AudioSystem = WebAudioSystem;
|
||||
|
||||
fn init_logging() {
|
||||
// copied from tracing_web example usage
|
||||
|
||||
use tracing_subscriber::fmt::format::Pretty;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_web::{performance_layer, MakeWebConsoleWriter};
|
||||
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false) // Only partially supported across browsers
|
||||
.without_time() // std::time is not available in browsers
|
||||
.with_writer(MakeWebConsoleWriter::new()) // write events to the console
|
||||
.with_filter(LevelFilter::DEBUG);
|
||||
let perf_layer = performance_layer().with_details_from_fields(Pretty::default());
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt_layer)
|
||||
.with(perf_layer)
|
||||
.init();
|
||||
|
||||
info!("logging initiated");
|
||||
}
|
||||
|
||||
fn request_permissions() {
|
||||
// No-op on web
|
||||
}
|
||||
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") {
|
||||
Some(url) => Url::parse(url)?,
|
||||
None => absolute_url("config")?,
|
||||
};
|
||||
info!("loading config from {}", config_url);
|
||||
|
||||
let config = reqwest::get(config_url)
|
||||
.await?
|
||||
.json::<ClientConfig>()
|
||||
.await?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.get_item("username")
|
||||
.ok()?
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_username(username: &str) -> Option<()> {
|
||||
web_sys::window()?
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.set_item("username", username)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn set_default_server(_server: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
network_connect(address, username, event_rx, gui_config).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
Ok(client
|
||||
.get(absolute_url("status")?)
|
||||
.send()
|
||||
.await?
|
||||
.json::<ServerStatus>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn sleep(duration: Duration) {
|
||||
TimeoutFuture::new(duration.as_millis() as u32).await;
|
||||
}
|
||||
}
|
||||
|
||||
trait ResultExt<T> {
|
||||
@@ -71,7 +174,7 @@ impl<T> ResultExt<T> for Result<T, JsError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioSystem {
|
||||
pub struct WebAudioSystem {
|
||||
webctx: AudioContext,
|
||||
processors: AudioProcessorSender,
|
||||
}
|
||||
@@ -102,8 +205,10 @@ async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AudioSystem {
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
impl super::AudioSystemInterface for WebAudioSystem {
|
||||
type AudioPlayer = WebAudioPlayer;
|
||||
|
||||
async fn new() -> Result<Self, Error> {
|
||||
// Create MediaStreams to playback decoded audio
|
||||
// The audio context is used to reproduce audio.
|
||||
let webctx = configure_audio_context();
|
||||
@@ -111,14 +216,14 @@ impl AudioSystem {
|
||||
|
||||
let processors = AudioProcessorSender::default();
|
||||
|
||||
Ok(AudioSystem { webctx, processors })
|
||||
Ok(WebAudioSystem { webctx, processors })
|
||||
}
|
||||
|
||||
pub fn set_processor(&self, processor: AudioProcessor) {
|
||||
fn set_processor(&self, processor: AudioProcessor) {
|
||||
self.processors.store(Some(processor))
|
||||
}
|
||||
|
||||
pub fn start_recording(&mut self, each: impl FnMut(Vec<u8>) + 'static) -> Result<(), Error> {
|
||||
fn start_recording(&mut self, each: impl FnMut(Vec<u8>, bool) + 'static) -> Result<(), Error> {
|
||||
let audio_context_worklet = self.webctx.clone();
|
||||
let processors = self.processors.clone();
|
||||
spawn(async move {
|
||||
@@ -130,7 +235,7 @@ impl AudioSystem {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
|
||||
fn create_player(&mut self) -> Result<WebAudioPlayer, Error> {
|
||||
let sink_node = AudioWorkletNode::new(&self.webctx, "rust_speaker_worklet").ey()?;
|
||||
|
||||
// Connect worklet to destination
|
||||
@@ -183,14 +288,14 @@ impl AudioSystem {
|
||||
decoder_error.forget();
|
||||
output.forget();
|
||||
|
||||
Ok(AudioPlayer(audio_decoder))
|
||||
Ok(WebAudioPlayer(audio_decoder))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioPlayer(AudioDecoder);
|
||||
pub struct WebAudioPlayer(AudioDecoder);
|
||||
|
||||
impl AudioPlayer {
|
||||
pub fn play_opus(&mut self, payload: &[u8]) {
|
||||
impl super::AudioPlayerInterface for WebAudioPlayer {
|
||||
fn play_opus(&mut self, payload: &[u8]) {
|
||||
let js_audio_payload = Uint8Array::from(payload);
|
||||
let _ = self.0.decode(
|
||||
&EncodedAudioChunk::new(&EncodedAudioChunkInit::new(
|
||||
@@ -222,22 +327,26 @@ impl PromiseExt for Promise {
|
||||
}
|
||||
}
|
||||
|
||||
fn process_audio(frame: &JsValue, processor: &mut AudioProcessor) {
|
||||
fn process_audio(frame: &JsValue, processor: &mut AudioProcessor) -> TransmitState {
|
||||
let Ok(samples) = Reflect::get(&frame, &"data".into()) else {
|
||||
return;
|
||||
return TransmitState::Silent;
|
||||
};
|
||||
let Ok(samples) = samples.dyn_into::<Float32Array>() else {
|
||||
return;
|
||||
return TransmitState::Silent;
|
||||
};
|
||||
let input = samples.to_vec();
|
||||
let mut output = Vec::with_capacity(input.len());
|
||||
processor.process(&input, 1, &mut output);
|
||||
samples.copy_from(&output);
|
||||
let state = processor.process(&input, 1, &mut output);
|
||||
if !output.is_empty() {
|
||||
samples.copy_from(&output);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
async fn run_encoder_worklet(
|
||||
audio_context: &AudioContext,
|
||||
mut each: impl FnMut(Vec<u8>) + 'static,
|
||||
mut each: impl FnMut(Vec<u8>, bool) + 'static,
|
||||
processors: AudioProcessorSender,
|
||||
) -> Result<AudioWorkletNode, Error> {
|
||||
let constraints = MediaStreamConstraints::new();
|
||||
@@ -262,12 +371,19 @@ async fn run_encoder_worklet(
|
||||
let encoder_error: Closure<dyn FnMut(JsValue)> =
|
||||
Closure::new(|e| error!("error encoding audio {:?}", e));
|
||||
|
||||
// Shared state to signal terminator between onmessage and output closures
|
||||
// The output closure runs asynchronously after encoding completes
|
||||
let pending_terminator = Arc::new(AtomicCell::new(false));
|
||||
let pending_terminator_output = pending_terminator.clone();
|
||||
|
||||
// This knows what MediaStreamTrackGenerator to use as it closes around it
|
||||
let output: Closure<dyn FnMut(EncodedAudioChunk)> =
|
||||
Closure::new(move |audio_data: EncodedAudioChunk| {
|
||||
let mut array = vec![0u8; audio_data.byte_length() as usize];
|
||||
audio_data.copy_to_with_u8_slice(&mut array);
|
||||
each(array);
|
||||
// Check if this frame was marked as a terminator
|
||||
let is_terminator = pending_terminator_output.swap(false);
|
||||
each(array, is_terminator);
|
||||
});
|
||||
|
||||
let audio_encoder = AudioEncoder::new(&AudioEncoderInit::new(
|
||||
@@ -294,8 +410,19 @@ async fn run_encoder_worklet(
|
||||
}
|
||||
|
||||
let frame = event.data();
|
||||
process_audio(&frame, &mut current_processor);
|
||||
let state = process_audio(&frame, &mut current_processor);
|
||||
|
||||
match state {
|
||||
TransmitState::Silent => {
|
||||
// Don't encode or send anything
|
||||
return;
|
||||
}
|
||||
TransmitState::Transmitting => (), // Normal transmission
|
||||
TransmitState::Terminator => {
|
||||
// Mark this as a terminator before encoding
|
||||
pending_terminator.store(true);
|
||||
}
|
||||
}
|
||||
match AudioData::new(frame.unchecked_ref()) {
|
||||
Ok(data) => {
|
||||
let _ = audio_encoder.encode(&data);
|
||||
@@ -329,7 +456,7 @@ pub async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ProxyOverides,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
@@ -342,8 +469,8 @@ pub async fn network_connect(
|
||||
)
|
||||
.ey()?;
|
||||
|
||||
if !gui_config.cert_hash.is_empty() {
|
||||
let hash = web_sys::js_sys::Uint8Array::from(gui_config.cert_hash.as_slice());
|
||||
if let Some(server_hash) = &gui_config.cert_hash {
|
||||
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
||||
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
||||
}
|
||||
|
||||
@@ -391,94 +518,8 @@ pub async fn network_connect(
|
||||
crate::network_loop(username, event_rx, reader, writer).await
|
||||
}
|
||||
|
||||
pub fn set_default_username(username: &str) -> Option<()> {
|
||||
web_sys::window()?
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.set_item("username", username)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn set_default_server(username: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn load_username() -> Option<String> {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.get_item("username")
|
||||
.ok()?
|
||||
}
|
||||
|
||||
pub fn load_server_url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
||||
let window: web_sys::Window = web_sys::window().expect("no global `window` exists");
|
||||
let location = window.location();
|
||||
Ok(Url::parse(&location.href().ey()?)?.join(path)?)
|
||||
}
|
||||
|
||||
pub async fn load_proxy_overides() -> color_eyre::Result<Option<ProxyOverides>> {
|
||||
let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") {
|
||||
Some(url) => Url::parse(url)?,
|
||||
None => absolute_url("config")?,
|
||||
};
|
||||
info!("loading config from {}", config_url);
|
||||
|
||||
let config = reqwest::get(config_url)
|
||||
.await?
|
||||
.json::<ProxyOverides>()
|
||||
.await?;
|
||||
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
Ok(client
|
||||
.get(absolute_url("status")?)
|
||||
.send()
|
||||
.await?
|
||||
.json::<ServerStatus>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn init_logging() {
|
||||
// copied from tracing_web example usage
|
||||
|
||||
use tracing_subscriber::fmt::format::Pretty;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_web::{performance_layer, MakeWebConsoleWriter};
|
||||
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false) // Only partially supported across browsers
|
||||
.without_time() // std::time is not available in browsers
|
||||
.with_writer(MakeWebConsoleWriter::new()) // write events to the console
|
||||
.with_filter(LevelFilter::DEBUG);
|
||||
let perf_layer = performance_layer().with_details_from_fields(Pretty::default());
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt_layer)
|
||||
.with(perf_layer)
|
||||
.init();
|
||||
|
||||
info!("logging initiated");
|
||||
}
|
||||
|
||||
pub struct SpawnHandle;
|
||||
|
||||
impl SpawnHandle {
|
||||
pub fn current() -> Self {
|
||||
SpawnHandle
|
||||
}
|
||||
|
||||
pub fn spawn<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
spawn(future);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-47
@@ -7,11 +7,12 @@ use asynchronous_codec::FramedWrite;
|
||||
use color_eyre::eyre::{bail, Error};
|
||||
use dioxus::prelude::*;
|
||||
use futures::select;
|
||||
use futures::AsyncRead;
|
||||
use futures::AsyncWrite;
|
||||
use futures::FutureExt as _;
|
||||
use futures::SinkExt as _;
|
||||
use futures::StreamExt as _;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
pub use imp::spawn;
|
||||
use msghtml::process_message_html;
|
||||
use mumble_protocol::control::msgs;
|
||||
use mumble_protocol::control::ControlCodec;
|
||||
@@ -20,17 +21,17 @@ use mumble_protocol::voice::VoicePacket;
|
||||
use mumble_protocol::voice::VoicePacketPayload;
|
||||
use mumble_protocol::Clientbound;
|
||||
use mumble_protocol::Serverbound;
|
||||
use mumble_web2_common::ProxyOverides;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
|
||||
use crate::effects::AudioProcessor;
|
||||
use crate::imp::AudioSystem;
|
||||
use crate::imp::{
|
||||
AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform,
|
||||
PlatformInterface as _,
|
||||
};
|
||||
|
||||
pub mod app;
|
||||
mod effects;
|
||||
@@ -50,7 +51,9 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
|
||||
|
||||
*STATE.server.write() = Default::default();
|
||||
*STATE.status.write() = ConnectionState::Connecting;
|
||||
if let Err(error) = imp::network_connect(address, username, &mut event_rx, &config).await {
|
||||
if let Err(error) =
|
||||
Platform::network_connect(address, username, &mut event_rx, &config).await
|
||||
{
|
||||
error!("could not connect {:?}", error);
|
||||
*STATE.status.write() = ConnectionState::Failed(error.to_string());
|
||||
} else {
|
||||
@@ -59,7 +62,7 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
|
||||
pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
||||
@@ -108,23 +111,23 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
|
||||
break;
|
||||
}
|
||||
|
||||
imp::sleep(Duration::from_millis(3000)).await;
|
||||
Platform::sleep(Duration::from_millis(3000)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut audio = imp::AudioSystem::new().await?;
|
||||
let mut audio = AudioSystem::new().await?;
|
||||
{
|
||||
let send_chan = send_chan.clone();
|
||||
let mut sequence_num = 0;
|
||||
audio.start_recording(move |opus_frame| {
|
||||
audio.start_recording(move |opus_frame, is_terminator| {
|
||||
let _ =
|
||||
send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
|
||||
_dst: std::marker::PhantomData,
|
||||
target: 0,
|
||||
session_id: (),
|
||||
seq_num: sequence_num,
|
||||
payload: VoicePacketPayload::Opus(opus_frame.into(), false),
|
||||
payload: VoicePacketPayload::Opus(opus_frame.into(), is_terminator),
|
||||
position_info: None,
|
||||
})));
|
||||
sequence_num = sequence_num.wrapping_add(2);
|
||||
@@ -299,8 +302,8 @@ fn accept_command(
|
||||
|
||||
fn accept_packet(
|
||||
msg: ControlPacket<mumble_protocol::Clientbound>,
|
||||
audio_context: &mut imp::AudioSystem,
|
||||
player_map: &mut HashMap<u32, imp::AudioPlayer>,
|
||||
audio_context: &mut AudioSystem,
|
||||
player_map: &mut HashMap<u32, AudioPlayer>,
|
||||
) -> Result<(), Error> {
|
||||
match msg {
|
||||
ControlPacket::UDPTunnel(u) => {
|
||||
@@ -338,41 +341,11 @@ fn accept_packet(
|
||||
}
|
||||
ControlPacket::ChannelState(u) => {
|
||||
let mut server = STATE.server.write();
|
||||
let id = u.get_channel_id();
|
||||
|
||||
let state = server.channels.entry(id).or_default();
|
||||
let new_parent = if u.has_parent() {
|
||||
if let Some(parent) = state.parent.and_then(|p| server.channels.get_mut(&p)) {
|
||||
parent.children.remove(&id);
|
||||
}
|
||||
|
||||
let parent_id = u.get_parent();
|
||||
let parent = server.channels.entry(parent_id).or_default();
|
||||
if u.has_position() && u.get_position() as usize <= parent.children.len() {
|
||||
// TODO: what if positions are received out of order? we need to sort afterwards?
|
||||
parent.children.insert_before(u.get_position() as usize, id);
|
||||
} else {
|
||||
parent.children.insert(id);
|
||||
}
|
||||
Some(parent_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let state = server.channels.entry(id).or_default();
|
||||
state.parent = new_parent;
|
||||
if u.has_name() {
|
||||
state.name = u.get_name().to_string();
|
||||
}
|
||||
server.channels_state.update_from_channel_state(&u);
|
||||
}
|
||||
ControlPacket::ChannelRemove(u) => {
|
||||
let mut server = STATE.server.write();
|
||||
let id = u.get_channel_id();
|
||||
if let Some(channel) = server.channels.remove(&id) {
|
||||
if let Some(parent) = channel.parent.and_then(|p| server.channels.get_mut(&p)) {
|
||||
parent.children.remove(&id);
|
||||
}
|
||||
}
|
||||
server.channels_state.update_from_channel_remove(&u);
|
||||
}
|
||||
ControlPacket::UserState(u) => {
|
||||
let mut server = STATE.server.write();
|
||||
@@ -384,12 +357,13 @@ fn accept_packet(
|
||||
let state = state_entry.or_default();
|
||||
// the server might now send a channel_id if the user is in channel=0
|
||||
if u.has_channel_id() || new {
|
||||
if let Some(parent) = server.channels.get_mut(&state.channel) {
|
||||
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||
parent.users.remove(&id);
|
||||
}
|
||||
|
||||
let channel_id = u.get_channel_id();
|
||||
server
|
||||
.channels_state
|
||||
.channels
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
@@ -407,6 +381,9 @@ fn accept_packet(
|
||||
if u.has_deaf() {
|
||||
state.deaf = u.get_deaf();
|
||||
}
|
||||
if u.has_suppress() {
|
||||
state.suppress = u.get_suppress();
|
||||
}
|
||||
if u.has_self_mute() {
|
||||
state.self_mute = u.get_self_mute();
|
||||
}
|
||||
@@ -418,7 +395,7 @@ fn accept_packet(
|
||||
let mut server = STATE.server.write();
|
||||
let id = u.get_session();
|
||||
if let Some(state) = server.users.remove(&id) {
|
||||
if let Some(parent) = server.channels.get_mut(&state.channel) {
|
||||
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||
parent.users.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
use mumble_web2_gui::{app, imp};
|
||||
use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
|
||||
|
||||
pub fn main() {
|
||||
imp::init_logging();
|
||||
Platform::init_logging();
|
||||
dioxus::launch(app::app);
|
||||
}
|
||||
|
||||
+17
-23
@@ -1,5 +1,5 @@
|
||||
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
||||
use mumble_web2_common::{ProxyOverides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use rand::Rng;
|
||||
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
||||
use salvo::cors::{AllowOrigin, Cors};
|
||||
@@ -77,13 +77,13 @@ async fn main() -> Result<()> {
|
||||
.install_default()
|
||||
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
|
||||
|
||||
let mut client_config = ProxyOverides {
|
||||
proxy_url: server_config
|
||||
.proxy_url
|
||||
.as_ref()
|
||||
.map(|url| url.to_string())
|
||||
.unwrap_or_default(),
|
||||
cert_hash: Vec::new(),
|
||||
let mut client_config = ClientConfig {
|
||||
proxy_url: match &server_config.proxy_url {
|
||||
Some(url) => Some(url.to_string()),
|
||||
None => None,
|
||||
},
|
||||
cert_hash: None,
|
||||
any_server: false,
|
||||
};
|
||||
|
||||
let (cert, key) = match (&server_config.cert_path, &server_config.key_path) {
|
||||
@@ -102,7 +102,7 @@ async fn main() -> Result<()> {
|
||||
let cert = cert_params.self_signed(&key_pair)?;
|
||||
|
||||
let hash = hmac_sha256::Hash::hash(cert.der().as_ref());
|
||||
client_config.cert_hash = hash.into();
|
||||
client_config.cert_hash = Some(hash.into());
|
||||
|
||||
(cert.pem().into(), key_pair.serialize_pem().into())
|
||||
}
|
||||
@@ -252,13 +252,13 @@ impl StatusCraft {
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigCraft {
|
||||
server_config: Arc<Config>,
|
||||
client_config: ProxyOverides,
|
||||
client_config: ClientConfig,
|
||||
}
|
||||
|
||||
#[craft]
|
||||
impl ConfigCraft {
|
||||
#[craft(handler)]
|
||||
async fn get_config(&self) -> Json<ProxyOverides> {
|
||||
async fn get_config(&self) -> Json<ClientConfig> {
|
||||
Json(self.client_config.clone())
|
||||
}
|
||||
|
||||
@@ -335,19 +335,13 @@ async fn connect_proxy_impl(
|
||||
|
||||
info!("connected to Mumble server");
|
||||
|
||||
// Spawn tasks to handle transmitting data between the WebTransport client and Mumble TCP Server
|
||||
let c2s = tokio::spawn(
|
||||
pass_bytes_loop(incoming, write_server)
|
||||
.instrument(info_span!("Handler", "Client to server")),
|
||||
);
|
||||
let s2c = tokio::spawn(
|
||||
pass_bytes_loop(read_server, outgoing)
|
||||
.instrument(info_span!("Handler", "Server to client")),
|
||||
);
|
||||
|
||||
// Handle transmitting data between the WebTransport client and Mumble TCP Server
|
||||
// When one direction completes/fails, the other is dropped and its streams are closed
|
||||
tokio::select! {
|
||||
res = c2s => res??,
|
||||
res = s2c => res??,
|
||||
res = pass_bytes_loop(incoming, write_server)
|
||||
.instrument(info_span!("Handler", "Client to server")) => res?,
|
||||
res = pass_bytes_loop(read_server, outgoing)
|
||||
.instrument(info_span!("Handler", "Server to client")) => res?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user