diff --git a/Cargo.lock b/Cargo.lock index bd009d0..76be2d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,28 +250,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "async-trait" version = "0.1.88" @@ -314,9 +292,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd9b83179adf8998576317ce47785948bcff399ec5b15f4dfbdedd44ddf5b92" +checksum = "c0baa720ebadea158c5bda642ac444a2af0cdf7bb66b46d1e4533de5d1f449d0" dependencies = [ "aws-credential-types", "aws-runtime", @@ -405,9 +383,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.98.0" +version = "1.100.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029e89cae7e628553643aecb3a3f054a0a0912ff0fd1f5d6a0b4fda421dce64b" +checksum = "8c5eafbdcd898114b839ba68ac628e31c4cfc3e11dfca38dc1b2de2f35bb6270" dependencies = [ "aws-credential-types", "aws-runtime", @@ -439,9 +417,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.76.0" +version = "1.78.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bf26698dd6d238ef1486bdda46f22a589dc813368ba868dc3d94c8d27b56ba" +checksum = "dbd7bc4bd34303733bded362c4c997a39130eac4310257c79aae8484b1c4b724" dependencies = [ "aws-credential-types", "aws-runtime", @@ -461,9 +439,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.77.0" +version = "1.79.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cd07ed1edd939fae854a22054299ae3576500f4e0fadc560ca44f9c6ea1664" +checksum = "77358d25f781bb106c1a69531231d4fd12c6be904edb0c47198c604df5a2dbca" dependencies = [ "aws-credential-types", "aws-runtime", @@ -483,9 +461,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.78.0" +version = "1.80.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37f7766d2344f56d10d12f3c32993da36d78217f32594fe4fb8e57a538c1cdea" +checksum = "06e3ed2a9b828ae7763ddaed41d51724d2661a50c45f845b08967e52f4939cfc" dependencies = [ "aws-credential-types", "aws-runtime", @@ -565,9 +543,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.9" +version = "0.60.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338a3642c399c0a5d157648426110e199ca7fd1c689cc395676b81aa563700c4" +checksum = "604c7aec361252b8f1c871a7641d5e0ba3a7f5a586e51b66bc9510a5519594d9" dependencies = [ "aws-smithy-types", "bytes", @@ -576,9 +554,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99335bec6cdc50a346fda1437f9fefe33abf8c99060739a546a16457f2862ca9" +checksum = "43c82ba4cab184ea61f6edaafc1072aad3c2a17dcf4c0fce19ac5694b90d8b5f" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -616,11 +594,11 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tower 0.5.2", + "tower", "tracing", ] @@ -654,9 +632,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3aaec682eb189e43c8a19c3dab2fe54590ad5f2cc2d26ab27608a20f2acf81c" +checksum = "660f70d9d8af6876b4c9aa8dcb0dbaf0f89b04ee9a4455bea1b4ba03b15f26f6" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -678,9 +656,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.3" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852b9226cb60b78ce9369022c0df678af1cac231c882d5da97a0c4e03be6e67" +checksum = "937a49ecf061895fca4a6dd8e864208ed9be7546c0527d04bc07d502ec5fba1c" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -730,9 +708,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.7" +version = "1.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a322fec39e4df22777ed3ad8ea868ac2f94cd15e1a55f6ee8d8d6305057689a" +checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -742,47 +720,20 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.2", + "axum-core", "bytes", "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -790,27 +741,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", + "tower", "tower-layer", "tower-service", ] @@ -1409,19 +1340,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1431,16 +1349,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -1450,15 +1358,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1725,11 +1624,11 @@ dependencies = [ "etcd-client", "futures-util", "prost", - "redis 0.32.4", + "redis", "serde", "serde_json", "tokio", - "tonic 0.13.1", + "tonic", "tracing", "waterbus-proto", ] @@ -1886,8 +1785,7 @@ dependencies = [ [[package]] name = "engineioxide" version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6d2ef56d20b87b7f02bc08935e21bd7cfd88da6b82b129d5d89a28ddf3a389" +source = "git+https://github.com/Totodore/socketioxide.git?rev=ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963#ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963" dependencies = [ "base64 0.22.1", "bytes", @@ -1913,8 +1811,7 @@ dependencies = [ [[package]] name = "engineioxide-core" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e5d58eb7374df380cbb53ef65f9c35f544c9c217528adb1458c8df05978475" +source = "git+https://github.com/Totodore/socketioxide.git?rev=ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963#ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963" dependencies = [ "base64 0.22.1", "bytes", @@ -1983,17 +1880,17 @@ dependencies = [ [[package]] name = "etcd-client" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f998f8294bc5e7d4f8ea31bd08a1e4a69f94e79d88bc289ea48e9eb7c33b39" +checksum = "88365f1a5671eb2f7fc240adb216786bc6494b38ce15f1d26ad6eaa303d5e822" dependencies = [ "http 1.3.1", "prost", "tokio", "tokio-stream", - "tonic 0.12.3", - "tonic-build 0.12.3", - "tower 0.4.13", + "tonic", + "tonic-build", + "tower", "tower-service", ] @@ -2078,6 +1975,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -3297,7 +3209,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "log", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -3319,24 +3231,46 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", - "socket2 0.5.9", + "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -3554,6 +3488,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3650,6 +3594,16 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "kanal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3953adf0cd667798b396c2fa13552d6d9b3269d7dd1154c4c416442d1ff574" +dependencies = [ + "futures-core", + "lock_api", +] + [[package]] name = "kstring" version = "2.0.2" @@ -3786,12 +3740,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -4005,7 +3953,7 @@ dependencies = [ "rcgen", "reqwest", "ring", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", "time", @@ -4088,6 +4036,23 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.26.4" @@ -4291,12 +4256,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-operations" version = "0.5.0" @@ -4722,7 +4725,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.28", + "rustls 0.23.30", "socket2 0.5.9", "thiserror 2.0.12", "tokio", @@ -4742,7 +4745,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-pki-types", "rustls-platform-verifier", "slab", @@ -4873,11 +4876,10 @@ dependencies = [ [[package]] name = "redis" -version = "0.30.0" +version = "0.32.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "438a4e5f8e9aa246d6f3666d6978441bf1b37d5f417b50c4dd220be09f5fcc17" +checksum = "e1f66bf4cac9733a23bcdf1e0e01effbaaad208567beba68be8f67e5f4af3ee1" dependencies = [ - "arc-swap", "bytes", "cfg-if", "combine", @@ -4886,32 +4888,17 @@ dependencies = [ "futures-util", "itoa", "log", + "native-tls", "num-bigint", "percent-encoding", "pin-project-lite", "rand 0.9.2", "ryu", - "socket2 0.5.9", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redis" -version = "0.32.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f66bf4cac9733a23bcdf1e0e01effbaaad208567beba68be8f67e5f4af3ee1" -dependencies = [ - "combine", - "crc16", - "itoa", - "num-bigint", - "percent-encoding", - "rand 0.9.2", - "ryu", "sha1_smol", "socket2 0.6.0", + "tokio", + "tokio-native-tls", + "tokio-util", "url", ] @@ -4985,9 +4972,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", @@ -5000,35 +4987,34 @@ dependencies = [ "http-body-util", "hyper 1.6.0", "hyper-rustls 0.27.5", + "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", - "once_cell", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.28", - "rustls-pemfile 2.2.0", + "rustls 0.23.30", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.2", "tokio-util", - "tower 0.5.2", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.11", - "windows-registry", + "webpki-roots 1.0.0", ] [[package]] @@ -5269,16 +5255,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "069a8df149a16b1a12dcc31497c3396a173844be3cac4bd40c9e7671fef96671" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] @@ -5346,10 +5332,10 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-native-certs 0.8.1", "rustls-platform-verifier-android", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.4", "security-framework 3.2.0", "security-framework-sys", "webpki-root-certs", @@ -5374,9 +5360,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -5657,7 +5643,7 @@ dependencies = [ "serde_json", "tokio", "tokio-tungstenite 0.27.0", - "tower 0.5.2", + "tower", "tracing", "ulid", ] @@ -5870,6 +5856,17 @@ dependencies = [ "unsafe-libyaml-norway", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -5932,12 +5929,12 @@ dependencies = [ "nanoid", "parking_lot", "prost", - "rustls 0.23.28", + "rustls 0.23.30", "serde", "serde_json", "sysinfo", "tokio", - "tonic 0.13.1", + "tonic", "tracing", "tracing-subscriber", "waterbus-proto", @@ -6013,17 +6010,19 @@ dependencies = [ "jsonwebtoken", "nanoid", "rand 0.9.2", + "reqwest", "rust-embed", - "rustls 0.23.28", + "rustls 0.23.30", "salvo", "serde", "serde_json", + "serde_repr", "socketioxide", "socketioxide-redis", "thiserror 2.0.12", "time", "tokio", - "tower 0.5.2", + "tower", "tower-http", "tracing", "tracing-subscriber", @@ -6119,9 +6118,8 @@ dependencies = [ [[package]] name = "socketioxide" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476190583b592f1e3d55584269600f2d3c4f18af36adad03c41c27e82dcb6bd5" +version = "0.17.1" +source = "git+https://github.com/Totodore/socketioxide.git?rev=ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963#ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963" dependencies = [ "bytes", "engineioxide", @@ -6130,7 +6128,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", - "matchit 0.8.4", + "matchit", "pin-project-lite", "serde", "socketioxide-core", @@ -6146,8 +6144,7 @@ dependencies = [ [[package]] name = "socketioxide-core" version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b07b95089a961994921d23dd6e70792a06f5daa250b5ec8919f6f9de371d2cc5" +source = "git+https://github.com/Totodore/socketioxide.git?rev=ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963#ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963" dependencies = [ "arbitrary", "bytes", @@ -6161,8 +6158,7 @@ dependencies = [ [[package]] name = "socketioxide-parser-common" version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe3b57122bf9c17fe8c2f364e1d307983068396cfb1b0407ec897de411f8033" +source = "git+https://github.com/Totodore/socketioxide.git?rev=ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963#ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963" dependencies = [ "bytes", "itoa", @@ -6174,8 +6170,7 @@ dependencies = [ [[package]] name = "socketioxide-parser-msgpack" version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d815cc0ea52f2026d16435c3d075d2234d26bcff53ae381031870bbb6a3611ae" +source = "git+https://github.com/Totodore/socketioxide.git?rev=ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963#ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963" dependencies = [ "bytes", "rmp", @@ -6187,14 +6182,13 @@ dependencies = [ [[package]] name = "socketioxide-redis" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "997d1017a354c2ed1edf013ac330d8595a2bb2baf0ece9efa782e9182e08dd1c" +source = "git+https://github.com/Totodore/socketioxide.git?rev=ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963#ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963" dependencies = [ "bytes", "futures-core", "futures-util", "pin-project-lite", - "redis 0.30.0", + "redis", "rmp", "rmp-serde", "serde", @@ -6542,9 +6536,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" dependencies = [ "backtrace", "bytes", @@ -6555,9 +6549,9 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.5.9", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6571,6 +6565,16 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -6587,7 +6591,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.28", + "rustls 0.23.30", "tokio", ] @@ -6673,36 +6677,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.9", - "base64 0.22.1", - "bytes", - "h2 0.4.10", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost", - "socket2 0.5.9", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic" version = "0.13.1" @@ -6710,7 +6684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", - "axum 0.8.4", + "axum", "base64 0.22.1", "bytes", "h2 0.4.10", @@ -6726,26 +6700,12 @@ dependencies = [ "socket2 0.5.9", "tokio", "tokio-stream", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "tonic-build" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn 2.0.104", -] - [[package]] name = "tonic-build" version = "0.13.1" @@ -6760,26 +6720,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -6815,12 +6755,14 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", + "iri-string", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tokio", "tokio-util", + "tower", "tower-layer", "tower-service", "tracing", @@ -7254,8 +7196,8 @@ name = "waterbus-proto" version = "0.1.0" dependencies = [ "prost", - "tonic 0.13.1", - "tonic-build 0.13.1", + "tonic", + "tonic-build", ] [[package]] @@ -7328,7 +7270,7 @@ dependencies = [ "log", "quinn", "quinn-proto", - "rustls 0.23.28", + "rustls 0.23.30", "rustls-native-certs 0.8.1", "thiserror 2.0.12", "tokio", @@ -7410,7 +7352,7 @@ dependencies = [ "ring", "rtcp", "rtp", - "rustls 0.23.28", + "rustls 0.23.30", "sdp", "serde", "serde_json", @@ -7472,7 +7414,7 @@ dependencies = [ "rand_core 0.6.4", "rcgen", "ring", - "rustls 0.23.28", + "rustls 0.23.30", "sec1 0.7.3", "serde", "sha1", @@ -7516,15 +7458,16 @@ version = "0.2.0" dependencies = [ "anyhow", "bytes", - "crossbeam", "dashmap", "egress-manager", + "kanal", "mockall", "nanoid", "parking_lot", "rand 0.9.2", "serde", "serde_json", + "serde_repr", "thiserror 2.0.12", "tokio", "tokio-util", @@ -7802,13 +7745,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" dependencies = [ + "windows-link", "windows-result 0.3.3", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-strings 0.4.1", ] [[package]] @@ -7839,15 +7782,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-strings" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index fa8f273..bfc96f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,13 +30,15 @@ salvo = { version = "0.81.0", features = [ "serve-static", ] } serde = "1.0.219" -socketioxide = { version = "0.17.1", features = [ +socketioxide = { git = "https://github.com/Totodore/socketioxide.git", rev = "ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963", features = [ "extensions", "state", "msgpack", ] } -socketioxide-redis = { version = "0.2.2", features = ["redis-cluster"] } -tokio = { version = "1.46.1", features = ["full"] } +socketioxide-redis = { git = "https://github.com/Totodore/socketioxide.git", rev = "ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963", features = [ + "redis-cluster", +] } +tokio = { version = "1.47.0", features = ["full"] } tokio-util = "0.7.15" tower = { version = "0.5.2", default-features = false } tower-http = { version = "0.6.4", features = ["cors", "fs", "auth"] } @@ -44,7 +46,6 @@ tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] } chrono = { version = "0.4.41", features = ["serde"] } diesel = { version = "2.2.12", features = ["postgres", "r2d2", "chrono"] } -# diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } dotenvy = "0.15.7" time = "0.3.41" jsonwebtoken = "9.3.1" @@ -52,13 +53,14 @@ anyhow = "1.0.98" thiserror = "2.0.12" validator = "0.20.0" validator_derive = "0.20.0" -aws-sdk-s3 = { version = "1.98.0", features = ["rt-tokio"] } -aws-config = { version = "1.8.2", features = ["behavior-version-latest"] } +aws-sdk-s3 = { version = "1.100.0", features = ["rt-tokio"] } +aws-config = { version = "1.8.3", features = ["behavior-version-latest"] } aws-credential-types = "1.2.4" -rustls = { version = "0.23.27", features = ["ring"] } +rustls = { version = "0.23.30", features = ["ring"] } nanoid = "0.4.0" rand = "0.9.2" serde_json = "1.0.141" +serde_repr = "0.1.20" bcrypt = "0.17.0" async-channel = "2.5.0" rust-embed = "8.7.2" @@ -88,14 +90,20 @@ moq-gst = { git = "https://github.com/waterbustech/moq-gst.git", branch = "main" gst-plugin-fmp4 = "0.14.0" prost = "0.13.5" tonic = "0.13.1" -etcd-client = "0.15.0" +etcd-client = "0.16.1" sysinfo = "0.36.1" futures-util = "0.3.31" -redis = { version = "0.32.4", features = ["cluster"] } +redis = { version = "0.32.4", features = [ + "cluster", + "tls-native-tls", + "tokio-native-tls-comp", +] } futures = "0.3.31" -crossbeam = "0.8.4" +crossbeam-channel = "0.5.15" mimalloc = "0.1.46" bytes = "1.10.1" +reqwest = { version = "0.12.22", features = ["json"] } +kanal = "0.1.1" # Local crates waterbus-proto = { path = "./crates/waterbus-proto" } diff --git a/crates/dispatcher/Cargo.toml b/crates/dispatcher/Cargo.toml index 42a0ce1..a174ac0 100644 --- a/crates/dispatcher/Cargo.toml +++ b/crates/dispatcher/Cargo.toml @@ -16,3 +16,8 @@ serde_json = { workspace = true } serde = { workspace = true } futures-util = { workspace = true } redis = { workspace = true } + +[features] +default = [] + +redis-cluster = [] diff --git a/crates/dispatcher/src/application/sfu_grpc_client.rs b/crates/dispatcher/src/application/sfu_grpc_client.rs index 59a55bf..589323a 100644 --- a/crates/dispatcher/src/application/sfu_grpc_client.rs +++ b/crates/dispatcher/src/application/sfu_grpc_client.rs @@ -4,7 +4,8 @@ use waterbus_proto::{ LeaveRoomRequest, LeaveRoomResponse, MigratePublisherRequest, MigratePublisherResponse, PublisherRenegotiationRequest, PublisherRenegotiationResponse, SetCameraType, SetEnabledRequest, SetScreenSharingRequest, SetSubscriberSdpRequest, StatusResponse, - SubscribeRequest, SubscribeResponse, sfu_service_client::SfuServiceClient, + SubscribeHlsLiveStreamRequest, SubscribeHlsLiveStreamResponse, SubscribeRequest, + SubscribeResponse, sfu_service_client::SfuServiceClient, }; #[derive(Debug, Clone, Default)] @@ -45,6 +46,21 @@ impl SfuGrpcClient { Ok(response) } + pub async fn subscribe_hls_live_stream( + &self, + server_address: String, + request: SubscribeHlsLiveStreamRequest, + ) -> Result, tonic::Status> { + let mut client = self + .get_client(server_address) + .await + .map_err(|e| Status::unavailable(format!("Failed to connect to SFU: {e}")))?; + let response = client + .subscribe_hls_live_stream(Request::new(request)) + .await?; + Ok(response) + } + pub async fn set_subscriber_sdp( &self, server_address: String, diff --git a/crates/dispatcher/src/dispatcher_manager.rs b/crates/dispatcher/src/dispatcher_manager.rs index 71e0278..1dada82 100644 --- a/crates/dispatcher/src/dispatcher_manager.rs +++ b/crates/dispatcher/src/dispatcher_manager.rs @@ -6,7 +6,8 @@ use waterbus_proto::{ AddPublisherCandidateRequest, AddSubscriberCandidateRequest, JoinRoomRequest, JoinRoomResponse, LeaveRoomRequest, MigratePublisherRequest, MigratePublisherResponse, PublisherRenegotiationRequest, PublisherRenegotiationResponse, SetCameraType, - SetEnabledRequest, SetScreenSharingRequest, SetSubscriberSdpRequest, SubscribeRequest, + SetEnabledRequest, SetScreenSharingRequest, SetSubscriberSdpRequest, + SubscribeHlsLiveStreamRequest, SubscribeHlsLiveStreamResponse, SubscribeRequest, SubscribeResponse, }; @@ -130,6 +131,41 @@ impl DispatcherManager { } } + pub async fn subscribe_hls_live_stream( + &self, + req: SubscribeHlsLiveStreamRequest, + ) -> Result { + let client = self.cache_manager.get_by_participant_id(&req.target_id); + + match client { + Ok(client) => { + if let Some(client) = client { + let node_id = client.sfu_node_id; + let node_addr = client.node_addr; + + let server_addr = format!("{}:{}", node_addr, self.sfu_port); + + let response = self + .sfu_grpc_client + .subscribe_hls_live_stream(server_addr, req) + .await; + + match response { + Ok(resp) => Ok(resp.into_inner()), + Err(e) => Err(anyhow::anyhow!( + "Failed to join room on node {}: {}", + node_id, + e + )), + } + } else { + Err(anyhow::anyhow!("Client not found!")) + } + } + Err(_) => Err(anyhow::anyhow!("Client not found!")), + } + } + pub async fn set_subscribe_sdp( &self, req: SetSubscriberSdpRequest, diff --git a/crates/dispatcher/src/infrastructure/cache/cache_manager.rs b/crates/dispatcher/src/infrastructure/cache/cache_manager.rs index eab8694..589699d 100644 --- a/crates/dispatcher/src/infrastructure/cache/cache_manager.rs +++ b/crates/dispatcher/src/infrastructure/cache/cache_manager.rs @@ -1,7 +1,18 @@ -use redis::{Commands, cluster::ClusterClient}; +use redis::Commands; use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; +#[cfg(not(feature = "redis-cluster"))] +use redis::Client; +#[cfg(feature = "redis-cluster")] +use redis::cluster::ClusterClient; + +#[cfg(feature = "redis-cluster")] +type DefaultClient = ClusterClient; + +#[cfg(not(feature = "redis-cluster"))] +type DefaultClient = Client; + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ClientMetadata { pub room_id: String, @@ -23,12 +34,23 @@ impl CacheKey { #[derive(Clone)] pub struct CacheManager { - client: Arc>, + client: Arc>, } impl CacheManager { pub fn new(urls: Vec) -> Self { - let client = ClusterClient::new(urls).unwrap(); + let client: DefaultClient = { + #[cfg(feature = "redis-cluster")] + { + ClusterClient::new(urls).expect("Failed to create ClusterClient") + } + + #[cfg(not(feature = "redis-cluster"))] + { + Client::open(urls.first().unwrap().as_str()).expect("Failed to create Redis Client") + } + }; + Self { client: Arc::new(Mutex::new(client)), } diff --git a/crates/egress-manager/src/egress/hls_writer.rs b/crates/egress-manager/src/egress/hls_writer.rs index 677381e..b74e554 100644 --- a/crates/egress-manager/src/egress/hls_writer.rs +++ b/crates/egress-manager/src/egress/hls_writer.rs @@ -24,21 +24,22 @@ pub struct HlsWriter { start_time: Instant, video_offset: Arc>, audio_offset: Arc>, + pub hls_url: String, } impl HlsWriter { - pub async fn new(dir: &str, prefix_path: String) -> Result { + pub fn new(dir: &str, prefix_path: &str) -> Result { init()?; let path = PathBuf::from(dir); let pipeline = gst::Pipeline::default(); std::fs::create_dir_all(&path).expect("failed to create directory"); - let r2_config: Option = Self::_get_r2_config(prefix_path); + let r2_config: Option = Self::_get_r2_config(prefix_path.to_string()); - let (r2_storage, master_state) = if let Some(config) = r2_config { + let (r2_storage, master_state, cloud_url_base) = if let Some(config) = r2_config { // Use new_with_worker instead of new - let (r2_storage, upload_receiver) = R2Storage::new_with_worker(config.clone()).await?; + let (r2_storage, upload_receiver) = R2Storage::new_with_worker(config.clone())?; let r2_storage = Arc::new(r2_storage); // Start the upload worker @@ -65,9 +66,9 @@ impl HlsWriter { cloud_url_base.clone(), ))); - (Some(r2_storage), Some(master_state)) + (Some(r2_storage), Some(master_state), cloud_url_base) } else { - (None, None) + (None, None, None) }; let mut manifest_path = path.clone(); @@ -128,6 +129,12 @@ impl HlsWriter { start_time: Instant::now(), video_offset: Arc::new(Mutex::new(0)), audio_offset: Arc::new(Mutex::new(0)), + hls_url: format!( + "{}/{}/{}", + cloud_url_base.unwrap(), + prefix_path, + "manifest.m3u8" + ), }; let hls_writer_arc = Arc::new(this.clone()); @@ -222,7 +229,7 @@ impl HlsWriter { dotenvy::dotenv().ok(); let account_id = env::var("STORAGE_ACCOUNT_ID").ok()?; - let bucket_name = env::var("STORAGE_BUCKET_NAME").ok()?; + let bucket_name = env::var("STORAGE_BUCKET").ok()?; let custom_domain = env::var("STORAGE_CUSTOM_DOMAIN").ok(); let r2_config = R2Config { diff --git a/crates/egress-manager/src/egress/utils/aws_utils.rs b/crates/egress-manager/src/egress/utils/aws_utils.rs index 336b160..f84575e 100644 --- a/crates/egress-manager/src/egress/utils/aws_utils.rs +++ b/crates/egress-manager/src/egress/utils/aws_utils.rs @@ -1,16 +1,18 @@ -use aws_config::meta::region::RegionProviderChain; +use aws_config::{BehaviorVersion, SdkConfig}; use aws_credential_types::Credentials; -use aws_sdk_s3::{Client, config::Region}; +use aws_sdk_s3::{ + Client, + config::{Region, SharedCredentialsProvider}, +}; use std::env; -pub async fn get_storage_object_client() -> Client { +pub fn get_storage_object_client() -> Client { dotenvy::dotenv().ok(); - let access_key_id = env::var("STORAGE_ACCESS_KEY_ID").expect("STORAGE_ACCESS_KEY_ID not set"); - let secret_access_key = - env::var("STORAGE_SECRET_ACCESS_KEY").expect("STORAGE_SECRET_ACCESS_KEY not set"); - let region = env::var("STORAGE_REGION").ok(); - let endpoint_url = env::var("STORAGE_ENDPOINT_URL").ok(); + let access_key_id = env::var("STORAGE_ACCESS_KEY").expect("STORAGE_ACCESS_KEY not set"); + let secret_access_key = env::var("STORAGE_SECRET_KEY").expect("STORAGE_SECRET_KEY not set"); + let region = env::var("STORAGE_REGION").unwrap_or_else(|_| "auto".to_string()); + let endpoint_url = env::var("STORAGE_ENDPOINT").ok(); let credentials = Credentials::new( access_key_id, @@ -20,16 +22,12 @@ pub async fn get_storage_object_client() -> Client { "waterbus_provider", ); - let region_provider = RegionProviderChain::first_try(region.map(Region::new)) - .or_default_provider() - .or_else(Region::new("us-west-2")); - - let shared_config = aws_config::from_env() - .region(region_provider) + let config = SdkConfig::builder() + .behavior_version(BehaviorVersion::latest()) .endpoint_url(endpoint_url.unwrap_or_default()) - .credentials_provider(credentials) - .load() - .await; + .region(Region::new(region)) + .credentials_provider(SharedCredentialsProvider::new(credentials)) + .build(); - Client::new(&shared_config) + Client::new(&config) } diff --git a/crates/egress-manager/src/egress/utils/cloud_upload.rs b/crates/egress-manager/src/egress/utils/cloud_upload.rs index 2f0d210..6077f7f 100644 --- a/crates/egress-manager/src/egress/utils/cloud_upload.rs +++ b/crates/egress-manager/src/egress/utils/cloud_upload.rs @@ -37,8 +37,8 @@ pub struct R2Storage { impl R2Storage { /// Create a new R2Storage instance - pub async fn new(config: R2Config) -> Result { - let client = get_storage_object_client().await; + pub fn new(config: R2Config) -> Result { + let client = get_storage_object_client(); Ok(Self { client, @@ -48,10 +48,10 @@ impl R2Storage { } /// Create a new R2Storage instance with background upload worker - pub async fn new_with_worker( + pub fn new_with_worker( config: R2Config, ) -> Result<(Self, mpsc::UnboundedReceiver)> { - let client = get_storage_object_client().await; + let client = get_storage_object_client(); let (tx, rx) = mpsc::unbounded_channel(); let storage = Self { diff --git a/crates/waterbus-proto/proto/sfu.proto b/crates/waterbus-proto/proto/sfu.proto index 6c04cb3..903310e 100644 --- a/crates/waterbus-proto/proto/sfu.proto +++ b/crates/waterbus-proto/proto/sfu.proto @@ -15,6 +15,8 @@ message JoinRoomRequest { bool isE2eeEnabled = 7; int32 totalTracks = 8; int32 connectionType = 9; + int32 streamingProtocol = 10; + bool isIpv6Supported = 11; } message SubscribeRequest { @@ -22,6 +24,14 @@ message SubscribeRequest { string targetId = 2; string participantId = 3; string roomId = 4; + bool isIpv6Supported = 5; +} + +message SubscribeHlsLiveStreamRequest { + string clientId = 1; + string targetId = 2; + string roomId = 3; + string participantId = 4; } message SetSubscriberSdpRequest { @@ -92,6 +102,10 @@ message SubscribeResponse { optional string screenTrackId = 9; } +message SubscribeHlsLiveStreamResponse { + repeated string hlsUrls = 1; +} + message PublisherRenegotiationResponse { string sdp = 1; } @@ -112,6 +126,7 @@ message StatusResponse { service SfuService { rpc joinRoom(JoinRoomRequest) returns (JoinRoomResponse) {} rpc subscribe(SubscribeRequest) returns (SubscribeResponse) {} + rpc subscribeHlsLiveStream(SubscribeHlsLiveStreamRequest) returns (SubscribeHlsLiveStreamResponse) {} rpc setSubscriberSdp(SetSubscriberSdpRequest) returns (StatusResponse) {} rpc publisherRenegotiation(PublisherRenegotiationRequest) returns (PublisherRenegotiationResponse) {} rpc addPublisherCandidate(AddPublisherCandidateRequest) returns (StatusResponse) {} diff --git a/crates/webrtc-manager/Cargo.toml b/crates/webrtc-manager/Cargo.toml index eb94e37..5fdecba 100644 --- a/crates/webrtc-manager/Cargo.toml +++ b/crates/webrtc-manager/Cargo.toml @@ -10,6 +10,7 @@ parking_lot = { workspace = true } rand = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +serde_repr = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } @@ -17,7 +18,7 @@ tracing = { workspace = true } nanoid = { workspace = true } webrtc = { workspace = true } egress-manager = { workspace = true } -crossbeam = { workspace = true } +kanal = { workspace = true } bytes = { workspace = true } [dev-dependencies] diff --git a/crates/webrtc-manager/src/entities/forward_track.rs b/crates/webrtc-manager/src/entities/forward_track.rs index c38a459..ff6ff8e 100644 --- a/crates/webrtc-manager/src/entities/forward_track.rs +++ b/crates/webrtc-manager/src/entities/forward_track.rs @@ -3,8 +3,7 @@ use std::sync::{ atomic::{AtomicU8, Ordering}, }; -use crossbeam::channel::{Receiver, TryRecvError}; -use dashmap::DashMap; +use kanal::{AsyncReceiver, ReceiveError, Receiver, Sender}; use tracing::{debug, warn}; use webrtc::{ @@ -19,47 +18,89 @@ use webrtc::{ track::track_local::track_local_static_rtp::TrackLocalStaticRTP, }; -use crate::models::{quality::TrackQuality, rtp_foward_info::RtpForwardInfo}; +use crate::{ + models::{quality::TrackQuality, rtp_foward_info::RtpForwardInfo}, + utils::multicast_sender::MulticastSender, +}; -pub struct ForwardTrack { +/// ForwardTrack is a track that forwards RTP packets to the local track +pub struct ForwardTrack { pub local_track: Arc, pub track_id: String, requested_quality: Arc, effective_quality: Arc, + current_subscribed_quality: Arc, ssrc: u32, keyframe_request_callback: Option>, + multicast: Arc, + receiver_id: String, + quality_change_sender: Sender, } -impl ForwardTrack { +impl ForwardTrack { + /// Create a new ForwardTrack + /// + /// # Arguments + /// + /// * `codec` - The codec of the track + /// * `track_id` - The id of the track pub fn new( codec: RTCRtpCodecCapability, track_id: String, sid: String, - receiver: Receiver, forward_track_id: String, ssrc: u32, keyframe_request_callback: Option>, + multicast: Arc, ) -> Arc { + let (quality_change_sender, quality_change_receiver) = kanal::bounded(0); + + // Start with medium quality + let initial_quality = TrackQuality::Medium; + let this = Arc::new(Self { local_track: Arc::new(TrackLocalStaticRTP::new(codec, track_id.clone(), sid)), - track_id: forward_track_id, - requested_quality: Arc::new(AtomicU8::new(TrackQuality::Medium.as_u8())), - effective_quality: Arc::new(AtomicU8::new(TrackQuality::Medium.as_u8())), + track_id: forward_track_id.clone(), + requested_quality: Arc::new(AtomicU8::new(initial_quality.as_u8())), + effective_quality: Arc::new(AtomicU8::new(initial_quality.as_u8())), + current_subscribed_quality: Arc::new(AtomicU8::new(initial_quality.as_u8())), ssrc, keyframe_request_callback, + multicast, + receiver_id: forward_track_id, + quality_change_sender, }); - Self::_receive_rtp(Arc::clone(&this), receiver); + // Subscribe to initial quality layer + let initial_receiver = this + .multicast + .add_receiver_for_quality(initial_quality.clone(), this.receiver_id.clone()); + + Self::dynamic_receive_rtp(Arc::clone(&this), initial_receiver, quality_change_receiver); this } + /// Set the requested quality + /// + /// # Arguments + /// + /// * `quality` - The quality to set + /// + #[inline] pub fn set_requested_quality(&self, quality: &TrackQuality) { let current = TrackQuality::from_u8(self.requested_quality.load(Ordering::Relaxed)); if *quality != current { debug!("[quality] change requested quality to: {:?}", quality); self.requested_quality .store(quality.as_u8(), Ordering::SeqCst); + + // Notify quality change + let new_desired = self.get_desired_quality(); + if let Err(_) = self.quality_change_sender.send(new_desired) { + warn!("[quality] failed to send quality change notification"); + } + // Request keyframe on quality switch if let Some(cb) = &self.keyframe_request_callback { cb(self.ssrc); @@ -67,116 +108,152 @@ impl ForwardTrack { } } + /// Set the effective quality + /// + /// # Arguments + /// + /// * `quality` - The quality to set + /// + #[inline] pub fn set_effective_quality(&self, quality: &TrackQuality) { let current = TrackQuality::from_u8(self.effective_quality.load(Ordering::Relaxed)); if *quality != current { debug!("[quality] change effective quality to: {:?}", quality); self.effective_quality .store(quality.as_u8(), Ordering::SeqCst); + + // Notify quality change + let new_desired = self.get_desired_quality(); + if let Err(_) = self.quality_change_sender.send(new_desired) { + warn!("[quality] failed to send quality change notification"); + } } } - fn _receive_rtp(this: Arc, receiver: Receiver) { - tokio::spawn(async move { - // Use blocking receiver in a spawn_blocking to avoid blocking the async runtime - let this_clone = Arc::clone(&this); - tokio::task::spawn_blocking(move || { - // Process packets in batches for better performance - let mut batch = Vec::with_capacity(32); - - loop { - // Try to collect a batch of packets - match receiver.recv() { - Ok(info) => { - batch.push(info); - - // Try to collect more packets without blocking - while batch.len() < 32 { - match receiver.try_recv() { - Ok(info) => batch.push(info), - Err(TryRecvError::Empty) => break, - Err(TryRecvError::Disconnected) => return, - } - } - - // Process the batch - let rt = tokio::runtime::Handle::current(); - rt.block_on(async { - Self::_process_batch(&this_clone, std::mem::take(&mut batch)).await; - }); - } - Err(_) => { - debug!( - "[track] receiver disconnected for track {}", - this_clone.track_id - ); - break; - } - } - } - }) - .await - .unwrap_or_else(|e| { - warn!("[track] spawn_blocking error: {}", e); - }); - }); + /// Get the desired quality (minimum of requested and effective) + #[inline] + pub fn get_desired_quality(&self) -> TrackQuality { + let requested = TrackQuality::from_u8(self.requested_quality.load(Ordering::Relaxed)); + let effective = TrackQuality::from_u8(self.effective_quality.load(Ordering::Relaxed)); + requested.min(effective) } - async fn _process_batch(this: &Arc, batch: Vec) { - for info in batch { - let is_svc = info.is_svc; - let is_simulcast = info.is_simulcast; - let current_quality = info.track_quality.clone(); - let acceptable_map = info.acceptable_map.clone(); + /// Switch the subscription to a new quality channel if needed + /// + /// # Arguments + /// + /// * `new_quality` - The new desired quality + /// * `this` - Arc for ForwardTrack + /// * `receiver_id` - The receiver id + /// * `multicast` - The multicast sender + /// + fn switch_subscription( + this: &Arc, + new_quality: TrackQuality, + ) -> AsyncReceiver { + let old_quality = + TrackQuality::from_u8(this.current_subscribed_quality.load(Ordering::Relaxed)); + if old_quality != new_quality { + // Remove old receiver + this.multicast + .remove_receiver_for_quality(old_quality, &this.receiver_id); + // Add new receiver + let new_receiver = this + .multicast + .add_receiver_for_quality(new_quality, this.receiver_id.clone()); + this.current_subscribed_quality + .store(new_quality.as_u8(), Ordering::SeqCst); + debug!( + "[quality] switched subscription from {:?} to {:?}", + old_quality, new_quality + ); + new_receiver + } else { + // Already subscribed to the correct quality, just return a dummy receiver (will not be used) + this.multicast + .add_receiver_for_quality(new_quality, format!("{}_dummy", this.receiver_id)) + } + } - let desired_quality = this.get_desired_quality(); + /// Dynamic receive RTP with fast, performant channel switching based on desired quality + /// + /// # Arguments + /// + /// * `this` - The ForwardTrack + /// * `initial_receiver` - The initial receiver + /// * `quality_change_receiver` - Receiver for quality change notifications + /// + fn dynamic_receive_rtp( + this: Arc, + mut receiver: AsyncReceiver, + quality_change_receiver: Receiver, + ) { + tokio::spawn(async move { + loop { + match quality_change_receiver.try_recv() { + Ok(Some(new_quality)) => { + let new_receiver = Self::switch_subscription(&this, new_quality); + receiver = new_receiver; + } + Ok(None) => {} + Err(ReceiveError::Closed) | Err(ReceiveError::SendClosed) => { + break; + } + } - if desired_quality == TrackQuality::None { - continue; + match receiver.recv().await { + Ok(info) => { + let local_track = Arc::clone(&this.local_track); + tokio::spawn(async move { + Self::process_packet(local_track, info).await; + }); + } + Err(_) => { + break; + } + } } + }); + } - let is_video = true; - - if !is_svc - && is_video - && !Self::_is_acceptable_track( - &acceptable_map, - current_quality.clone(), - desired_quality.clone(), - is_simulcast, - ) - { - continue; - } + /// Process a batch of RTP packets + /// + /// # Arguments + /// + /// * `this` - The ForwardTrack + /// * `batch` - The batch of RTP packets + /// + /// + async fn process_packet(local_track: Arc, info: RtpForwardInfo) { + let is_svc = info.is_svc; - let should_forward = if is_svc && is_video { + let should_forward = match is_svc { + false => true, + true => { let mut vp9_packet = Vp9Packet::default(); match vp9_packet.depacketize(&info.packet.payload) { - Ok(_) => desired_quality.should_forward_vp9_svc(&vp9_packet), + Ok(_) => info.track_quality.should_forward_vp9_svc(&vp9_packet), Err(err) => { warn!("Failed to depacketize VP9: {}", err); true } } - } else { - true - }; - - if !should_forward { - continue; } + }; - // Write RTP packet - Self::_write_rtp(&this.local_track, &info.packet).await; + if should_forward { + Self::_write_rtp(&local_track, &info.packet).await; } } - pub fn get_desired_quality(&self) -> TrackQuality { - let requested = TrackQuality::from_u8(self.requested_quality.load(Ordering::Relaxed)); - let effective = TrackQuality::from_u8(self.effective_quality.load(Ordering::Relaxed)); - requested.min(effective) - } - + /// Write RTP packet + /// + /// # Arguments + /// + /// * `local_track` - The local track + /// * `rtp` - The RTP packet + /// + #[inline] async fn _write_rtp(local_track: &Arc, rtp: &Packet) { if let Err(err) = local_track .write_rtp_with_extensions( @@ -192,20 +269,4 @@ impl ForwardTrack { } } } - - fn _is_acceptable_track( - acceptable_map: &Arc>, - current: TrackQuality, - desired: TrackQuality, - is_simulcast: bool, - ) -> bool { - if !is_simulcast { - return true; - } - - acceptable_map - .get(&(current, desired)) - .map(|v| *v) - .unwrap_or(false) - } } diff --git a/crates/webrtc-manager/src/entities/media.rs b/crates/webrtc-manager/src/entities/media.rs index 87b7f38..725397d 100644 --- a/crates/webrtc-manager/src/entities/media.rs +++ b/crates/webrtc-manager/src/entities/media.rs @@ -1,4 +1,4 @@ -use std::{fs, path::Path, sync::Arc}; +use std::{collections::HashMap, fs, path::Path, sync::Arc}; use dashmap::DashMap; use egress_manager::egress::{hls_writer::HlsWriter, moq_writer::MoQWriter}; @@ -11,24 +11,26 @@ use webrtc::{rtp_transceiver::rtp_codec::RTPCodecType, track::track_remote::Trac use crate::models::{ data_channel_msg::TrackSubscribedMessage, params::{AddTrackResponse, TrackMutexWrapper}, + streaming_protocol::StreamingProtocol, }; use super::track::Track; pub type TrackSubscribedCallback = Arc; +/// Media is a media that is used to manage the media of the participant pub struct Media { pub media_id: String, pub participant_id: String, pub tracks: Arc>, pub state: Arc>, - hls_writer: Option>, - moq_writer: Option>, - output_dir: String, - sdp: Option, + pub moq_writer: Option>, + pub sdp: Option, + pub hls_writers: Arc>>>, pub track_subscribed_callback: Option, pub track_event_sender: Option>, pub keyframe_request_callback: Option>, + pub streaming_protocol: StreamingProtocol, } #[derive(Debug)] @@ -44,29 +46,24 @@ pub struct MediaState { } impl Media { + /// Create a new Media + /// + /// # Arguments + /// + /// * `publisher_id` - The id of the publisher + /// * `is_video_enabled` - Whether the video is enabled pub fn new( publisher_id: String, is_video_enabled: bool, is_audio_enabled: bool, is_e2ee_enabled: bool, + streaming_protocol: StreamingProtocol, ) -> Self { - let output_dir = format!("./hls/{publisher_id}"); - - if !Path::new(&output_dir).exists() { - fs::create_dir_all(&output_dir).unwrap(); - } - Self { media_id: format!("m_{}", nanoid!(12)), participant_id: publisher_id, tracks: Arc::new(DashMap::new()), - hls_writer: None, - moq_writer: None, - output_dir, - sdp: None, - track_subscribed_callback: None, - track_event_sender: None, - keyframe_request_callback: None, + hls_writers: Arc::new(RwLock::new(HashMap::new())), state: Arc::new(RwLock::new(MediaState { video_enabled: is_video_enabled, audio_enabled: is_audio_enabled, @@ -77,25 +74,45 @@ impl Media { codec: String::new(), screen_track_id: None, })), + track_subscribed_callback: None, + track_event_sender: None, + keyframe_request_callback: None, + moq_writer: None, + sdp: None, + streaming_protocol, } } - pub async fn initialize_hls_writer(&mut self) -> Result<(), Box> { - let hls_writer = HlsWriter::new(&self.output_dir, self.participant_id.clone()).await?; - self.hls_writer = Some(Arc::new(hls_writer)); - Ok(()) - } - + /// Initialize the moq writer + /// + /// # Arguments + /// + /// * `self` - The Media + /// pub fn initialize_moq_writer(&mut self) -> Result<(), anyhow::Error> { let moq_writer = MoQWriter::new(&self.participant_id.clone())?; self.moq_writer = Some(Arc::new(moq_writer)); Ok(()) } + /// Cache the sdp incase peer to peer connection + /// + /// # Arguments + /// + /// * `sdp` - The sdp to cache + /// + #[inline] pub fn cache_sdp(&mut self, sdp: String) { self.sdp = Some(sdp); } + /// Get the sdp + /// + /// # Arguments + /// + /// * `self` - The Media + /// + #[inline] pub fn get_sdp(&mut self) -> Option { let sdp = self.sdp.clone(); @@ -104,23 +121,13 @@ impl Media { sdp } - // Alternative: Static method that creates and initializes everything - pub async fn new_with_hls( - publisher_id: String, - is_video_enabled: bool, - is_audio_enabled: bool, - is_e2ee_enabled: bool, - ) -> Result> { - let mut media = Self::new( - publisher_id, - is_video_enabled, - is_audio_enabled, - is_e2ee_enabled, - ); - media.initialize_hls_writer().await?; - Ok(media) - } - + /// Add a new track to the Media + /// + /// # Arguments + /// + /// * `rtp_track` - The rtp track to add + /// * `room_id` - The id of the room + /// pub fn add_track(&self, rtp_track: Arc, room_id: String) -> AddTrackResponse { if let Some(existing_track_arc) = self.tracks.get(&rtp_track.id()) { let mut track_guard = existing_track_arc.write(); @@ -135,6 +142,8 @@ impl Media { return AddTrackResponse::AddSimulcastTrackSuccess(existing_track_arc.clone()); } + let mut hls_writer = None; + if rtp_track.kind() == RTPCodecType::Video { let codec = match rtp_track .codec() @@ -150,8 +159,8 @@ impl Media { _ => "h264", }; - if let Some(hls_writer) = &self.hls_writer { - hls_writer.set_video_codec(codec); + if self.streaming_protocol == StreamingProtocol::HLS { + hls_writer = self.add_track_to_hls_writer(rtp_track.clone()); } if let Some(moq_writer) = &self.moq_writer { @@ -163,7 +172,7 @@ impl Media { rtp_track.clone(), room_id, self.participant_id.clone(), - self.hls_writer.clone(), + hls_writer, self.moq_writer.clone(), self.keyframe_request_callback.clone(), ))); @@ -180,6 +189,58 @@ impl Media { AddTrackResponse::AddTrackSuccess(new_track) } + /// Add a new track to the hls writer + /// + /// # Arguments + /// + /// * `rtp_track` - The rtp track to add + /// + #[inline] + pub fn add_track_to_hls_writer(&self, rtp_track: Arc) -> Option> { + if self.streaming_protocol == StreamingProtocol::HLS { + let hls_writer = self._initialize_hls_writer(&rtp_track.id()); + + if let Ok(hls_writer) = hls_writer { + hls_writer.set_video_codec(&rtp_track.codec().capability.mime_type); + + return Some(hls_writer); + } + } + + None + } + + /// Initialize the hls writer + /// + /// # Arguments + /// + /// * `track_id` - The id of the track + /// + fn _initialize_hls_writer(&self, track_id: &str) -> Result, anyhow::Error> { + let output_dir = format!("./hls/{}/{}", self.participant_id, track_id); + + if !Path::new(&output_dir).exists() { + fs::create_dir_all(&output_dir).unwrap(); + } + + let prefix_path = format!("{}/{}", self.participant_id, track_id); + + let hls_writer = HlsWriter::new(&output_dir, &prefix_path)?; + let hls_writer_arc = Arc::new(hls_writer); + self.hls_writers + .write() + .insert(self.participant_id.clone(), hls_writer_arc.clone()); + Ok(hls_writer_arc) + } + + /// Set the screen sharing + /// + /// # Arguments + /// + /// * `is_enabled` - Whether the screen sharing is enabled + /// * `screen_track_id` - The id of the screen track + /// + #[inline] pub fn set_screen_sharing(&self, is_enabled: bool, screen_track_id: Option) { let mut state = self.state.write(); if state.is_screen_sharing != is_enabled { @@ -195,11 +256,25 @@ impl Media { } } + /// Set the hand raising + /// + /// # Arguments + /// + /// * `is_enabled` - Whether the hand raising is enabled + /// + #[inline] pub fn set_hand_rasing(&self, is_enabled: bool) { let mut state = self.state.write(); state.is_hand_raising = is_enabled; } + /// Remove the screen track + /// + /// # Arguments + /// + /// * `self` - The Media + /// + #[inline] fn remove_screen_track(&self) { let screen_track_id_opt = { let state = self.state.read(); @@ -223,6 +298,13 @@ impl Media { } } + /// Remove all tracks + /// + /// # Arguments + /// + /// * `self` - The Media + /// + #[inline] pub fn remove_all_tracks(&self) { for entry in self.tracks.iter() { let track_mutex = entry.value().clone(); @@ -234,28 +316,68 @@ impl Media { self.tracks.clear(); } + /// Set the camera type + /// + /// # Arguments + /// + /// * `camera_type` - The camera type + /// + #[inline] pub fn set_camera_type(&self, camera_type: u8) { self.state.write().camera_type = camera_type; } + /// Set the video enabled + /// + /// # Arguments + /// + /// * `is_enabled` - Whether the video is enabled + /// + #[inline] pub fn set_video_enabled(&self, is_enabled: bool) { self.state.write().video_enabled = is_enabled; } + /// Set the audio enabled + /// + /// # Arguments + /// + /// * `is_enabled` - Whether the audio is enabled + /// + #[inline] pub fn set_audio_enabled(&self, is_enabled: bool) { self.state.write().audio_enabled = is_enabled; } + /// Set the e2ee enabled + /// + /// # Arguments + /// + /// * `is_enabled` - Whether the e2ee is enabled + /// + #[inline] pub fn set_e2ee_enabled(&self, is_enabled: bool) { self.state.write().is_e2ee_enabled = is_enabled; } + /// Stop the Media + /// + /// # Arguments + /// + /// * `self` - The Media + /// pub fn stop(&self) { self.remove_all_tracks(); - if let Some(writer) = &self.hls_writer { - writer.stop(); - } + let hls_writers = self.hls_writers.clone(); + tokio::spawn(async move { + let mut hls_writers = hls_writers.write(); + for writer in hls_writers.values() { + writer.stop(); + } + hls_writers.clear(); + }); + if let Some(writer) = &self.moq_writer { writer.stop(); } @@ -272,6 +394,13 @@ impl Media { } } + /// Log the track added + /// + /// # Arguments + /// + /// * `rtp_track` - The rtp track + /// + #[inline] fn _log_track_added(&self, rtp_track: Arc) { let rid = if rtp_track.kind() == RTPCodecType::Audio { "audio" @@ -291,4 +420,19 @@ impl Media { rtp_track.ssrc(), ); } + + /// Get the hls urls + /// + /// # Arguments + /// + /// * `self` - The Media + /// + #[inline] + pub fn get_hls_urls(&self) -> Vec { + self.hls_writers + .read() + .values() + .map(|writer| writer.hls_url.clone()) + .collect() + } } diff --git a/crates/webrtc-manager/src/entities/mod.rs b/crates/webrtc-manager/src/entities/mod.rs index 937a899..d969cb5 100644 --- a/crates/webrtc-manager/src/entities/mod.rs +++ b/crates/webrtc-manager/src/entities/mod.rs @@ -1,5 +1,6 @@ pub mod forward_track; pub mod media; pub mod publisher; +pub mod room; pub mod subscriber; pub mod track; diff --git a/crates/webrtc-manager/src/entities/publisher.rs b/crates/webrtc-manager/src/entities/publisher.rs index 331d248..0592dd2 100644 --- a/crates/webrtc-manager/src/entities/publisher.rs +++ b/crates/webrtc-manager/src/entities/publisher.rs @@ -69,6 +69,7 @@ impl Publisher { publisher } + #[inline] pub fn send_rtcp_pli(&self, media_ssrc: u32) { let pc2 = Arc::downgrade(&self.peer_connection); let cancel = self.cancel_token.clone(); @@ -99,6 +100,7 @@ impl Publisher { }); } + #[inline] pub fn send_rtcp_pli_once(&self, media_ssrc: u32) { let pc2 = Arc::downgrade(&self.peer_connection); @@ -114,15 +116,18 @@ impl Publisher { }); } + #[inline] pub fn set_connection_type(&self, connection_type: ConnectionType) { self.connection_type .store(connection_type.into(), Ordering::Relaxed); } + #[inline] pub fn get_connection_type(&self) -> ConnectionType { self.connection_type.load(Ordering::Relaxed).into() } + #[inline] pub fn close(&self) { let pc = self.peer_connection.clone(); let media = self.media.clone(); diff --git a/crates/webrtc-manager/src/room.rs b/crates/webrtc-manager/src/entities/room.rs similarity index 92% rename from crates/webrtc-manager/src/room.rs rename to crates/webrtc-manager/src/entities/room.rs index 1e07cfc..ecd3678 100644 --- a/crates/webrtc-manager/src/room.rs +++ b/crates/webrtc-manager/src/entities/room.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use dashmap::DashMap; use parking_lot::{Mutex, RwLock}; @@ -25,7 +25,7 @@ use webrtc::{ sdp::session_description::RTCSessionDescription, }, rtp_transceiver::{ - RTCPFeedback, TYPE_RTCP_FB_GOOG_REMB, TYPE_RTCP_FB_NACK, TYPE_RTCP_FB_TRANSPORT_CC, + RTCPFeedback, TYPE_RTCP_FB_CCM, TYPE_RTCP_FB_GOOG_REMB, TYPE_RTCP_FB_TRANSPORT_CC, rtp_codec::{RTCRtpHeaderExtensionCapability, RTPCodecType}, }, }; @@ -36,9 +36,11 @@ use crate::{ models::{ connection_type::ConnectionType, params::{ - AddTrackResponse, IceCandidate, JoinRoomParams, JoinRoomResponse, SubscribeParams, + AddTrackResponse, IceCandidate, JoinRoomParams, JoinRoomResponse, + SubscribeHlsLiveStreamParams, SubscribeHlsLiveStreamResponse, SubscribeParams, SubscribeResponse, TrackMutexWrapper, WebRTCManagerConfigs, }, + streaming_protocol::StreamingProtocol, }, }; @@ -65,13 +67,14 @@ impl Room { ) -> Result, WebRTCError> { let participant_id = params.participant_id; - let pc = self._create_pc().await?; + let pc = self._create_pc(params.is_ipv6_supported).await?; let mut media = Media::new( participant_id.clone(), params.is_video_enabled, params.is_audio_enabled, params.is_e2ee_enabled, + params.streaming_protocol, ); if params.connection_type == ConnectionType::P2P { @@ -280,7 +283,7 @@ impl Room { let peer_id = self._get_subscriber_peer_id(target_id, participant_id); - let pc = self._create_pc().await?; + let pc = self._create_pc(params.is_ipv6_supported).await?; self._add_subscriber(&peer_id, &pc, participant_id.clone()) .await; @@ -355,6 +358,23 @@ impl Room { } } + pub fn subscribe_hls_live_stream( + &self, + params: SubscribeHlsLiveStreamParams, + ) -> Result { + let target_id = ¶ms.target_id; + + let media_arc = self._get_media(target_id)?; + + if media_arc.read().streaming_protocol != StreamingProtocol::HLS { + return Err(WebRTCError::InvalidStreamingProtocol); + } + + let hls_urls = media_arc.read().get_hls_urls(); + + Ok(SubscribeHlsLiveStreamResponse { hls_urls }) + } + pub fn set_subscriber_remote_sdp( &self, target_id: &str, @@ -514,6 +534,7 @@ impl Room { }) } + #[inline] pub fn leave_room(&mut self, participant_id: &str) { self._remove_all_subscribers_with_target_id(participant_id); @@ -522,6 +543,7 @@ impl Room { } } + #[inline] pub fn set_e2ee_enabled( &self, participant_id: &str, @@ -536,6 +558,7 @@ impl Room { Ok(()) } + #[inline] pub fn set_camera_type( &self, participant_id: &str, @@ -550,6 +573,7 @@ impl Room { Ok(()) } + #[inline] pub fn set_video_enabled( &self, participant_id: &str, @@ -564,6 +588,7 @@ impl Room { Ok(()) } + #[inline] pub fn set_audio_enabled( &self, participant_id: &str, @@ -578,6 +603,7 @@ impl Room { Ok(()) } + #[inline] pub fn set_screen_sharing( &self, participant_id: &str, @@ -593,6 +619,7 @@ impl Room { Ok(()) } + #[inline] pub fn set_hand_raising( &self, participant_id: &str, @@ -607,6 +634,7 @@ impl Room { Ok(()) } + #[inline] fn _get_publisher(&self, participant_id: &str) -> Result, WebRTCError> { let result = self .publishers @@ -617,11 +645,13 @@ impl Room { Ok(result) } + #[inline] fn _add_publisher(&self, participant_id: &str, participant: &Arc) { self.publishers .insert(participant_id.to_owned(), participant.clone()); } + #[inline] async fn _add_subscriber(&self, peer_id: &str, pc: &Arc, user_id: String) { let subscriber = Subscriber::new(pc.clone(), user_id).await; let subscriber = Arc::new(subscriber); @@ -629,6 +659,7 @@ impl Room { self.subscribers.insert(peer_id.to_owned(), subscriber); } + #[inline] fn _get_subscriber_peer( &self, target_id: &str, @@ -646,6 +677,7 @@ impl Room { } } + #[inline] fn _get_subscriber( &self, target_id: &str, @@ -663,17 +695,20 @@ impl Room { } } + #[inline] fn _get_subscriber_peer_id(&self, target_id: &str, participant_id: &str) -> String { let key = format!("p_{target_id}_{participant_id}"); key } + #[inline] fn _get_media(&self, participant_id: &str) -> Result>, WebRTCError> { let participant = self._get_publisher(participant_id)?; Ok(Arc::clone(&participant.media)) } + #[inline] fn _remove_all_subscribers_with_target_id(&self, participant_id: &str) { let prefix = format!("p_{participant_id}_"); @@ -694,6 +729,7 @@ impl Room { } } + #[inline] async fn _add_track_to_subscribers( subscribers_lock: Arc>>, remote_track: TrackMutexWrapper, @@ -716,7 +752,10 @@ impl Room { Ok(()) } - pub async fn _create_pc(&self) -> Result, WebRTCError> { + pub async fn _create_pc( + &self, + is_ipv6_supported: bool, + ) -> Result, WebRTCError> { let config = RTCConfiguration { ice_servers: vec![], bundle_policy: RTCBundlePolicy::MaxBundle, @@ -739,8 +778,12 @@ impl Room { parameter: "".to_string(), }, RTCPFeedback { - typ: TYPE_RTCP_FB_NACK.to_owned(), - parameter: "".to_string(), + typ: TYPE_RTCP_FB_CCM.to_owned(), + parameter: "fir".to_string(), + }, + RTCPFeedback { + typ: TYPE_RTCP_FB_CCM.to_owned(), + parameter: "pli".to_string(), }, ]; @@ -767,10 +810,24 @@ impl Room { let mut setting_engine = SettingEngine::default(); setting_engine.set_lite(true); - setting_engine.set_network_types(vec![NetworkType::Udp4]); + setting_engine.set_ice_timeouts( + Some(Duration::from_secs(10)), + Some(Duration::from_secs(25)), + Some(Duration::from_secs(1)), + ); + + let mut network_types = vec![]; + if is_ipv6_supported { + network_types.push(NetworkType::Udp6); + } else { + network_types.push(NetworkType::Udp4); + } + + setting_engine.set_network_types(network_types); setting_engine.set_udp_network(UDPNetwork::Ephemeral( EphemeralUDP::new(self.configs.port_min, self.configs.port_max).unwrap(), )); + if !self.configs.public_ip.is_empty() { setting_engine.set_nat_1to1_ips( vec![self.configs.public_ip.to_owned()], @@ -797,6 +854,7 @@ impl Room { Ok(peer) } + #[inline] async fn _extract_subscribe_response( &self, media_arc: &Arc>, @@ -817,6 +875,7 @@ impl Room { } } + #[inline] async fn _forward_all_tracks( &self, subscriber: Arc, diff --git a/crates/webrtc-manager/src/entities/subscriber.rs b/crates/webrtc-manager/src/entities/subscriber.rs index 631c838..3120b9b 100644 --- a/crates/webrtc-manager/src/entities/subscriber.rs +++ b/crates/webrtc-manager/src/entities/subscriber.rs @@ -32,6 +32,7 @@ use crate::{ params::TrackMutexWrapper, quality::TrackQuality, track_quality_request::TrackQualityRequest, }, + utils::multicast_sender::MulticastSenderImpl, }; use super::forward_track::ForwardTrack; @@ -51,7 +52,7 @@ const MIN_QUALITY_CHANGE_INTERVAL: Duration = Duration::from_secs(2); // History sizes for better stability const HISTORY_SIZE: usize = 10; -type TrackMap = Arc>>; +type TrackMap = Arc>>>; #[derive(Debug)] struct NetworkStats { @@ -592,6 +593,7 @@ impl Subscriber { } // Get current network stats for monitoring + #[inline] pub async fn get_network_stats(&self) -> (TrackQuality, TrackQuality) { let stats = self.network_stats.read().await; let current = TrackQuality::from_u8(self.preferred_quality.load(Ordering::Relaxed)); @@ -599,6 +601,7 @@ impl Subscriber { (current, stats.twcc_quality.clone()) } + #[inline] pub fn close(&self) { self.cancel_token.cancel(); self.clear_all_forward_tracks(); diff --git a/crates/webrtc-manager/src/entities/track.rs b/crates/webrtc-manager/src/entities/track.rs index e7ade48..c0c5523 100644 --- a/crates/webrtc-manager/src/entities/track.rs +++ b/crates/webrtc-manager/src/entities/track.rs @@ -1,3 +1,4 @@ +use bytes::BytesMut; use dashmap::DashMap; use egress_manager::egress::hls_writer::HlsWriter; use egress_manager::egress::moq_writer::MoQWriter; @@ -7,15 +8,16 @@ use std::sync::atomic::{AtomicBool, Ordering}; use tracing::debug; use webrtc::rtp_transceiver::rtp_codec::{RTCRtpCodecCapability, RTPCodecType}; use webrtc::track::track_remote::TrackRemote; +use webrtc::util::Marshal; use crate::errors::WebRTCError; use crate::models::quality::TrackQuality; use crate::models::rtp_foward_info::RtpForwardInfo; -use crate::utils::multicast_sender::MulticastSender; +use crate::utils::multicast_sender::{MulticastSender, MulticastSenderImpl}; use super::forward_track::ForwardTrack; -#[derive(Debug, Clone, PartialEq)] +#[derive(Clone, PartialEq)] pub enum CodecType { H264, VP8, @@ -24,6 +26,8 @@ pub enum CodecType { Other, } +/// Track is a track that is used to forward RTP packets to the local track +/// It is used to forward RTP packets to the local track #[derive(Clone)] pub struct Track { pub id: String, @@ -36,14 +40,20 @@ pub struct Track { pub capability: RTCRtpCodecCapability, pub kind: RTPCodecType, pub remote_tracks: Vec>, - pub forward_tracks: Arc>>, + pub forward_tracks: Arc>>>, pub ssrc: u32, - acceptable_map: Arc>, - rtp_multicast: MulticastSender, + // acceptable_map: Arc>, + rtp_multicast: Arc, keyframe_request_callback: Option>, } impl Track { + /// Create a new Track + /// + /// # Arguments + /// + /// * `track` - The track to create the Track for + /// * `room_id` - The id of the room pub fn new( track: Arc, room_id: String, @@ -66,7 +76,7 @@ impl Track { // Determine if SVC is used based on codec let is_svc = matches!(codec_type, CodecType::VP9); - let rtp_multicast = MulticastSender::new(); + let rtp_multicast = Arc::new(MulticastSenderImpl::new()); let handler = Track { id: track.id(), @@ -80,138 +90,195 @@ impl Track { kind, remote_tracks: vec![track.clone()], forward_tracks: Arc::new(DashMap::new()), - acceptable_map: Arc::new(DashMap::new()), + // acceptable_map: Arc::new(DashMap::new()), ssrc: track.ssrc(), rtp_multicast, keyframe_request_callback: keyframe_request_callback.clone(), }; - handler.rebuild_acceptable_map(); + // handler.rebuild_acceptable_map(); handler._forward_rtp(track, hls_writer, moq_writer, kind); handler } + /// Add a new track to the Track + /// + /// # Arguments + /// + /// * `track` - The track to add to the Track + /// + #[inline] pub fn add_track(&mut self, track: Arc) { self.remote_tracks.push(track.clone()); - self.rebuild_acceptable_map(); + // self.rebuild_acceptable_map(); self._forward_rtp(track, None, None, self.kind); self.is_simulcast.store(true, Ordering::Relaxed); } + /// Stop the Track + /// + /// # Arguments + /// + /// * `track` - The track to stop + /// + #[inline] pub fn stop(&mut self) { self.remote_tracks.clear(); self.forward_tracks.clear(); + // self.acceptable_map.clear(); + self.is_simulcast.store(false, Ordering::Relaxed); + self.rtp_multicast.clear(); } - pub fn new_forward_track(&self, id: &str, ssrc: u32) -> Result, WebRTCError> { + /// Create a new ForwardTrack + /// + /// # Arguments + /// + /// * `id` - The id of the ForwardTrack + /// * `ssrc` - The ssrc of the ForwardTrack + /// + pub fn new_forward_track( + &self, + id: &str, + ssrc: u32, + ) -> Result>, WebRTCError> { if self.forward_tracks.contains_key(id) { return Err(WebRTCError::FailedToAddTrack); } - let receiver = self.rtp_multicast.add_receiver(id.to_string()); + // Start with Medium (h) quality; ForwardTrack will switch as needed + let forward_track = ForwardTrack::new( self.capability.clone(), self.id.clone(), self.stream_id.clone(), - receiver, + // receiver, id.to_string(), ssrc, self.keyframe_request_callback.clone(), + self.rtp_multicast.clone(), // pass Arc ); self.forward_tracks .insert(id.to_owned(), forward_track.clone()); Ok(forward_track) } + /// Remove a ForwardTrack + /// + /// # Arguments + /// + /// * `id` - The id of the ForwardTrack + /// + #[inline] pub fn remove_forward_track(&self, id: &str) { - self.rtp_multicast.remove_receiver(id); + // Remove from all quality lines + self.rtp_multicast + .remove_receiver_for_quality(TrackQuality::High, id); + self.rtp_multicast + .remove_receiver_for_quality(TrackQuality::Medium, id); + self.rtp_multicast + .remove_receiver_for_quality(TrackQuality::Low, id); self.forward_tracks.remove(id); } - pub fn rebuild_acceptable_map(&self) { - let available_qualities: Vec = self - .remote_tracks - .iter() - .map(|track| TrackQuality::from_str(track.rid()).unwrap()) - .collect::>() // Remove duplicates - .into_iter() - .collect(); - - self.acceptable_map.clear(); - - // Pre-calculate quality fallback mapping - let quality_fallback = |desired: &TrackQuality| -> TrackQuality { - if available_qualities.contains(desired) { - return desired.clone(); - } + /// Rebuild the acceptable map + /// + /// # Arguments + /// + /// * `self` - The Track + /// + // pub fn rebuild_acceptable_map(&self) { + // let available_qualities: Vec = self + // .remote_tracks + // .iter() + // .map(|track| TrackQuality::from_str(track.rid()).unwrap()) + // .collect::>() // Remove duplicates + // .into_iter() + // .collect(); - // Smart fallback logic - match desired { - TrackQuality::High => available_qualities - .iter() - .find(|&q| matches!(q, TrackQuality::Medium)) - .or_else(|| { - available_qualities - .iter() - .find(|&q| matches!(q, TrackQuality::Low)) - }) - .unwrap_or(&TrackQuality::Low) - .clone(), - TrackQuality::Medium => available_qualities - .iter() - .find(|&q| matches!(q, TrackQuality::Low)) - .or_else(|| { - available_qualities - .iter() - .find(|&q| matches!(q, TrackQuality::High)) - }) - .unwrap_or(&TrackQuality::Low) - .clone(), - TrackQuality::Low => available_qualities - .iter() - .find(|&q| matches!(q, TrackQuality::Medium)) - .or_else(|| { - available_qualities - .iter() - .find(|&q| matches!(q, TrackQuality::High)) - }) - .unwrap_or(&TrackQuality::Low) - .clone(), - _ => TrackQuality::Low, - } - }; + // self.acceptable_map.clear(); - // Build mapping more efficiently - for current in &[TrackQuality::Low, TrackQuality::Medium, TrackQuality::High] { - for desired in &[TrackQuality::Low, TrackQuality::Medium, TrackQuality::High] { - let target_quality = quality_fallback(desired); - let acceptable = current == &target_quality; + // // Pre-calculate quality fallback mapping + // let quality_fallback = |desired: &TrackQuality| -> TrackQuality { + // if available_qualities.contains(desired) { + // return desired.clone(); + // } - self.acceptable_map - .insert((current.clone(), desired.clone()), acceptable); - } - } - } + // // Smart fallback logic + // match desired { + // TrackQuality::High => available_qualities + // .iter() + // .find(|&q| matches!(q, TrackQuality::Medium)) + // .or_else(|| { + // available_qualities + // .iter() + // .find(|&q| matches!(q, TrackQuality::Low)) + // }) + // .unwrap_or(&TrackQuality::Low) + // .clone(), + // TrackQuality::Medium => available_qualities + // .iter() + // .find(|&q| matches!(q, TrackQuality::Low)) + // .or_else(|| { + // available_qualities + // .iter() + // .find(|&q| matches!(q, TrackQuality::High)) + // }) + // .unwrap_or(&TrackQuality::Low) + // .clone(), + // TrackQuality::Low => available_qualities + // .iter() + // .find(|&q| matches!(q, TrackQuality::Medium)) + // .or_else(|| { + // available_qualities + // .iter() + // .find(|&q| matches!(q, TrackQuality::High)) + // }) + // .unwrap_or(&TrackQuality::Low) + // .clone(), + // _ => TrackQuality::Low, + // } + // }; + // // Build mapping more efficiently + // for current in &[TrackQuality::Low, TrackQuality::Medium, TrackQuality::High] { + // for desired in &[TrackQuality::Low, TrackQuality::Medium, TrackQuality::High] { + // let target_quality = quality_fallback(desired); + // let acceptable = current == &target_quality; + + // self.acceptable_map + // .insert((current.clone(), desired.clone()), acceptable); + // } + // } + // } + + /// Forward RTP + /// + /// # Arguments + /// + /// * `remote_track` - The remote track to forward + /// * `hls_writer` - The hls writer to write to the cloud storage + /// * `moq_writer` - The moq writer to write to the moq host + /// * `kind` - The kind of the track (video or audio) + /// pub fn _forward_rtp( &self, remote_track: Arc, - _hls_writer: Option>, - _moq_writer: Option>, + hls_writer: Option>, + moq_writer: Option>, kind: RTPCodecType, ) { let multicast = self.rtp_multicast.clone(); let current_quality = Arc::new(TrackQuality::from_str(remote_track.rid()).unwrap()); - let acceptable_map = Arc::clone(&self.acceptable_map); let is_svc = self.is_svc; let is_simulcast = Arc::clone(&self.is_simulcast); tokio::spawn(async move { - let _is_video = kind == RTPCodecType::Video; + let is_video = kind == RTPCodecType::Video; loop { let result = remote_track.read_rtp().await; @@ -219,15 +286,41 @@ impl Track { match result { Ok((rtp, _)) => { if !rtp.payload.is_empty() { - let info = RtpForwardInfo { - packet: Arc::new(rtp), - acceptable_map: acceptable_map.clone(), - is_svc, - is_simulcast: is_simulcast.load(Ordering::Relaxed), - track_quality: (*current_quality).clone(), - }; - - multicast.send(info); + if hls_writer.is_some() || moq_writer.is_some() { + if let Ok(header_bytes) = rtp.header.marshal() { + let mut buf = BytesMut::with_capacity( + header_bytes.len() + rtp.payload.len(), + ); + buf.extend_from_slice(&header_bytes); + buf.extend_from_slice(&rtp.payload); + + if let Some(writer) = &hls_writer { + let _ = writer.write_rtp(&buf, is_video); + } + + if let Some(writer) = &moq_writer { + let _ = writer.write_rtp(&buf, is_video); + } + } else { + debug!("Failed to marshal RTP header"); + } + } else { + let info = RtpForwardInfo { + packet: Arc::new(rtp), + is_svc, + track_quality: (*current_quality).clone(), + }; + + // For simulcast, send to the correct quality line based on rid + if is_simulcast.load(Ordering::Relaxed) { + let quality = TrackQuality::from_str(remote_track.rid()) + .unwrap_or(TrackQuality::Medium); + multicast.send_to_quality(quality, info); + } else { + // Non-simulcast: always use Medium (h) + multicast.send_to_quality(TrackQuality::Medium, info); + } + } } } Err(err) => { diff --git a/crates/webrtc-manager/src/errors/mod.rs b/crates/webrtc-manager/src/errors/mod.rs index 5b8228c..43d2d83 100644 --- a/crates/webrtc-manager/src/errors/mod.rs +++ b/crates/webrtc-manager/src/errors/mod.rs @@ -43,4 +43,7 @@ pub enum WebRTCError { #[error("Room not found")] RoomNotFound, + + #[error("Invalid streaming protocol")] + InvalidStreamingProtocol, } diff --git a/crates/webrtc-manager/src/lib.rs b/crates/webrtc-manager/src/lib.rs index 25d1171..23b8b7a 100644 --- a/crates/webrtc-manager/src/lib.rs +++ b/crates/webrtc-manager/src/lib.rs @@ -1,7 +1,6 @@ pub mod entities; pub mod errors; pub mod models; -pub mod room; pub mod services; pub mod utils; pub mod webrtc_manager; diff --git a/crates/webrtc-manager/src/models/mod.rs b/crates/webrtc-manager/src/models/mod.rs index 2d32367..54ad7eb 100644 --- a/crates/webrtc-manager/src/models/mod.rs +++ b/crates/webrtc-manager/src/models/mod.rs @@ -3,4 +3,5 @@ pub mod data_channel_msg; pub mod params; pub mod quality; pub mod rtp_foward_info; +pub mod streaming_protocol; pub mod track_quality_request; diff --git a/crates/webrtc-manager/src/models/params.rs b/crates/webrtc-manager/src/models/params.rs index f492fb6..e1ea56a 100644 --- a/crates/webrtc-manager/src/models/params.rs +++ b/crates/webrtc-manager/src/models/params.rs @@ -3,7 +3,7 @@ use std::{pin::Pin, sync::Arc}; use parking_lot::RwLock; use serde::Serialize; -use crate::entities::track::Track; +use crate::{entities::track::Track, models::streaming_protocol::StreamingProtocol}; use super::connection_type::ConnectionType; @@ -38,6 +38,8 @@ pub struct JoinRoomParams { pub connection_type: ConnectionType, pub callback: JoinedCallback, pub on_candidate: IceCandidateCallback, + pub streaming_protocol: StreamingProtocol, + pub is_ipv6_supported: bool, } #[derive(Serialize)] @@ -53,6 +55,13 @@ pub struct SubscribeParams { pub participant_id: String, pub on_negotiation_needed: RenegotiationCallback, pub on_candidate: IceCandidateCallback, + pub is_ipv6_supported: bool, +} + +#[derive(Clone)] +pub struct SubscribeHlsLiveStreamParams { + pub target_id: String, + pub participant_id: String, } #[derive(Serialize)] @@ -69,6 +78,12 @@ pub struct SubscribeResponse { pub screen_track_id: Option, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeHlsLiveStreamResponse { + pub hls_urls: Vec, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct IceCandidate { diff --git a/crates/webrtc-manager/src/models/quality.rs b/crates/webrtc-manager/src/models/quality.rs index c40673b..9f79ba3 100644 --- a/crates/webrtc-manager/src/models/quality.rs +++ b/crates/webrtc-manager/src/models/quality.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use webrtc::rtp::codecs::vp9::Vp9Packet; -#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] #[repr(u8)] pub enum TrackQuality { None = 0, @@ -15,17 +15,19 @@ pub enum TrackQuality { impl FromStr for TrackQuality { type Err = (); + #[inline] fn from_str(s: &str) -> Result { Ok(match s { "q" => TrackQuality::Low, "h" => TrackQuality::Medium, "f" => TrackQuality::High, - _ => TrackQuality::None, + _ => TrackQuality::Medium, }) } } impl TrackQuality { + #[inline] pub fn from_u8(value: u8) -> TrackQuality { match value { 1 => TrackQuality::Low, @@ -35,11 +37,13 @@ impl TrackQuality { } } + #[inline] pub fn as_u8(&self) -> u8 { self.clone() as u8 } // Convert TrackQuality to SVC layer IDs for VP9/AV1 + #[inline] fn quality_to_svc_layers(&self) -> (u8, u8) { match self { TrackQuality::Low => (0, 0), @@ -50,6 +54,7 @@ impl TrackQuality { } // Check if an SVC packet should be forwarded based on desired quality + #[inline] pub fn should_forward_vp9_svc(&self, vp9_packet: &Vp9Packet) -> bool { if !vp9_packet.l || vp9_packet.tid == 0 { return true; diff --git a/crates/webrtc-manager/src/models/rtp_foward_info.rs b/crates/webrtc-manager/src/models/rtp_foward_info.rs index f1a2a21..42d11a4 100644 --- a/crates/webrtc-manager/src/models/rtp_foward_info.rs +++ b/crates/webrtc-manager/src/models/rtp_foward_info.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use dashmap::DashMap; use webrtc::rtp::packet::Packet; use super::quality::TrackQuality; @@ -8,8 +7,6 @@ use super::quality::TrackQuality; #[derive(Debug, Clone)] pub struct RtpForwardInfo { pub packet: Arc, - pub acceptable_map: Arc>, pub is_svc: bool, - pub is_simulcast: bool, pub track_quality: TrackQuality, } diff --git a/crates/webrtc-manager/src/models/streaming_protocol.rs b/crates/webrtc-manager/src/models/streaming_protocol.rs new file mode 100644 index 0000000..d5f4d9f --- /dev/null +++ b/crates/webrtc-manager/src/models/streaming_protocol.rs @@ -0,0 +1,25 @@ +use serde_repr::{Deserialize_repr, Serialize_repr}; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +pub enum StreamingProtocol { + SFU = 0, + HLS = 1, + MOQ = 2, +} + +impl From for StreamingProtocol { + fn from(val: u8) -> Self { + match val { + 1 => StreamingProtocol::HLS, + 2 => StreamingProtocol::MOQ, + _ => StreamingProtocol::SFU, + } + } +} + +impl From for u8 { + fn from(sp: StreamingProtocol) -> Self { + sp as u8 + } +} diff --git a/crates/webrtc-manager/src/services/media_monitor.rs b/crates/webrtc-manager/src/services/media_monitor.rs index 4904194..c337276 100644 --- a/crates/webrtc-manager/src/services/media_monitor.rs +++ b/crates/webrtc-manager/src/services/media_monitor.rs @@ -10,10 +10,12 @@ use crate::{ }; impl Media { + #[inline] pub fn set_track_subscribed_callback(&mut self, callback: TrackSubscribedCallback) { self.track_subscribed_callback = Some(callback); } + #[inline] pub fn create_event_channel(&mut self) -> mpsc::UnboundedReceiver { let (sender, receiver) = mpsc::unbounded_channel(); self.track_event_sender = Some(sender); @@ -95,6 +97,7 @@ impl Media { } // Helper function to check if track subscription state changed + #[inline] fn track_subscribed_changed(old_state: &TrackSubscribed, new_state: &TrackSubscribed) -> bool { // Check if simulcast status changed if old_state.is_simulcast != new_state.is_simulcast { @@ -121,6 +124,7 @@ impl Media { } // Helper function to get the highest quality layer with active subscribers + #[inline] fn get_highest_active_quality(track_subscribed: &TrackSubscribed) -> Option { if !track_subscribed.is_simulcast { return None; diff --git a/crates/webrtc-manager/src/services/publisher_messenger.rs b/crates/webrtc-manager/src/services/publisher_messenger.rs index 02ef479..33e26d4 100644 --- a/crates/webrtc-manager/src/services/publisher_messenger.rs +++ b/crates/webrtc-manager/src/services/publisher_messenger.rs @@ -52,6 +52,7 @@ impl Publisher { } } + #[inline] pub async fn start_data_channel_handler(&mut self) { if let Some(mut receiver) = self.track_event_receiver.take() { let data_channel = self.data_channel.clone(); @@ -77,6 +78,7 @@ impl Publisher { } } + #[inline] async fn send_track_subscribed_message( data_channel: &Arc, message: TrackSubscribedMessage, @@ -89,6 +91,7 @@ impl Publisher { Ok(()) } + #[inline] pub async fn create_data_channel(&mut self) -> Result<(), Box> { let data_channel = self .peer_connection diff --git a/crates/webrtc-manager/src/services/track_monitor.rs b/crates/webrtc-manager/src/services/track_monitor.rs index 15e8ba8..11cf47f 100644 --- a/crates/webrtc-manager/src/services/track_monitor.rs +++ b/crates/webrtc-manager/src/services/track_monitor.rs @@ -12,7 +12,7 @@ pub struct TrackSubscribed { pub struct LayerInfo { pub quality: TrackQuality, pub subscriber_count: usize, - pub subscribers: Vec, // forward_track_ids + pub subscribers: Vec, } impl Track { @@ -120,6 +120,7 @@ impl Track { } /// Get a summary of subscriber counts for each layer + #[inline] pub fn get_layer_subscriber_summary(&self) -> HashMap { let track_subscribed = self.get_track_subscribed(); track_subscribed @@ -130,6 +131,7 @@ impl Track { } /// Check if any subscribers exist for simulcast layers + #[inline] pub fn has_simulcast_subscribers(&self) -> bool { if !self.is_simulcast.load(Ordering::Relaxed) { return false; @@ -143,6 +145,7 @@ impl Track { } /// Get the most demanded quality layer + #[inline] pub fn get_most_demanded_quality(&self) -> Option { let track_subscribed = self.get_track_subscribed(); diff --git a/crates/webrtc-manager/src/utils/multicast_sender.rs b/crates/webrtc-manager/src/utils/multicast_sender.rs index e807c6b..43745f3 100644 --- a/crates/webrtc-manager/src/utils/multicast_sender.rs +++ b/crates/webrtc-manager/src/utils/multicast_sender.rs @@ -1,66 +1,125 @@ use std::sync::Arc; -use crossbeam::channel::{self, Receiver, Sender}; use dashmap::DashMap; -use tracing::debug; +use kanal::{self, AsyncReceiver, AsyncSender}; +use crate::models::quality::TrackQuality; use crate::models::rtp_foward_info::RtpForwardInfo; +use std::sync::Mutex; +use std::thread; -// Multi-cast sender wrapper for broadcasting to multiple receivers +const MAX_BUFFER: usize = 1; + +/// Trait for multicast sender +pub trait MulticastSender: Send + Sync { + fn add_receiver_for_quality( + &self, + quality: TrackQuality, + id: String, + ) -> AsyncReceiver; + fn remove_receiver_for_quality(&self, quality: TrackQuality, id: &str); + fn send_to_quality(&self, quality: TrackQuality, info: RtpForwardInfo); + fn clear(&self); +} + +/// Multi-cast sender wrapper for broadcasting to multiple receivers per quality layer #[derive(Debug, Clone)] -pub struct MulticastSender { - senders: Arc>>, +pub struct MulticastSenderImpl { + quality_senders: Arc>>>, } -impl Default for MulticastSender { +impl Default for MulticastSenderImpl { fn default() -> Self { Self::new() } } -impl MulticastSender { +impl MulticastSenderImpl { + /// Create a new MulticastSender pub fn new() -> Self { + let quality_senders = DashMap::new(); + for q in [TrackQuality::High, TrackQuality::Medium, TrackQuality::Low] { + quality_senders.insert(q, DashMap::new()); + } Self { - senders: Arc::new(DashMap::new()), + quality_senders: Arc::new(quality_senders), } } +} - pub fn add_receiver(&self, id: String) -> Receiver { - // Use bounded channel with reasonable buffer size - let (tx, rx) = channel::bounded(1024); - self.senders.insert(id, tx); +impl MulticastSender for MulticastSenderImpl { + /// Add a receiver for a specific quality (f/h/q) + /// + /// # Arguments + /// + /// * `quality` - The quality to add the receiver for + /// * `id` - The id of the receiver + /// + #[inline] + fn add_receiver_for_quality( + &self, + quality: TrackQuality, + id: String, + ) -> AsyncReceiver { + let (tx, rx) = kanal::bounded_async(MAX_BUFFER); + if let Some(map) = self.quality_senders.get(&quality) { + map.insert(id, tx); + } rx } - pub fn remove_receiver(&self, id: &str) { - self.senders.remove(id); + /// Remove a receiver for a specific quality + /// + /// # Arguments + /// + /// * `quality` - The quality to remove the receiver for + /// * `id` - The id of the receiver + /// + #[inline] + fn remove_receiver_for_quality(&self, quality: TrackQuality, id: &str) { + if let Some(map) = self.quality_senders.get(&quality) { + map.remove(id); + } } - pub fn send(&self, info: RtpForwardInfo) { - // Remove any disconnected senders and send to active ones - let mut to_remove = Vec::new(); + /// Send to all receivers for a specific quality + /// + /// # Arguments + /// + /// * `quality` - The quality to send to + /// * `info` - The RtpForwardInfo to send + /// + fn send_to_quality(&self, quality: TrackQuality, info: RtpForwardInfo) { + if let Some(map) = self.quality_senders.get(&quality) { + let to_remove = Arc::new(Mutex::new(Vec::new())); - for entry in self.senders.iter() { - match entry.value().try_send(info.clone()) { - Ok(_) => {} // Success - Err(crossbeam::channel::TrySendError::Full(_)) => { - // Channel full, drop this packet for this receiver - debug!("Channel full for receiver {}, dropping packet", entry.key()); - } - Err(crossbeam::channel::TrySendError::Disconnected(_)) => { - // Receiver disconnected, mark for removal - to_remove.push(entry.key().clone()); + thread::scope(|s| { + for entry in map.iter() { + let tx = entry.value().clone_sync(); + let id = entry.key().clone(); + let info = info.clone(); + let to_remove = Arc::clone(&to_remove); + + s.spawn(move || match tx.send(info) { + Ok(_) => {} + Err(kanal::SendError::ReceiveClosed) | Err(kanal::SendError::Closed) => { + to_remove.lock().unwrap().push(id); + } + }); } - } - } + }); - // Clean up disconnected receivers - for id in to_remove { - self.senders.remove(&id); + // Remove closed channels + let to_remove = to_remove.lock().unwrap(); + for id in to_remove.iter() { + map.remove(id); + } } } - pub fn receiver_count(&self) -> usize { - self.senders.len() + /// Clear the multicast sender + #[inline] + fn clear(&self) { + self.quality_senders.clear(); } } diff --git a/crates/webrtc-manager/src/webrtc_manager.rs b/crates/webrtc-manager/src/webrtc_manager.rs index 5aa82b1..9f330a5 100644 --- a/crates/webrtc-manager/src/webrtc_manager.rs +++ b/crates/webrtc-manager/src/webrtc_manager.rs @@ -4,16 +4,16 @@ use dashmap::DashMap; use parking_lot::RwLock; use crate::{ + entities::room::Room, errors::WebRTCError, models::{ connection_type::ConnectionType, params::{ IceCandidate, IceCandidateCallback, JoinRoomParams, JoinRoomResponse, JoinedCallback, - RenegotiationCallback, SubscribeParams, SubscribeResponse, WClient, - WebRTCManagerConfigs, + RenegotiationCallback, SubscribeHlsLiveStreamParams, SubscribeHlsLiveStreamResponse, + SubscribeParams, SubscribeResponse, WClient, WebRTCManagerConfigs, }, }, - room::Room, }; pub struct JoinRoomReq { @@ -28,6 +28,8 @@ pub struct JoinRoomReq { pub connection_type: u8, pub callback: JoinedCallback, pub ice_candidate_callback: IceCandidateCallback, + pub streaming_protocol: u8, + pub is_ipv6_supported: bool, } #[derive(Clone)] @@ -81,6 +83,8 @@ impl WebRTCManager { connection_type: ConnectionType::from(req.connection_type), callback: req.callback, on_candidate: req.ice_candidate_callback, + streaming_protocol: req.streaming_protocol.into(), + is_ipv6_supported: req.is_ipv6_supported, }; let res = { @@ -100,6 +104,7 @@ impl WebRTCManager { room_id: &str, renegotiation_callback: RenegotiationCallback, ice_candidate_callback: IceCandidateCallback, + is_ipv6_supported: bool, ) -> Result { self._add_client( client_id, @@ -117,6 +122,7 @@ impl WebRTCManager { target_id: (&target_id).to_string(), on_candidate: ice_candidate_callback, on_negotiation_needed: renegotiation_callback, + is_ipv6_supported, }; let res = room.subscribe(params).await?; @@ -124,6 +130,29 @@ impl WebRTCManager { Ok(res) } + pub fn subscribe_hls_live_stream( + &self, + client_id: &str, + target_id: &str, + ) -> Result { + let client = self.get_client_by_id(client_id)?; + + let client = client.clone(); + + let room_id = &client.room_id; + let participant_id = &client.participant_id; + + let room = self._get_room_by_id(room_id)?; + let room = room.read(); + + let res = room.subscribe_hls_live_stream(SubscribeHlsLiveStreamParams { + target_id: target_id.to_string(), + participant_id: participant_id.to_string(), + })?; + + Ok(res) + } + #[allow(clippy::all)] pub fn set_subscriber_desc( &self, @@ -253,6 +282,7 @@ impl WebRTCManager { Ok(client) } + #[inline] pub fn set_audio_enabled(&self, client_id: &str, is_enabled: bool) -> Result<(), WebRTCError> { let client = self.get_client_by_id(client_id)?; @@ -269,6 +299,7 @@ impl WebRTCManager { Ok(()) } + #[inline] pub fn set_video_enabled(&self, client_id: &str, is_enabled: bool) -> Result<(), WebRTCError> { let client = self.get_client_by_id(client_id)?; @@ -285,6 +316,7 @@ impl WebRTCManager { Ok(()) } + #[inline] pub fn set_camera_type(&self, client_id: &str, camera_type: u8) -> Result<(), WebRTCError> { let client = self.get_client_by_id(client_id)?; @@ -301,6 +333,7 @@ impl WebRTCManager { Ok(()) } + #[inline] pub fn set_e2ee_enabled(&self, client_id: &str, is_enabled: bool) -> Result<(), WebRTCError> { let client = self.get_client_by_id(client_id)?; @@ -317,6 +350,7 @@ impl WebRTCManager { Ok(()) } + #[inline] pub fn set_screen_sharing( &self, client_id: &str, @@ -338,6 +372,7 @@ impl WebRTCManager { Ok(()) } + #[inline] pub fn set_hand_raising(&self, client_id: &str, is_enabled: bool) -> Result<(), WebRTCError> { let client = self.get_client_by_id(client_id)?; @@ -354,16 +389,19 @@ impl WebRTCManager { Ok(()) } + #[inline] pub fn _add_client(&self, client_id: &str, info: WClient) { if !self.clients.contains_key(client_id) { self.clients.insert(client_id.to_string(), info); } } + #[inline] pub fn _remove_client(&self, client_id: &str) { self.clients.remove(client_id); } + #[inline] fn _add_room(&self, room_id: &str) -> Result>, WebRTCError> { let room_value = Arc::new(RwLock::new(Room::new(self.configs.clone()))); @@ -373,6 +411,7 @@ impl WebRTCManager { Ok(room_value) } + #[inline] pub fn get_client_by_id(&self, client_id: &str) -> Result { if let Some(client) = self.clients.get(client_id) { Ok(client.clone()) @@ -381,6 +420,7 @@ impl WebRTCManager { } } + #[inline] pub fn _get_room_by_id(&self, room_id: &str) -> Result>, WebRTCError> { if let Some(room) = self.rooms.get(room_id) { Ok(room.clone()) diff --git a/docker/Cargo.toml.sfu b/docker/Cargo.toml.sfu index 5e1ea25..1c09411 100644 --- a/docker/Cargo.toml.sfu +++ b/docker/Cargo.toml.sfu @@ -14,7 +14,7 @@ codegen-units = 1 [workspace.dependencies] serde = "1.0.219" -tokio = { version = "1.45.0", features = ["full"] } +tokio = { version = "1.47.0", features = ["full"] } tokio-util = "0.7.15" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] } @@ -25,17 +25,18 @@ anyhow = "1.0.98" thiserror = "2.0.12" validator = "0.20.0" validator_derive = "0.20.0" -aws-sdk-s3 = { version = "1.86.0", features = ["rt-tokio"] } -aws-config = { version = "1.6.3", features = ["behavior-version-latest"] } -aws-credential-types = "1.2.3" -rustls = { version = "0.23.27", features = ["ring"] } +aws-sdk-s3 = { version = "1.98.0", features = ["rt-tokio"] } +aws-config = { version = "1.8.2", features = ["behavior-version-latest"] } +aws-credential-types = "1.2.4" +rustls = { version = "0.23.29", features = ["ring"] } nanoid = "0.4.0" -rand = "0.9.1" -serde_json = "1.0.140" -async-channel = "2.3.1" +rand = "0.9.2" +serde_json = "1.0.141" +serde_repr = "0.1.20" +async-channel = "2.5.0" rust-embed = "8.7.2" dashmap = "6.1.0" -parking_lot = "0.12.3" +parking_lot = "0.12.4" webrtc = "0.13.0" gst = { package = "gstreamer", git = "https://github.com/GStreamer/gstreamer-rs", branch = "main", features = [ "v1_18", @@ -57,12 +58,11 @@ gst-pbutils = { package = "gstreamer-pbutils", git = "https://github.com/GStream ] } m3u8-rs = { git = "https://github.com/JeWe37/m3u8-rs", branch = "ll-hls" } moq-gst = { git = "https://github.com/waterbustech/moq-gst.git", branch = "main" } -gst-plugin-fmp4 = "0.13.6" +gst-plugin-fmp4 = "0.14.0" prost = "0.13.5" tonic = "0.13.1" -etcd-client = "0.15.0" -sysinfo = "0.35.1" -redis = "0.31.0" +etcd-client = "0.16.1" +sysinfo = "0.36.1" futures = "0.3.31" crossbeam = "0.8.4" mimalloc = "0.1.46" @@ -72,4 +72,3 @@ bytes = "1.10.1" waterbus-proto = { path = "./crates/waterbus-proto" } webrtc-manager = { path = "./crates/webrtc-manager" } egress-manager = { path = "./crates/egress-manager" } - diff --git a/docker/Cargo.toml.signalling b/docker/Cargo.toml.signalling index 6fa306c..005f0e4 100644 --- a/docker/Cargo.toml.signalling +++ b/docker/Cargo.toml.signalling @@ -12,7 +12,7 @@ lto = true codegen-units = 1 [workspace.dependencies] -salvo = { version = "0.78.0", features = [ +salvo = { version = "0.81.0", features = [ "quinn", "tower-compat", "oapi", @@ -27,20 +27,22 @@ salvo = { version = "0.78.0", features = [ "serve-static", ] } serde = "1.0.219" -socketioxide = { version = "0.17.1", features = [ +socketioxide = { git = "https://github.com/Totodore/socketioxide.git", rev = "ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963", features = [ "extensions", "state", "msgpack", ] } -socketioxide-redis = "0.2.2" -tokio = { version = "1.45.0", features = ["full"] } +socketioxide-redis = { git = "https://github.com/Totodore/socketioxide.git", rev = "ba71aa5f07ca72a22ae0ecaf3e026ea0ec114963", features = [ + "redis-cluster", +] } +tokio = { version = "1.47.0", features = ["full"] } tokio-util = "0.7.15" tower = { version = "0.5.2", default-features = false } tower-http = { version = "0.6.4", features = ["cors", "fs", "auth"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] } chrono = { version = "0.4.41", features = ["serde"] } -diesel = { version = "2.2.10", features = ["postgres", "r2d2", "chrono"] } +diesel = { version = "2.2.12", features = ["postgres", "r2d2", "chrono"] } dotenvy = "0.15.7" time = "0.3.41" jsonwebtoken = "9.3.1" @@ -48,18 +50,19 @@ anyhow = "1.0.98" thiserror = "2.0.12" validator = "0.20.0" validator_derive = "0.20.0" -aws-sdk-s3 = { version = "1.86.0", features = ["rt-tokio"] } -aws-config = { version = "1.6.3", features = ["behavior-version-latest"] } -aws-credential-types = "1.2.3" -rustls = { version = "0.23.27", features = ["ring"] } +aws-sdk-s3 = { version = "1.98.0", features = ["rt-tokio"] } +aws-config = { version = "1.8.2", features = ["behavior-version-latest"] } +aws-credential-types = "1.2.4" +rustls = { version = "0.23.29", features = ["ring"] } nanoid = "0.4.0" -rand = "0.9.1" -serde_json = "1.0.140" +rand = "0.9.2" +serde_json = "1.0.141" +serde_repr = "0.1.20" bcrypt = "0.17.0" -async-channel = "2.3.1" +async-channel = "2.5.0" rust-embed = "8.7.2" dashmap = "6.1.0" -parking_lot = "0.12.3" +parking_lot = "0.12.4" webrtc = "0.13.0" gst = { package = "gstreamer", git = "https://github.com/GStreamer/gstreamer-rs", branch = "main", features = [ "v1_18", @@ -81,13 +84,17 @@ gst-pbutils = { package = "gstreamer-pbutils", git = "https://github.com/GStream ] } m3u8-rs = { git = "https://github.com/JeWe37/m3u8-rs", branch = "ll-hls" } moq-gst = { git = "https://github.com/waterbustech/moq-gst.git", branch = "main" } -gst-plugin-fmp4 = "0.13.6" prost = "0.13.5" tonic = "0.13.1" -etcd-client = "0.15.0" -sysinfo = "0.35.1" +etcd-client = "0.16.1" +sysinfo = "0.36.1" futures-util = "0.3.31" -redis = "0.31.0" +redis = { version = "0.32.4", features = [ + "cluster", + "tls-native-tls", + "tokio-native-tls-comp", +] } +reqwest = { version = "0.12.22", features = ["json"] } # Local crates waterbus-proto = { path = "./crates/waterbus-proto" } diff --git a/example.env b/example.env index 17a1f73..c7d5f1c 100644 --- a/example.env +++ b/example.env @@ -1,35 +1,36 @@ APP_PORT=5998 -CLIENT_SECRET_KEY= -SERVER_SECRET_KEY= -TLS_ENABLED=false +API_SUFFIX=busapi/v3 +APP_CLIENT_SECRET=change-this-client-secret +APP_SERVER_SECRET=change-this-server-secret +APP_TLS_ENABLED=false -DATABASE_URL=postgres://postgres:password@localhost:5432/database +DATABASE_URL=postgres://user:password@127.0.0.1:5432/waterbusdb -REDIS_URIS=redis://127.0.0.1:6379?protocol=resp3,redis://127.0.0.1:6380?protocol=resp3,redis://127.0.0.1:6381?protocol=resp3,redis://127.0.0.1:6382?protocol=resp3,redis://127.0.0.1:6383?protocol=resp3,redis://127.0.0.1:6384?protocol=resp3 +REDIS_URIS=redis://127.0.0.1:6379?protocol=resp3,redis://127.0.0.1:6380?protocol=resp3 -STORAGE_ACCOUNT_ID= -STORAGE_ACCESS_KEY_ID= -STORAGE_SECRET_ACCESS_KEY= +STORAGE_ACCOUNT_ID=your-account-id +STORAGE_ACCESS_KEY=your-access-key +STORAGE_SECRET_KEY=your-secret-key STORAGE_REGION=auto -STORAGE_BUCKET_NAME= -STORAGE_ENDPOINT_URL= -STORAGE_CUSTOM_DOMAIN= +STORAGE_BUCKET=your-bucket +STORAGE_ENDPOINT=https://.r2.cloudflarestorage.com/your-bucket +STORAGE_CUSTOM_DOMAIN=cdn.yourdomain.com -AUTH_JWT_SECRET= +AUTH_JWT_SECRET=your-jwt-secret AUTH_JWT_TOKEN_EXPIRES_IN=86400 -AUTH_REFRESH_SECRET= +AUTH_REFRESH_SECRET=your-refresh-secret AUTH_REFRESH_TOKEN_EXPIRES_IN=31536000 -PUBLIC_IP= -PORT_MIN_UDP=19000 -PORT_MAX_UDP=60000 +RTC_EXTERNAL_IP= +RTC_PORT_MIN=19000 +RTC_PORT_MAX=60000 -GROUP_ID=waterbus-group-1 -SFU_HOST=http://0.0.0.0 -SFU_PORT=50051 -DISPATCHER_HOST=http://0.0.0.0 -DISPATCHER_PORT=50052 -ETCD_URI=127.0.0.1:2379 +DIST_GROUP_ID=waterbus-group-0 +DIST_SFU_HOST=http://0.0.0.0 +DIST_SFU_PORT=50051 +DIST_DISPATCHER_HOST=http://0.0.0.0 +DIST_DISPATCHER_PORT=50052 +DIST_ETCD_URI=127.0.0.1:2379 -MOQ_URI=http://localhost:4443/waterbus/ -HLS_MODE=LOCAL +EGRESS_MOQ_URI=http://localhost:4443/waterbus/ +EGRESS_HLS_MODE=LOCAL diff --git a/justfile b/justfile index acbb521..5235dcc 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,7 @@ signalling: cargo run --bin signalling sfu: - cargo run --bin sfu + cargo run --bin sfu --release build-proto: cargo build -p waterbus-proto build-signalling: diff --git a/migrations/2025-07-23-192442_add_capacity_streaming_protocol_to_rooms/down.sql b/migrations/2025-07-23-192442_add_capacity_streaming_protocol_to_rooms/down.sql new file mode 100644 index 0000000..fc2547a --- /dev/null +++ b/migrations/2025-07-23-192442_add_capacity_streaming_protocol_to_rooms/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE rooms DROP COLUMN IF EXISTS capacity; +ALTER TABLE rooms DROP COLUMN IF EXISTS streaming_protocol; diff --git a/migrations/2025-07-23-192442_add_capacity_streaming_protocol_to_rooms/up.sql b/migrations/2025-07-23-192442_add_capacity_streaming_protocol_to_rooms/up.sql new file mode 100644 index 0000000..a31dcb1 --- /dev/null +++ b/migrations/2025-07-23-192442_add_capacity_streaming_protocol_to_rooms/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE rooms ADD COLUMN capacity INTEGER; +ALTER TABLE rooms ADD COLUMN streaming_protocol SMALLINT; diff --git a/sfu/src/application/sfu_grpc_service.rs b/sfu/src/application/sfu_grpc_service.rs index cc4273c..0f8b43a 100644 --- a/sfu/src/application/sfu_grpc_service.rs +++ b/sfu/src/application/sfu_grpc_service.rs @@ -8,7 +8,8 @@ use waterbus_proto::{ LeaveRoomRequest, LeaveRoomResponse, MigratePublisherRequest, MigratePublisherResponse, NewUserJoinedRequest, PublisherCandidateRequest, PublisherRenegotiationRequest, PublisherRenegotiationResponse, SetCameraType, SetEnabledRequest, SetScreenSharingRequest, - SetSubscriberSdpRequest, StatusResponse, SubscribeRequest, SubscribeResponse, + SetSubscriberSdpRequest, StatusResponse, SubscribeHlsLiveStreamRequest, + SubscribeHlsLiveStreamResponse, SubscribeRequest, SubscribeResponse, SubscriberCandidateRequest, SubscriberRenegotiateRequest, sfu_service_server::SfuService, }; use webrtc_manager::{ @@ -123,6 +124,8 @@ impl SfuService for SfuGrpcService { connection_type: req.connection_type as u8, callback: joined_callback, ice_candidate_callback, + streaming_protocol: req.streaming_protocol as u8, + is_ipv6_supported: req.is_ipv6_supported, }) .await }) @@ -217,6 +220,7 @@ impl SfuService for SfuGrpcService { &req.room_id, renegotiation_callback, ice_candidate_callback, + req.is_ipv6_supported, ) .await }) @@ -243,6 +247,28 @@ impl SfuService for SfuGrpcService { } } + async fn subscribe_hls_live_stream( + &self, + req: Request, + ) -> Result, Status> { + let req = req.into_inner(); + + let webrtc_manager = self.webrtc_manager.clone(); + + let response = webrtc_manager + .read() + .subscribe_hls_live_stream(&req.client_id, &req.target_id); + + match response { + Ok(response) => Ok(Response::new(SubscribeHlsLiveStreamResponse { + hls_urls: response.hls_urls, + })), + Err(err) => Err(Status::internal(format!( + "Failed to subscribe hls live stream: {err}" + ))), + } + } + async fn set_subscriber_sdp( &self, req: Request, diff --git a/sfu/src/infrastructure/config/app_env.rs b/sfu/src/infrastructure/config/app_env.rs index 83ea8c2..8735a6b 100644 --- a/sfu/src/infrastructure/config/app_env.rs +++ b/sfu/src/infrastructure/config/app_env.rs @@ -37,23 +37,27 @@ impl AppEnv { dotenv().ok(); Self { - group_id: env::var("GROUP_ID").unwrap_or_else(|_| "waterbus-group-1".to_string()), - public_ip: env::var("PUBLIC_IP").unwrap_or_else(|_| "".to_string()), + group_id: env::var("DIST_GROUP_ID").unwrap_or_else(|_| "waterbus-group-0".to_string()), + public_ip: env::var("RTC_EXTERNAL_IP").unwrap_or_else(|_| "".to_string()), node_id: Self::get_node_id(), - etcd_addr: env::var("ETCD_URI").expect("ETCD_URI must be set"), + etcd_addr: env::var("DIST_ETCD_URI").expect("DIST_ETCD_URI must be set"), udp_port_range: UdpPortRange { - port_min: Self::get_env("PORT_MIN_UDP", 19200), - port_max: Self::get_env("PORT_MAX_UDP", 19250), + port_min: Self::get_env("RTC_PORT_MIN", 19200), + port_max: Self::get_env("RTC_PORT_MAX", 19250), }, grpc_configs: GrpcConfigs { - sfu_host: Self::get_str_env("SFU_HOST", "http://[::1]".to_owned()), - sfu_port: Self::get_env("SFU_PORT", 50051), - dispatcher_host: Self::get_str_env("DISPATCHER_HOST", "http://[::1]".to_owned()), - dispatcher_port: Self::get_env("DISPATCHER_PORT", 50052), + sfu_host: Self::get_str_env("DIST_SFU_HOST", "http://[::1]".to_owned()), + sfu_port: Self::get_env("DIST_SFU_PORT", 50051), + dispatcher_host: Self::get_str_env( + "DIST_DISPATCHER_HOST", + "http://[::1]".to_owned(), + ), + dispatcher_port: Self::get_env("DIST_DISPATCHER_PORT", 50052), }, } } + #[inline] fn get_env(var: &str, default: u16) -> u16 { env::var(var) .ok() @@ -61,6 +65,7 @@ impl AppEnv { .unwrap_or(default) } + #[inline] fn get_str_env(var: &str, default: String) -> String { env::var(var) .ok() @@ -68,6 +73,7 @@ impl AppEnv { .unwrap_or(default) } + #[inline] fn get_node_id() -> String { env::var("POD_ID") .ok() @@ -75,6 +81,7 @@ impl AppEnv { .unwrap_or(Self::get_random_node_id()) } + #[inline] fn get_random_node_id() -> String { let node_id = format!("sfu-node-{}", nanoid!(12)); node_id diff --git a/sfu/src/infrastructure/etcd/mod.rs b/sfu/src/infrastructure/etcd/mod.rs index caaa17e..7cd3908 100644 --- a/sfu/src/infrastructure/etcd/mod.rs +++ b/sfu/src/infrastructure/etcd/mod.rs @@ -3,8 +3,9 @@ use serde::Serialize; use std::time::Duration; use sysinfo::System; use tokio::{sync::oneshot, time::interval}; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; +/// NodeMetadata is the metadata of the node #[derive(Debug, Serialize)] struct NodeMetadata { addr: String, @@ -13,6 +14,7 @@ struct NodeMetadata { group_id: String, } +/// EtcdNode is a node that is used to register and deregister the sfu node to etcd pub struct EtcdNode { lease_id: i64, client: Client, @@ -20,6 +22,19 @@ pub struct EtcdNode { } impl EtcdNode { + /// Register the node to etcd + /// + /// # Arguments + /// + /// * `etcd_addr` - The address of the etcd server + /// * `node_id` - The id of the node + /// * `node_ip` - The ip of the node + /// * `group_id` - The id of the group + /// * `ttl` - The ttl of the lease + /// + /// # Returns + /// + /// * `Self` - The EtcdNode pub async fn register( etcd_addr: String, node_id: String, @@ -54,42 +69,83 @@ impl EtcdNode { let key_clone = key.clone(); tokio::spawn(async move { - let mut tick = interval(Duration::from_secs(5)); + let mut metrics_tick = interval(Duration::from_secs(15)); + + let mut keepalive_tick = interval(Duration::from_secs((ttl - 1).try_into().unwrap())); + + metrics_tick.tick().await; + keepalive_tick.tick().await; + + let mut system: Option = None; + let mut last_cpu = 0.0f32; + let mut last_ram = 0.0f32; + + let mut metrics_counter = 0u32; + loop { tokio::select! { _ = &mut shutdown_rx => { info!("Stopping lease keep-alive"); break; } - result = keeper.keep_alive() => { - if let Err(err) = result { + + _ = keepalive_tick.tick() => { + if let Err(err) = keeper.keep_alive().await { error!("Keep-alive error: {:?}", err); break; } - } - resp = responses.message() => { - if let Ok(Some(msg)) = resp { + + if let Ok(Some(msg)) = responses.message().await { debug!("Lease keep-alive: {:?}", msg); } } - _ = tick.tick() => { - let cpu_free = Self::get_free_cpu().unwrap_or(0.0); - let ram_free = Self::get_free_ram().unwrap_or(0.0); - - let updated_metadata = NodeMetadata { - addr: node_ip.clone(), - cpu: cpu_free, - ram: ram_free, - group_id: group_id.clone(), + + _ = metrics_tick.tick() => { + metrics_counter += 1; + + if metrics_counter % 5 != 0 { + continue; + } + + if system.is_none() { + system = Some(System::new()); + } + + let (cpu_free, ram_free) = if let Some(ref mut sys) = system { + let cpu = Self::get_free_cpu_efficient(sys).unwrap_or(0.0); + let ram = Self::get_free_ram_efficient(sys).unwrap_or(0.0); + (cpu, ram) + } else { + (0.0, 0.0) }; - let new_value = serde_json::to_string(&updated_metadata).unwrap(); - if let Err(err) = client_clone.put( - key_clone.clone(), - new_value, - Some(PutOptions::new().with_lease(lease_id)) - ).await { - error!("Failed to update node resource info: {:?}", err); + let cpu_diff = (cpu_free - last_cpu).abs(); + let ram_diff = (ram_free - last_ram).abs(); + + if cpu_diff >= 5.0 || ram_diff >= 5.0 { + let updated_metadata = NodeMetadata { + addr: node_ip.clone(), + cpu: cpu_free, + ram: ram_free, + group_id: group_id.clone(), + }; + + if let Ok(new_value) = serde_json::to_string(&updated_metadata) { + match client_clone.put( + key_clone.clone(), + new_value, + Some(PutOptions::new().with_lease(lease_id)) + ).await { + Ok(_) => { + last_cpu = cpu_free; + last_ram = ram_free; + debug!("Updated node metrics: CPU: {:.1}%, RAM: {:.1}%", cpu_free, ram_free); + } + Err(err) => { + error!("Failed to update node resource info: {:?}", err); + } + } + } } } } @@ -99,46 +155,69 @@ impl EtcdNode { Ok(Self { lease_id, client, - // key, shutdown_tx: Some(shutdown_tx), }) } + /// Deregister the node from etcd + /// + /// # Arguments + /// + /// * `self` - The EtcdNode + /// + #[inline] pub async fn deregister(mut self) { info!("Revoking lease and shutting down etcd registration"); - let _ = self.client.lease_revoke(self.lease_id).await; + if let Err(err) = self.client.lease_revoke(self.lease_id).await { + warn!("Failed to revoke lease: {:?}", err); + } if let Some(tx) = self.shutdown_tx.take() { let _ = tx.send(()); } } - fn get_free_cpu() -> Option { - let mut system = System::new(); - system.refresh_cpu_all(); + /// Get the free cpu + /// + /// # Arguments + /// + /// * `system` - The system + /// + /// # Returns + #[inline] + fn get_free_cpu_efficient(system: &mut System) -> Option { + system.refresh_cpu_usage(); + + let cpus = system.cpus(); + if cpus.is_empty() { + return Some(50.0); + } - std::thread::sleep(std::time::Duration::from_millis(100)); - system.refresh_cpu_all(); + let total_usage: f32 = cpus.iter().map(|cpu| cpu.cpu_usage()).sum(); + let avg_usage = total_usage / cpus.len() as f32; + let free_percent = (100.0 - avg_usage).max(0.0).min(100.0); - let avg_idle = system - .cpus() - .iter() - .map(|cpu| 100.0 - cpu.cpu_usage()) - .sum::() - / system.cpus().len() as f32; - Some(avg_idle) + Some(free_percent) } - fn get_free_ram() -> Option { - let mut system = System::new(); + /// Get the free ram + /// + /// # Arguments + /// + /// * `system` - The system + /// + /// # Returns + #[inline] + fn get_free_ram_efficient(system: &mut System) -> Option { system.refresh_memory(); - let total = system.total_memory() as f32; - let free = system.free_memory() as f32; + let total = system.total_memory() as f64; + let available = system.available_memory() as f64; if total > 0.0 { - Some((free / total) * 100.0) + let free_percent = ((available / total) * 100.0) as f32; + Some(free_percent.max(0.0).min(100.0)) } else { - None + Some(50.0) } } } diff --git a/sfu/src/main.rs b/sfu/src/main.rs index 341f58c..64804d3 100644 --- a/sfu/src/main.rs +++ b/sfu/src/main.rs @@ -49,7 +49,7 @@ async fn main() -> Result<(), anyhow::Error> { port_max: app_env.udp_port_range.port_max, }; - let ttl = 5; + let ttl = 10; let etcd_node = EtcdNode::register( app_env.etcd_addr, diff --git a/signalling/Cargo.toml b/signalling/Cargo.toml index 81e9d68..65be605 100644 --- a/signalling/Cargo.toml +++ b/signalling/Cargo.toml @@ -19,7 +19,6 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } chrono = { workspace = true, features = ["serde"] } diesel = { workspace = true, features = ["postgres", "r2d2", "chrono"] } -# diesel-derive-enum = { workspace = true, features = ["postgres"] } dotenvy = { workspace = true } time = { workspace = true } jsonwebtoken = { workspace = true } @@ -34,9 +33,15 @@ rustls = { workspace = true, features = ["ring"] } nanoid = { workspace = true } rand = { workspace = true } serde_json = { workspace = true } +serde_repr = { workspace = true } bcrypt = { workspace = true } async-channel = { workspace = true } rust-embed = { workspace = true } +reqwest = { workspace = true } -dispatcher = { workspace = true } +dispatcher = { workspace = true, features = [] } waterbus-proto = { workspace = true } + +[features] +default = [] +redis-cluster = ["dispatcher/redis-cluster"] diff --git a/signalling/src/core/api/salvo_config.rs b/signalling/src/core/api/salvo_config.rs index 375b3b6..2e37458 100644 --- a/signalling/src/core/api/salvo_config.rs +++ b/signalling/src/core/api/salvo_config.rs @@ -103,7 +103,7 @@ pub async fn get_salvo_service(env: &AppEnv) -> Service { .allow_headers(vec!["Authorization", "Content-Type", "X-API-Key"]) .into_handler(); - let router = Router::with_path("busapi/v3") + let router = Router::with_path(format!("{}/v3", env.api_suffix)) .hoop(Logger::new()) .hoop(affix_state::inject(db_pooled_connection)) .hoop(affix_state::inject(jwt_utils)) diff --git a/signalling/src/core/database/schema.rs b/signalling/src/core/database/schema.rs index 53ff58a..00a8073 100644 --- a/signalling/src/core/database/schema.rs +++ b/signalling/src/core/database/schema.rs @@ -59,6 +59,8 @@ diesel::table! { status -> Int2, #[sql_name = "type"] type_ -> Int2, + capacity -> Nullable, + streaming_protocol -> Nullable, } } @@ -88,4 +90,10 @@ diesel::joinable!(messages -> users (created_by_id)); diesel::joinable!(participants -> rooms (room_id)); diesel::joinable!(participants -> users (user_id)); -diesel::allow_tables_to_appear_in_same_query!(members, messages, participants, rooms, users,); +diesel::allow_tables_to_appear_in_same_query!( + members, + messages, + participants, + rooms, + users, +); diff --git a/signalling/src/core/dtos/room/create_room_dto.rs b/signalling/src/core/dtos/room/create_room_dto.rs index d675cd0..cd05bce 100644 --- a/signalling/src/core/dtos/room/create_room_dto.rs +++ b/signalling/src/core/dtos/room/create_room_dto.rs @@ -4,28 +4,23 @@ use validator_derive::Validate; use crate::core::entities::models::{RoomType, StreamingProtocol}; -fn default_room_type() -> RoomType { - RoomType::Conferencing -} - -fn default_streaming_protocol() -> StreamingProtocol { - StreamingProtocol::SFU -} - #[derive(Debug, Serialize, Deserialize, ToSchema, Validate, Clone)] -#[salvo(schema(example = json!({"title": "Dev Daily Meeting", "password": "123123", "room_type": 0})))] +#[serde(rename_all = "camelCase")] +#[salvo(schema(example = json!( + { + "title": "Dev Daily Meeting", + "password": "123123", + "roomType": 0, + "streamingProtocol": 0, + "capacity": 10 + } +)))] pub struct CreateRoomDto { #[validate(length(min = 3))] pub title: String, - #[validate(length(min = 6))] pub password: Option, - - #[serde(default = "default_room_type")] pub room_type: RoomType, - - #[serde(default = "default_streaming_protocol")] pub streaming_protocol: StreamingProtocol, - pub capacity: Option, } diff --git a/signalling/src/core/dtos/room/update_room_dto.rs b/signalling/src/core/dtos/room/update_room_dto.rs index 61b179e..b646950 100644 --- a/signalling/src/core/dtos/room/update_room_dto.rs +++ b/signalling/src/core/dtos/room/update_room_dto.rs @@ -2,10 +2,16 @@ use salvo::oapi::ToSchema; use serde::{Deserialize, Serialize}; use validator_derive::Validate; -use crate::core::entities::models::{RoomType, StreamingProtocol}; - #[derive(Debug, Serialize, Deserialize, ToSchema, Validate, Clone)] -#[salvo(schema(example = json!({"title": "Dev Daily Meeting", "password": "123123"})))] +#[serde(rename_all = "camelCase")] +#[salvo(schema(example = json!( + { + "title": "Dev Daily Meeting", + "password": "123123", + "avatar": "https://example.com/avatar.png", + "capacity": 10 + } +)))] pub struct UpdateRoomDto { #[validate(length(min = 3))] pub title: Option, @@ -16,9 +22,5 @@ pub struct UpdateRoomDto { #[validate(url)] pub avatar: Option, - pub room_type: Option, - - pub streaming_protocol: Option, - pub capacity: Option, } diff --git a/signalling/src/core/dtos/socket/socket_dto.rs b/signalling/src/core/dtos/socket/socket_dto.rs index 9e07d18..62fe1e5 100644 --- a/signalling/src/core/dtos/socket/socket_dto.rs +++ b/signalling/src/core/dtos/socket/socket_dto.rs @@ -11,6 +11,8 @@ pub struct JoinRoomDto { pub is_e2ee_enabled: bool, pub total_tracks: u8, pub connection_type: u8, + pub streaming_protocol: u8, + pub is_ipv6_supported: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -19,6 +21,15 @@ pub struct SubscribeDto { pub target_id: String, pub room_id: String, pub participant_id: String, + pub is_ipv6_supported: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeHlsLiveStreamDto { + pub target_id: String, + pub room_id: String, + pub participant_id: String, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/signalling/src/core/entities/models.rs b/signalling/src/core/entities/models.rs index 3b4e6ae..4fbd950 100644 --- a/signalling/src/core/entities/models.rs +++ b/signalling/src/core/entities/models.rs @@ -6,27 +6,40 @@ use chrono::NaiveDateTime; use diesel::prelude::*; use salvo::oapi::ToSchema; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::{core::database::schema::*, impl_from_i16_with_default}; +fn default_room_type() -> i16 { + 0 +} + +fn default_streaming_protocol() -> i16 { + 0 +} + #[repr(i16)] -#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, ToSchema)] +#[salvo(schema(default = default_room_type, example = 0))] pub enum RoomType { Conferencing = 0, LiveStreaming = 1, } + impl_from_i16_with_default!(RoomType { Conferencing = 0, LiveStreaming = 1, }); #[repr(i16)] -#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, ToSchema)] +#[salvo(schema(default = default_streaming_protocol, example = 0))] pub enum StreamingProtocol { SFU = 0, HLS = 1, MOQ = 2, } + impl_from_i16_with_default!(StreamingProtocol { SFU = 0, HLS = 1, @@ -141,6 +154,8 @@ pub struct Room { pub deleted_at: Option, pub latest_message_id: Option, pub type_: i16, + pub capacity: Option, + pub streaming_protocol: Option, } #[derive( @@ -160,6 +175,7 @@ pub struct Room { #[diesel(belongs_to(User))] #[serde(rename_all = "camelCase")] #[diesel(check_for_backend(diesel::pg::Pg))] +#[salvo(schema(example = json!({"id": 1, "role": 0, "created_at": "2021-01-01T00:00:00Z", "deleted_at": null, "soft_deleted_at": null, "user_id": 1, "room_id": 1})))] pub struct Member { pub id: i32, pub role: i16, @@ -217,6 +233,7 @@ pub struct Message { #[diesel(belongs_to(Room))] #[diesel(belongs_to(User))] #[diesel(check_for_backend(diesel::pg::Pg))] +#[salvo(schema(example = json!({"id": 1, "created_at": "2021-01-01T00:00:00Z", "deleted_at": null, "user_id": 1, "room_id": 1, "status": 0, "node_id": null})))] pub struct Participant { pub id: i32, pub created_at: NaiveDateTime, @@ -242,6 +259,7 @@ pub struct Participant { #[diesel(table_name = users)] #[serde(rename_all = "camelCase")] #[diesel(check_for_backend(diesel::pg::Pg))] +#[salvo(schema(example = json!({"id": 1, "full_name": "John Doe", "user_name": "john_doe", "bio": "I am a software engineer", "external_id": "123123", "avatar": "https://example.com/avatar.png", "created_at": "2021-01-01T00:00:00Z", "updated_at": "2021-01-01T00:00:00Z", "deleted_at": null, "last_seen_at": null})))] pub struct User { pub id: i32, pub full_name: Option, @@ -283,13 +301,15 @@ pub struct NewMessage<'a> { #[diesel(table_name = rooms)] pub struct NewRoom<'a> { pub title: &'a str, - pub password: &'a str, + pub password: Option<&'a str>, pub code: &'a str, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub latest_message_created_at: NaiveDateTime, pub status: i16, pub type_: i16, + pub capacity: Option, + pub streaming_protocol: Option, } #[derive(Insertable)] diff --git a/signalling/src/core/env/app_env.rs b/signalling/src/core/env/app_env.rs index 32b24e1..164a13a 100644 --- a/signalling/src/core/env/app_env.rs +++ b/signalling/src/core/env/app_env.rs @@ -5,15 +5,15 @@ use std::env; pub struct AppEnv { pub group_id: String, pub etcd_addr: String, - pub public_ip: String, pub app_port: u16, pub client_api_key: String, pub db_uri: DbUri, pub redis_uris: Vec, - pub jwt: JwtConfig, - pub udp_port_range: UdpPortRange, - pub grpc_configs: GrpcConfigs, pub tls_enabled: bool, + pub api_suffix: String, + pub jwt: JwtConfig, + pub grpc_configs: GrpcConfig, + pub turn_configs: TurnConfig, } #[derive(Debug, Clone)] @@ -28,19 +28,19 @@ pub struct JwtConfig { } #[derive(Debug, Clone)] -pub struct UdpPortRange { - pub port_min: u16, - pub port_max: u16, -} - -#[derive(Debug, Clone)] -pub struct GrpcConfigs { +pub struct GrpcConfig { pub sfu_host: String, pub sfu_port: u16, pub dispatcher_host: String, pub dispatcher_port: u16, } +#[derive(Debug, Clone)] +pub struct TurnConfig { + pub cf_turn_access_id: String, + pub cf_turn_secret_key: String, +} + impl Default for AppEnv { fn default() -> Self { Self::new() @@ -73,15 +73,10 @@ impl AppEnv { .unwrap_or(default_urls); Self { - group_id: env::var("GROUP_ID").unwrap_or_else(|_| "waterbus-group-1".to_string()), - etcd_addr: env::var("ETCD_URI").expect("ETCD_URI must be set"), - public_ip: env::var("PUBLIC_IP").unwrap_or_else(|_| "".to_string()), + group_id: env::var("DIST_GROUP_ID").unwrap_or_else(|_| "waterbus-group-0".to_string()), + etcd_addr: env::var("DIST_ETCD_URI").expect("DIST_ETCD_URI must be set"), app_port: Self::get_env("APP_PORT", 3000), - client_api_key: env::var("CLIENT_SECRET_KEY").unwrap_or_else(|_| "".to_string()), - udp_port_range: UdpPortRange { - port_min: Self::get_env("PORT_MIN_UDP", 19000), - port_max: Self::get_env("PORT_MAX_UDP", 60000), - }, + client_api_key: env::var("APP_CLIENT_SECRET").unwrap_or_else(|_| "".to_string()), db_uri: DbUri(env::var("DATABASE_URL").expect("DATABASE_URL must be set")), redis_uris, jwt: JwtConfig { @@ -94,16 +89,26 @@ impl AppEnv { 31_536_000, // a year ), }, - grpc_configs: GrpcConfigs { - sfu_host: Self::get_str_env("SFU_HOST", "http://[::1]".to_owned()), - sfu_port: Self::get_env("SFU_PORT", 50051), - dispatcher_host: Self::get_str_env("DISPATCHER_HOST", "http://[::1]".to_owned()), - dispatcher_port: Self::get_env("DISPATCHER_PORT", 50052), + grpc_configs: GrpcConfig { + sfu_host: Self::get_str_env("DIST_SFU_HOST", "http://[::1]".to_owned()), + sfu_port: Self::get_env("DIST_SFU_PORT", 50051), + dispatcher_host: Self::get_str_env( + "DIST_DISPATCHER_HOST", + "http://[::1]".to_owned(), + ), + dispatcher_port: Self::get_env("DIST_DISPATCHER_PORT", 50052), }, - tls_enabled: std::env::var("TLS_ENABLED") + tls_enabled: std::env::var("APP_TLS_ENABLED") .unwrap_or_else(|_| "false".into()) .to_lowercase() == "true", + api_suffix: env::var("APP_API_SUFFIX").unwrap_or_else(|_| "busapi".to_string()), + turn_configs: TurnConfig { + cf_turn_access_id: env::var("CF_TURN_ACCESS_KEY") + .unwrap_or_else(|_| "".to_string()), + cf_turn_secret_key: env::var("CF_TURN_SECRET_KEY") + .unwrap_or_else(|_| "".to_string()), + }, } } diff --git a/signalling/src/core/socket/mod.rs b/signalling/src/core/socket/mod.rs index 87734f4..a7e849b 100644 --- a/signalling/src/core/socket/mod.rs +++ b/signalling/src/core/socket/mod.rs @@ -14,9 +14,12 @@ use socketioxide::{ handler::ConnectHandler, socket::Sid, }; +#[cfg(feature = "redis-cluster")] +use socketioxide_redis::drivers::redis::ClusterDriver; +#[cfg(not(feature = "redis-cluster"))] +use socketioxide_redis::drivers::redis::RedisDriver; use socketioxide_redis::{ - ClusterAdapter, CustomRedisAdapter, RedisAdapterCtr, - drivers::redis::{ClusterDriver, redis_client as redis}, + CustomRedisAdapter, RedisAdapterCtr, drivers::redis::redis_client as redis, }; use tower::ServiceBuilder; use tower_http::cors::CorsLayer; @@ -24,7 +27,8 @@ use tracing::{info, warn}; use waterbus_proto::{ AddPublisherCandidateRequest, AddSubscriberCandidateRequest, JoinRoomRequest, LeaveRoomRequest, MigratePublisherRequest, PublisherRenegotiationRequest, SetCameraType, SetEnabledRequest, - SetScreenSharingRequest, SetSubscriberSdpRequest, SubscribeRequest, + SetScreenSharingRequest, SetSubscriberSdpRequest, SubscribeHlsLiveStreamRequest, + SubscribeRequest, }; use crate::{ @@ -32,7 +36,7 @@ use crate::{ dtos::socket::socket_dto::{ AnswerSubscribeDto, JoinRoomDto, MigrateConnectionDto, PublisherCandidateDto, PublisherRenegotiationDto, SetCameraTypeDto, SetEnabledDto, SetHandRaisingDto, - SetScreenSharingDto, SubscribeDto, SubscriberCandidateDto, + SetScreenSharingDto, SubscribeDto, SubscribeHlsLiveStreamDto, SubscriberCandidateDto, }, env::app_env::AppEnv, types::{ @@ -41,8 +45,9 @@ use crate::{ responses::socket_response::{ CameraTypeResponse, EnabledResponse, HandleRaisingResponse, IceCandidate, JoinRoomResponse, NewUserJoinedResponse, ParticipantHasLeftResponse, - RenegotiateResponse, ScreenSharingResponse, SubscribeParticipantResponse, - SubscribeResponse, SubscriberRenegotiationResponse, SubsriberCandidateResponse, + RenegotiateResponse, ScreenSharingResponse, SubscribeHlsLiveStreamResponse, + SubscribeParticipantResponse, SubscribeResponse, SubscriberRenegotiationResponse, + SubsriberCandidateResponse, }, }, utils::jwt_utils::JwtUtils, @@ -56,6 +61,12 @@ use crate::{ }, }; +#[cfg(feature = "redis-cluster")] +type DefaultDriver = ClusterDriver; + +#[cfg(not(feature = "redis-cluster"))] +type DefaultDriver = RedisDriver; + #[derive(Clone)] pub struct UserId(pub String); @@ -63,8 +74,12 @@ pub struct UserId(pub String); async fn version() -> &'static str { "[v3] Waterbus Service written in Rust" } + +#[cfg(feature = "redis-cluster")] #[derive(Clone)] struct RemoteUserCnt(redis::cluster_async::ClusterConnection); + +#[cfg(feature = "redis-cluster")] impl RemoteUserCnt { fn new(conn: redis::cluster_async::ClusterConnection) -> Self { Self(conn) @@ -87,15 +102,56 @@ impl RemoteUserCnt { } } +#[cfg(not(feature = "redis-cluster"))] +#[derive(Clone)] +struct RemoteUserCnt(redis::aio::MultiplexedConnection); + +#[cfg(not(feature = "redis-cluster"))] +impl RemoteUserCnt { + fn new(conn: redis::aio::MultiplexedConnection) -> Self { + Self(conn) + } + async fn add_user(&self) -> Result { + let mut conn = self.0.clone(); + let num_users: usize = redis::cmd("INCR") + .arg("num_users") + .query_async(&mut conn) + .await?; + Ok(num_users) + } + async fn remove_user(&self) -> Result { + let mut conn = self.0.clone(); + let num_users: usize = redis::cmd("DECR") + .arg("num_users") + .query_async(&mut conn) + .await?; + Ok(num_users) + } +} + pub async fn get_socket_router( env: &AppEnv, jwt_utils: JwtUtils, room_service: RoomServiceImpl, message_receiver: Receiver, ) -> Result> { - let client = redis::cluster::ClusterClient::new(env.clone().redis_uris).unwrap(); - let adapter = RedisAdapterCtr::new_with_cluster(&client).await?; - let conn = client.get_async_connection().await?; + let (adapter, conn); + + #[cfg(feature = "redis-cluster")] + { + let client = redis::cluster::ClusterClient::new(env.clone().redis_uris) + .expect("Failed to create Redis cluster client"); + adapter = RedisAdapterCtr::new_with_cluster(&client).await?; + conn = client.get_async_connection().await?; + } + + #[cfg(not(feature = "redis-cluster"))] + { + let client = redis::Client::open(env.clone().redis_uris.first().unwrap().as_str())?; + + adapter = RedisAdapterCtr::new_with_redis(&client).await?; + conn = client.get_multiplexed_tokio_connection().await?; + } let env_clone = env.clone(); @@ -112,16 +168,39 @@ pub async fn get_socket_router( let dispatcher = DispatcherManager::new(configs).await; - let (layer, io) = SocketIo::builder() - .with_state(RemoteUserCnt::new(conn)) - .with_state(jwt_utils.clone()) - .with_state(room_service.clone()) - .with_state(dispatcher) - .with_adapter::>(adapter) - .with_parser(ParserConfig::msgpack()) - .ping_interval(Duration::from_secs(5)) - .ping_timeout(Duration::from_secs(2)) - .build_layer(); + let (layer, io); + + #[cfg(feature = "redis-cluster")] + { + use socketioxide_redis::ClusterAdapter; + + (layer, io) = SocketIo::builder() + .with_state(RemoteUserCnt::new(conn)) + .with_state(jwt_utils.clone()) + .with_state(room_service.clone()) + .with_state(dispatcher) + .with_adapter::>(adapter) + .with_parser(ParserConfig::msgpack()) + .ping_interval(Duration::from_secs(5)) + .ping_timeout(Duration::from_secs(2)) + .build_layer() + } + + #[cfg(not(feature = "redis-cluster"))] + { + use socketioxide_redis::RedisAdapter; + + (layer, io) = SocketIo::builder() + .with_state(RemoteUserCnt::new(conn)) + .with_state(jwt_utils.clone()) + .with_state(room_service.clone()) + .with_state(dispatcher) + .with_adapter::>(adapter) + .with_parser(ParserConfig::msgpack()) + .ping_interval(Duration::from_secs(5)) + .ping_timeout(Duration::from_secs(2)) + .build_layer() + } let layer = ServiceBuilder::new() .layer(CorsLayer::permissive()) // Enable CORS policy @@ -147,7 +226,7 @@ pub async fn get_socket_router( } pub async fn handle_dispatcher_callback( - io: SocketIo>, + io: SocketIo>, receiver: Receiver, room_service: RoomServiceImpl, ) { @@ -293,7 +372,7 @@ pub async fn handle_dispatcher_callback( } pub async fn handle_message_update( - io: SocketIo>, + io: SocketIo>, receiver: Receiver, ) { // Non-blocking check for any new messages on the channel @@ -382,6 +461,10 @@ async fn on_connect(socket: SocketRef, user_id: Extension socket.on(WsEvent::RoomReconnect.to_str(), on_reconnect); socket.on(WsEvent::RoomPublish.to_str(), handle_join_room); socket.on(WsEvent::RoomSubscribe.to_str(), handle_subscribe); + socket.on( + WsEvent::RoomSubscribeHlsLiveStream.to_str(), + handle_subscribe_hls_live_stream, + ); socket.on( WsEvent::RoomAnswerSubscriber.to_str(), handle_answer_subscribe, @@ -449,6 +532,8 @@ async fn handle_join_room( participant_id: participant_id.to_string(), room_id: room_id.clone(), connection_type: data.connection_type as i32, + streaming_protocol: data.streaming_protocol as i32, + is_ipv6_supported: data.is_ipv6_supported, }; match dispatcher_manager.join_room(req).await { @@ -485,6 +570,7 @@ async fn handle_subscribe( target_id: target_id.clone(), participant_id, room_id, + is_ipv6_supported: data.is_ipv6_supported, }; let res = dispatcher_manager.subscribe(req).await; @@ -512,6 +598,37 @@ async fn handle_subscribe( } } +async fn handle_subscribe_hls_live_stream( + socket: SocketRef, + Data(data): Data, + dispatcher_manager: State, +) { + let client_id = socket.id.to_string(); + let target_id = data.target_id; + let room_id = data.room_id; + let participant_id = data.participant_id; + + let req = SubscribeHlsLiveStreamRequest { + client_id, + target_id, + room_id, + participant_id, + }; + + let res = dispatcher_manager.subscribe_hls_live_stream(req).await; + + if let Ok(res) = res { + let _ = socket + .emit( + WsEvent::RoomSubscribeHlsLiveStream.to_str(), + &SubscribeHlsLiveStreamResponse { + hls_urls: res.hls_urls, + }, + ) + .ok(); + } +} + async fn handle_answer_subscribe( socket: SocketRef, Data(data): Data, diff --git a/signalling/src/core/types/enums/ws_event.rs b/signalling/src/core/types/enums/ws_event.rs index 52b70c3..1365b0d 100644 --- a/signalling/src/core/types/enums/ws_event.rs +++ b/signalling/src/core/types/enums/ws_event.rs @@ -2,6 +2,7 @@ pub enum WsEvent { RoomPublish, RoomSubscribe, + RoomSubscribeHlsLiveStream, RoomAnswerSubscriber, RoomLeave, RoomReconnect, @@ -38,6 +39,7 @@ impl WsEvent { match self { WsEvent::RoomPublish => "room.publish", WsEvent::RoomSubscribe => "room.subscribe", + WsEvent::RoomSubscribeHlsLiveStream => "room.subscribe_hls_live_stream", WsEvent::RoomAnswerSubscriber => "room.answer_subscriber", WsEvent::RoomLeave => "room.leave", WsEvent::RoomReconnect => "room.reconnect", diff --git a/signalling/src/core/types/errors/auth_error.rs b/signalling/src/core/types/errors/auth_error.rs index c49b8f3..71ea5eb 100644 --- a/signalling/src/core/types/errors/auth_error.rs +++ b/signalling/src/core/types/errors/auth_error.rs @@ -21,9 +21,12 @@ pub enum AuthError { #[error("User with ID {0} not found")] UserNotFound(i32), - #[error("An unexpected error occurred in channel: {0}")] + #[error("An unexpected error occurred in auth: {0}")] UnexpectedError(String), + #[error("Failed to contact Cloudflare: {0}")] + CloudflareError(String), + #[error("General error: {0}")] General(#[from] GeneralError), } @@ -37,6 +40,7 @@ impl Writer for AuthError { AuthError::InvalidAPIKey | AuthError::InvalidToken => StatusCode::UNAUTHORIZED, AuthError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR, AuthError::General(_) => StatusCode::INTERNAL_SERVER_ERROR, + AuthError::CloudflareError(_) => StatusCode::INTERNAL_SERVER_ERROR, }; res.status_code(status); diff --git a/signalling/src/core/types/errors/general.rs b/signalling/src/core/types/errors/general.rs index 72dacc7..9edb4b2 100644 --- a/signalling/src/core/types/errors/general.rs +++ b/signalling/src/core/types/errors/general.rs @@ -2,7 +2,7 @@ use salvo::oapi::ToSchema; use serde::Serialize; use thiserror::Error; -#[derive(Debug, Error, ToSchema, Serialize, Clone)] +#[derive(Debug, Error, ToSchema, Serialize, Clone, PartialEq)] pub enum GeneralError { #[error("Database connection failed")] DbConnectionError, diff --git a/signalling/src/core/types/errors/room_error.rs b/signalling/src/core/types/errors/room_error.rs index d742ec5..7ead920 100644 --- a/signalling/src/core/types/errors/room_error.rs +++ b/signalling/src/core/types/errors/room_error.rs @@ -6,7 +6,7 @@ use salvo::prelude::*; use serde::Serialize; use thiserror::Error; -#[derive(Debug, Error, ToSchema, Serialize, Clone)] +#[derive(Debug, Error, ToSchema, Serialize, Clone, PartialEq)] pub enum RoomError { #[error("Room with ID {0} not found")] RoomNotFound(i32), @@ -22,6 +22,8 @@ pub enum RoomError { PasswordIncorrect, #[error("An unexpected error occurred in channel: {0}")] UnexpectedError(String), + #[error("Room is full")] + RoomIsFull, #[error("General error: {0}")] General(#[from] GeneralError), } @@ -38,6 +40,7 @@ impl Writer for RoomError { RoomError::PasswordIncorrect => StatusCode::UNAUTHORIZED, RoomError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR, RoomError::General(_) => StatusCode::INTERNAL_SERVER_ERROR, + RoomError::RoomIsFull => StatusCode::BAD_REQUEST, }; res.status_code(status); res.render(Json(serde_json::json!({ "message": self.to_string() }))); diff --git a/signalling/src/core/types/responses/auth_response.rs b/signalling/src/core/types/responses/auth_response.rs index f0111ac..9cd6bb8 100644 --- a/signalling/src/core/types/responses/auth_response.rs +++ b/signalling/src/core/types/responses/auth_response.rs @@ -7,6 +7,7 @@ use crate::core::entities::models::User; #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] +#[salvo(schema(example = json!({"token": "123123", "refresh_token": "123123", "user": {"id": 1, "full_name": "John Doe", "user_name": "john_doe", "bio": "I am a software engineer", "external_id": "123123", "avatar": "https://example.com/avatar.png"}})))] pub struct AuthResponse { pub token: String, pub refresh_token: String, diff --git a/signalling/src/core/types/responses/ice_response.rs b/signalling/src/core/types/responses/ice_response.rs new file mode 100644 index 0000000..3a9e958 --- /dev/null +++ b/signalling/src/core/types/responses/ice_response.rs @@ -0,0 +1,62 @@ +use salvo::http::StatusCode; +use salvo::oapi::{self, EndpointOutRegister, ToSchema}; +use salvo::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +#[salvo(schema(example = json!({"urls": ["turn:rtc.live.cloudflare.com:443?transport=tcp"], "username": "123123", "credential": "123123"})))] +pub struct IceServer { + pub urls: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub credential: Option, +} + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +#[salvo(schema(example = json!( + { + "iceServers": [ + { + "urls": [ + "stun:stun.cloudflare.com:3478", + "stun:stun.cloudflare.com:53", + "turn:turn.cloudflare.com:3478?transport=udp", + "turn:turn.cloudflare.com:53?transport=udp", + "turn:turn.cloudflare.com:3478?transport=tcp", + "turn:turn.cloudflare.com:80?transport=tcp", + "turns:turn.cloudflare.com:5349?transport=tcp", + "turns:turn.cloudflare.com:443?transport=tcp" + ], + "username": "bc91b63e2b5d759f8eb9f3b58062439e0a0e15893d76317d833265ad08d6631099ce7c7087caabb31ad3e1c386424e3e", + "credential": "ebd71f1d3edbc2b0edae3cd5a6d82284aeb5c3b8fdaa9b8e3bf9cec683e0d45fe9f5b44e5145db3300f06c250a15b4a0" + } + ] + } +)))] +pub struct IceServersResponse { + #[serde(rename = "iceServers")] + pub ice_servers: Vec, +} + +#[async_trait] +impl Writer for IceServersResponse { + async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) { + res.status_code(StatusCode::OK); + res.render(Json(self)); + } +} + +impl EndpointOutRegister for IceServersResponse { + fn register(components: &mut oapi::Components, operation: &mut oapi::Operation) { + operation.responses.insert( + StatusCode::OK.as_str(), + oapi::Response::new("OK").add_content( + "application/json", + IceServersResponse::to_schema(components), + ), + ); + } +} diff --git a/signalling/src/core/types/responses/mod.rs b/signalling/src/core/types/responses/mod.rs index 0575fee..a25751a 100644 --- a/signalling/src/core/types/responses/mod.rs +++ b/signalling/src/core/types/responses/mod.rs @@ -1,6 +1,7 @@ pub mod auth_response; pub mod check_username_response; pub mod failed_response; +pub mod ice_response; pub mod list_message_response; pub mod list_room_response; pub mod message_response; diff --git a/signalling/src/core/types/responses/room_response.rs b/signalling/src/core/types/responses/room_response.rs index b7f026e..b9ac646 100644 --- a/signalling/src/core/types/responses/room_response.rs +++ b/signalling/src/core/types/responses/room_response.rs @@ -15,6 +15,7 @@ pub struct RoomResponse { pub members: Vec, pub participants: Vec, pub latest_message: Option, + pub is_protected: Option, } #[derive(Debug, Serialize, ToSchema, Clone)] @@ -25,7 +26,7 @@ pub struct MemberResponse { pub user: Option, } -#[derive(Debug, Serialize, Clone, ToSchema)] +#[derive(Debug, Serialize, ToSchema, Clone)] #[serde(rename_all = "camelCase")] pub struct ParticipantResponse { #[serde(flatten)] diff --git a/signalling/src/core/types/responses/socket_response.rs b/signalling/src/core/types/responses/socket_response.rs index b0e59be..d2a60a3 100644 --- a/signalling/src/core/types/responses/socket_response.rs +++ b/signalling/src/core/types/responses/socket_response.rs @@ -50,6 +50,12 @@ pub struct SubscribeResponse { pub screen_track_id: Option, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeHlsLiveStreamResponse { + pub hls_urls: Vec, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct HandleRaisingResponse { diff --git a/signalling/src/core/utils/aws_utils.rs b/signalling/src/core/utils/aws_utils.rs index ef983c7..079ee83 100644 --- a/signalling/src/core/utils/aws_utils.rs +++ b/signalling/src/core/utils/aws_utils.rs @@ -1,18 +1,20 @@ -use aws_config::meta::region::RegionProviderChain; +use aws_config::{BehaviorVersion, SdkConfig}; use aws_credential_types::Credentials; -use aws_sdk_s3::{Client, config::Region}; +use aws_sdk_s3::{ + Client, + config::{Region, SharedCredentialsProvider}, +}; use std::env; -pub async fn get_storage_object_client() -> (Client, String, Option) { +pub fn get_storage_object_client() -> (Client, String, Option) { dotenvy::dotenv().ok(); - let access_key_id = env::var("STORAGE_ACCESS_KEY_ID").expect("STORAGE_ACCESS_KEY_ID not set"); - let secret_access_key = - env::var("STORAGE_SECRET_ACCESS_KEY").expect("STORAGE_SECRET_ACCESS_KEY not set"); - let region = env::var("STORAGE_REGION").ok(); - let endpoint_url = env::var("STORAGE_ENDPOINT_URL").ok(); + let access_key_id = env::var("STORAGE_ACCESS_KEY").expect("STORAGE_ACCESS_KEY not set"); + let secret_access_key = env::var("STORAGE_SECRET_KEY").expect("STORAGE_SECRET_KEY not set"); + let region = env::var("STORAGE_REGION").unwrap_or_else(|_| "auto".to_string()); + let endpoint_url = env::var("STORAGE_ENDPOINT").ok(); let custom_domain = env::var("STORAGE_CUSTOM_DOMAIN").ok(); - let bucket_name = env::var("STORAGE_BUCKET_NAME").expect("STORAGE_BUCKET_NAME must be set"); + let bucket_name = env::var("STORAGE_BUCKET").expect("STORAGE_BUCKET must be set"); let credentials = Credentials::new( access_key_id, @@ -22,18 +24,14 @@ pub async fn get_storage_object_client() -> (Client, String, Option) { "waterbus_provider", ); - let region_provider = RegionProviderChain::first_try(region.map(Region::new)) - .or_default_provider() - .or_else(Region::new("us-west-2")); - - let shared_config = aws_config::from_env() - .region(region_provider) + let config = SdkConfig::builder() + .behavior_version(BehaviorVersion::latest()) .endpoint_url(endpoint_url.unwrap_or_default()) - .credentials_provider(credentials) - .load() - .await; + .region(Region::new(region)) + .credentials_provider(SharedCredentialsProvider::new(credentials)) + .build(); - let client = Client::new(&shared_config); + let client = Client::new(&config); (client, bucket_name, custom_domain) } diff --git a/signalling/src/core/utils/try_from_i16.rs b/signalling/src/core/utils/try_from_i16.rs index 37569c6..9745c6a 100644 --- a/signalling/src/core/utils/try_from_i16.rs +++ b/signalling/src/core/utils/try_from_i16.rs @@ -10,6 +10,7 @@ macro_rules! impl_from_i16_with_default { } } } + impl From<$enum_name> for i16 { fn from(value: $enum_name) -> i16 { value as i16 diff --git a/signalling/src/features/auth/router.rs b/signalling/src/features/auth/router.rs index 36e621f..ff2dc38 100644 --- a/signalling/src/features/auth/router.rs +++ b/signalling/src/features/auth/router.rs @@ -8,26 +8,34 @@ use salvo::prelude::*; use salvo::{Response, Router, oapi::endpoint}; use crate::core::dtos::auth::create_token_dto::CreateTokenDto; +use crate::core::env::app_env::AppEnv; use crate::core::types::errors::auth_error::AuthError; use crate::core::types::responses::auth_response::AuthResponse; use crate::core::types::responses::failed_response::FailedResponse; +use crate::core::types::responses::ice_response::IceServersResponse; use crate::core::types::responses::presigned_url_response::PresignedResponse; use crate::core::utils::aws_utils::get_storage_object_client; use crate::core::utils::jwt_utils::JwtUtils; use crate::features::auth::repository::AuthRepositoryImpl; use super::service::{AuthService, AuthServiceImpl}; +use serde_json::json; pub fn get_auth_router(jwt_utils: JwtUtils) -> Router { let presinged_route = Router::with_hoop(jwt_utils.auth_middleware()) .path("presigned-url") .post(generate_presigned_url); + let ice_servers_route = Router::with_hoop(jwt_utils.auth_middleware()) + .path("ice-servers") + .get(generate_ice_servers); + Router::new() .path("auth") .post(create_token) .push(Router::with_hoop(jwt_utils.refresh_token_middleware()).get(refresh_token)) .push(presinged_route) + .push(ice_servers_route) } /// Get presigned url @@ -39,7 +47,7 @@ async fn generate_presigned_url(_res: &mut Response) -> Result Result Result { + let turn_config = depot.obtain::().unwrap().turn_configs.clone(); + + let api_url = format!( + "https://rtc.live.cloudflare.com/v1/turn/keys/{}/credentials/generate-ice-servers", + turn_config.cf_turn_access_id + ); + + let client = reqwest::Client::new(); + let resp = client + .post(&api_url) + .header( + "Authorization", + format!("Bearer {}", turn_config.cf_turn_secret_key), + ) + .header("Content-Type", "application/json") + .json(&json!({"ttl": 86400})) + .send() + .await + .map_err(|e| AuthError::CloudflareError(e.to_string()))?; + + let status = resp.status(); + + if !status.is_success() { + return Err(AuthError::CloudflareError(resp.text().await.unwrap())); + } + + let res: IceServersResponse = resp + .json() + .await + .map_err(|e| AuthError::UnexpectedError(e.to_string()))?; + + Ok(res) +} diff --git a/signalling/src/features/auth/service.rs b/signalling/src/features/auth/service.rs index 6f03e72..af9628b 100644 --- a/signalling/src/features/auth/service.rs +++ b/signalling/src/features/auth/service.rs @@ -155,11 +155,10 @@ mod tests { // Helper to create a dummy AppEnv for JwtUtils fn dummy_app_env() -> crate::core::env::app_env::AppEnv { - use crate::core::env::app_env::{AppEnv, DbUri, GrpcConfigs, JwtConfig, UdpPortRange}; + use crate::core::env::app_env::{AppEnv, DbUri, GrpcConfig, JwtConfig, TurnConfig}; AppEnv { group_id: "test-group".to_string(), etcd_addr: "localhost:2379".to_string(), - public_ip: "127.0.0.1".to_string(), app_port: 1234, client_api_key: "dummy".to_string(), db_uri: DbUri("dummy_db_uri".to_string()), @@ -170,17 +169,18 @@ mod tests { token_expires_in_seconds: 3600, refresh_token_expires_in_seconds: 7200, }, - udp_port_range: UdpPortRange { - port_min: 10000, - port_max: 20000, - }, - grpc_configs: GrpcConfigs { + grpc_configs: GrpcConfig { sfu_host: "localhost".to_string(), sfu_port: 1, dispatcher_host: "localhost".to_string(), dispatcher_port: 2, }, tls_enabled: false, + api_suffix: "busapi".to_string(), + turn_configs: TurnConfig { + cf_turn_access_id: "".to_string(), + cf_turn_secret_key: "".to_string(), + }, } } diff --git a/signalling/src/features/chat/router.rs b/signalling/src/features/chat/router.rs index a8b8f44..a259a4a 100644 --- a/signalling/src/features/chat/router.rs +++ b/signalling/src/features/chat/router.rs @@ -170,5 +170,6 @@ async fn delete_conversation( members: vec![], participants: vec![], latest_message: None, + is_protected: None, }) } diff --git a/signalling/src/features/chat/service.rs b/signalling/src/features/chat/service.rs index cca3778..9bfaca3 100644 --- a/signalling/src/features/chat/service.rs +++ b/signalling/src/features/chat/service.rs @@ -330,6 +330,8 @@ mod tests { deleted_at: None, latest_message_id: None, type_: 0, + capacity: None, + streaming_protocol: Some(StreamingProtocol::SFU as i16), } } @@ -378,6 +380,7 @@ mod tests { }], participants: vec![], latest_message: None, + is_protected: None, } } diff --git a/signalling/src/features/room/repository.rs b/signalling/src/features/room/repository.rs index 8be2b8b..7645031 100644 --- a/signalling/src/features/room/repository.rs +++ b/signalling/src/features/room/repository.rs @@ -7,7 +7,7 @@ use diesel::{ update, }; use salvo::async_trait; -use tracing::warn; +use tracing::{debug, warn}; use chrono::NaiveDateTime; @@ -204,6 +204,7 @@ impl RoomRepository for RoomRepositoryImpl { members, participants, latest_message, + is_protected: None, } }) .collect::>(); @@ -278,6 +279,7 @@ impl RoomRepository for RoomRepositoryImpl { members: member_responses, participants: participant_responses, latest_message: None, + is_protected: None, }; Ok(response) @@ -327,11 +329,14 @@ impl RoomRepository for RoomRepositoryImpl { .next() .ok_or(RoomError::RoomCodeNotFound(room_code.to_string()))?; + let is_protected = Some(room.password.is_some()); + let response = RoomResponse { room, members: member_responses, participants: participant_responses, latest_message: None, + is_protected, }; Ok(response) @@ -351,6 +356,7 @@ impl RoomRepository for RoomRepositoryImpl { members: Vec::new(), participants: Vec::new(), latest_message: None, + is_protected: None, }; Ok(room_response) @@ -390,6 +396,7 @@ impl RoomRepository for RoomRepositoryImpl { }], participants: vec![], latest_message: None, + is_protected: None, }; Ok(response) @@ -409,6 +416,9 @@ impl RoomRepository for RoomRepositoryImpl { rooms::latest_message_created_at.eq(room.latest_message_created_at), rooms::latest_message_id.eq(room.latest_message_id), rooms::status.eq(room.status), + rooms::type_.eq(room.type_), + rooms::streaming_protocol.eq(room.streaming_protocol), + rooms::capacity.eq(room.capacity), )) .returning(Room::as_select()) .get_result(&mut conn) @@ -576,7 +586,7 @@ impl RoomRepository for RoomRepositoryImpl { })?; if deleted_rows == 0 { - warn!("No participants found for node_id: {}", node_id); + debug!("No participants found for node_id: {}", node_id); } Ok(()) diff --git a/signalling/src/features/room/router.rs b/signalling/src/features/room/router.rs index bce0c6f..42badbb 100644 --- a/signalling/src/features/room/router.rs +++ b/signalling/src/features/room/router.rs @@ -33,12 +33,12 @@ pub fn get_room_router(jwt_utils: JwtUtils) -> Router { let deactivate_router = Router::with_path("/{room_id}/deactivate").post(deactivate_room); - Router::with_hoop(jwt_utils.auth_middleware()) + let auth_router = Router::new() + .hoop(jwt_utils.auth_middleware()) .path("rooms") .post(create_room) .get(get_rooms_by_user) .push(Router::with_path("inactive").get(get_inactive_rooms)) - .push(Router::with_path("/{code}").get(get_room_by_code)) .push( Router::with_path("/{room_id}") .put(update_room) @@ -46,7 +46,11 @@ pub fn get_room_router(jwt_utils: JwtUtils) -> Router { ) .push(member_router) .push(join_router) - .push(deactivate_router) + .push(deactivate_router); + + let public_router = Router::with_path("rooms/{code}").get(get_room_by_code); + + Router::new().push(auth_router).push(public_router) } /// Retrieves room details using a unique room code. diff --git a/signalling/src/features/room/service.rs b/signalling/src/features/room/service.rs index 56fd06d..a604484 100644 --- a/signalling/src/features/room/service.rs +++ b/signalling/src/features/room/service.rs @@ -3,7 +3,6 @@ use crate::core::dtos::room::create_room_dto::CreateRoomDto; use crate::core::dtos::room::update_room_dto::UpdateRoomDto; use crate::core::entities::models::{ MembersRoleEnum, NewMember, NewParticipant, NewRoom, ParticipantsStatusEnum, RoomStatusEnum, - RoomType, }; use crate::core::types::errors::room_error::RoomError; use crate::core::types::responses::room_response::{ParticipantResponse, RoomResponse}; @@ -113,12 +112,12 @@ impl RoomServi let password = data.password.clone(); async move { match password { - Some(pwd) => tokio::task::spawn_blocking(move || hash_password(&pwd)) + Some(pwd) => tokio::task::spawn_blocking(move || Some(hash_password(&pwd))) .await .map_err(|_| { RoomError::UnexpectedError("Failed to hash password".into()) }), - None => Ok("".to_string()), + None => Ok(None), } } }, @@ -129,13 +128,15 @@ impl RoomServi let new_room = NewRoom { title: &data.title, - password: &password_hashed, + password: password_hashed.as_deref(), code: &code, status: RoomStatusEnum::Active.into(), created_at: now, updated_at: now, latest_message_created_at: now, - type_: RoomType::Conferencing.into(), + type_: data.room_type as i16, + capacity: data.capacity, + streaming_protocol: Some(data.streaming_protocol as i16), }; self.room_repository @@ -177,6 +178,10 @@ impl RoomServi room.avatar = Some(avatar); } + if let Some(capacity) = update_room_dto.capacity { + room.capacity = Some(capacity); + } + let updated_room = self.room_repository.update_room(room).await?; Ok(updated_room) @@ -258,6 +263,13 @@ impl RoomServi .iter() .any(|member| member.member.user_id == user_id); + // Enforce capacity + if let Some(capacity) = room.room.capacity + && room.members.len() as i32 >= capacity + { + return Err(RoomError::RoomIsFull); + } + if !is_member { let is_password_correct = match room.room.password.as_ref() { Some(hash_password) => match password { @@ -457,7 +469,7 @@ mod tests { use crate::core::dtos::room::create_room_dto::CreateRoomDto; use crate::core::dtos::room::update_room_dto::UpdateRoomDto; use crate::core::entities::models::{ - Member, Message, Participant, Room, StreamingProtocol, User, + Member, Message, Participant, Room, RoomType, StreamingProtocol, User, }; use crate::core::types::responses::message_response::MessageResponse; use crate::core::types::responses::room_response::{ @@ -546,6 +558,8 @@ mod tests { deleted_at: None, latest_message_id: Some(1), type_: RoomType::Conferencing as i16, + capacity: Some(10), + streaming_protocol: Some(StreamingProtocol::SFU as i16), }, members: vec![MemberResponse { member: sample_member(1, owner_id, id, MembersRoleEnum::Owner as i16), @@ -560,6 +574,7 @@ mod tests { created_by: Some(sample_user(owner_id)), room: None, }), + is_protected: None, } } @@ -569,7 +584,7 @@ mod tests { password: None, room_type: RoomType::Conferencing, streaming_protocol: StreamingProtocol::SFU, - capacity: None, + capacity: Some(10), } } @@ -578,9 +593,7 @@ mod tests { title: Some("Updated Room".to_string()), password: Some("newpass".to_string()), avatar: Some("avatar.png".to_string()), - room_type: None, - streaming_protocol: None, - capacity: None, + capacity: Some(10), } } @@ -787,6 +800,12 @@ mod tests { assert!(result.is_ok()); let room = result.unwrap(); assert_eq!(room.room.title, "Test Room"); + assert_eq!(room.room.type_, RoomType::Conferencing as i16); + assert_eq!( + room.room.streaming_protocol, + Some(StreamingProtocol::SFU as i16) + ); + assert_eq!(room.room.capacity, Some(10)); } #[tokio::test] @@ -826,6 +845,12 @@ mod tests { assert!(result.is_ok()); let updated = result.unwrap(); assert_eq!(updated.room.title, "Updated Room"); + assert_eq!(updated.room.type_, RoomType::Conferencing as i16); + assert_eq!( + updated.room.streaming_protocol, + Some(StreamingProtocol::SFU as i16) + ); + assert_eq!(updated.room.capacity, Some(10)); } #[tokio::test] @@ -969,6 +994,36 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn test_join_room_full() { + let mut room = sample_room(1, 1); + // Set capacity to 3 (owner + 2 attendees) + room.room.capacity = Some(3); + room.members.push(MemberResponse { + member: sample_member(2, 2, 1, MembersRoleEnum::Attendee as i16), + user: Some(sample_user(2)), + }); + room.members.push(MemberResponse { + member: sample_member(3, 3, 1, MembersRoleEnum::Attendee as i16), + user: Some(sample_user(3)), + }); + // Now the room is full (3/3) + let rooms = Arc::new(Mutex::new(vec![room.clone()])); + let users = Arc::new(Mutex::new(vec![sample_user(4)])); + let room_repo = MockRoomRepository { + rooms: rooms.clone(), + fail: false, + }; + let user_repo = MockUserRepository { + users: users.clone(), + fail: false, + }; + let service = RoomServiceImpl::new(room_repo, user_repo); + let result = service.join_room(4, 1, None).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), RoomError::RoomIsFull); + } + #[tokio::test] async fn test_add_member_success() { let room = sample_room(1, 1);