diff --git a/.clippy.toml b/.clippy.toml index 1f12a39c9..bc95b1ac0 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -8,11 +8,13 @@ doc-valid-idents = [ "IPv4", "IPv6", "JavaScript", "NaN", "NaNs", "OAuth", "OpenGL", "OpenSSH", "OpenSSL", "OpenStreetMap", "TrueType", "iOS", "macOS", "TeX", "LaTeX", "BibTeX", "BibLaTeX", "MinGW", - "WebRTC", "WebSocket", + "gRPC", "MediaDeviceKind", "MediaStream", "MediaStreamConstraints", "MediaStreamTrack", - "RTCConfiguration", "RTCIceCandidate", "RTCIceServer", + "RTCConfiguration", + "RTCIceCandidate", "RTCIceCandidateInit", "RTCIceServer", "RTCPeerConnection", "RTCPeerConnectionIceEvent", "RTCRtpTransceiver", "RTCRtpTransceiverDirection", - "RTCSdpType", "RTCIceCandidateInit" + "RTCSdpType", + "WebRTC", "WebSocket", ] diff --git a/.travis.yml b/.travis.yml index c144f33da..8149673ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,7 +36,7 @@ jobs: stage: check rust: nightly cache: false - before_script: rustup component add rustfmt + before_script: rustup component add rustfmt --toolchain nightly-x86_64-unknown-linux-gnu script: make fmt check=yes - name: medea-jason (beta) diff --git a/CHANGELOG.md b/CHANGELOG.md index d08fd6095..73b87f9c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,19 +11,32 @@ All user visible changes to this project will be documented in this file. This p [Milestone](/../../milestone/2) | [Roadmap](/../../issues/27) +### BC Breaks + +- Configuration: + - Rename `[server]` section of Client API HTTP server as `[server.client.http]` ([#33]). + ### Added - Control API: - - Support for static Сontrol API specs ([#28]). + - Support for static Сontrol API specs ([#28]); + - Dynamic Control API exposed via gRPC ([#33]): + - `Create` method for `Room`, `Member`, `Endpoint`; + - `Get` method for `Room`, `Member`, `Endpoint`; + - `Delete` method for `Room`, `Member`, `Endpoint`. - Signalling: - Dynamic `Peer`s creation when client connects ([#28]); - Auto-removing `Peer`s when `Member` disconnects ([#28]); - Filter `SetIceCandidate` messages without `candidate` ([#50](/../../pull/50)); - Send reason of closing WebSocket connection as [Close](https://tools.ietf.org/html/rfc4566#section-5.14) frame's description ([#58](/../../pull/58)). +- Configuration: + - `[server.control.grpc]` section to configure Control API gRPC server ([#33]); + - `server.client.http.public_url` option to configure public URL of Client API HTTP server ([#33]). - Testing: - E2E tests for signalling ([#28]). [#28]: /../../pull/28 +[#33]: /../../pull/33 diff --git a/Cargo.lock b/Cargo.lock index c496d0fbe..2dc55797a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ dependencies = [ "hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "http 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -305,7 +305,7 @@ name = "actix-web-codegen" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -361,13 +361,13 @@ name = "atty" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "autocfg" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -394,22 +394,22 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.38" +version = "0.3.40" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "backtrace-sys" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -462,7 +462,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -471,7 +471,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "brotli-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -495,11 +495,10 @@ dependencies = [ [[package]] name = "c2-chacha" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -517,7 +516,7 @@ name = "chrono" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", @@ -572,7 +571,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -653,12 +652,67 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "darling" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "darling_core 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "darling_macro 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "darling_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "darling_macro" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "darling_core 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "derive-new" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "derive_builder" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "darling 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "derive_builder_core 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "derive_builder_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "darling 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -697,7 +751,7 @@ name = "dirs" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -762,7 +816,7 @@ name = "failure" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace 0.3.38 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -771,7 +825,7 @@ name = "failure_derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "synstructure 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -784,7 +838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "miniz-sys 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "miniz_oxide 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -899,11 +953,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "getrandom" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -914,7 +968,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "grpcio-sys 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -937,7 +991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)", "cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -951,7 +1005,7 @@ dependencies = [ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "http 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -981,7 +1035,7 @@ name = "hostname" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1017,6 +1071,11 @@ dependencies = [ "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "idna" version = "0.1.5" @@ -1039,15 +1098,18 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "iovec" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1068,10 +1130,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "js-sys" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1100,7 +1162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libc" -version = "0.2.64" +version = "0.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1174,14 +1236,17 @@ dependencies = [ "bb8-redis 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "config 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "derive_builder 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "grpcio 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "humantime-serde 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "medea-client-api-proto 0.1.1-dev", + "medea-control-api-proto 0.1.0-dev", "medea-macro 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "redis 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1197,6 +1262,7 @@ dependencies = [ "slog-scope 4.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "slog-stdlog 4.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "smart-default 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-signal 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1231,7 +1297,7 @@ dependencies = [ "downcast 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "fragile 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.29 (registry+https://github.com/rust-lang/crates.io-index)", "macro-attr 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "medea-client-api-proto 0.1.1-dev", "medea-macro 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1240,10 +1306,10 @@ dependencies = [ "predicates-tree 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-futures 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-test 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-test 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.29 (registry+https://github.com/rust-lang/crates.io-index)", "wee_alloc 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1252,7 +1318,7 @@ name = "medea-macro" version = "0.1.0" dependencies = [ "Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1297,7 +1363,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1317,7 +1383,7 @@ dependencies = [ "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1331,7 +1397,7 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1366,7 +1432,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1377,7 +1443,7 @@ version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1413,7 +1479,7 @@ name = "num-integer" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1430,7 +1496,7 @@ name = "num-traits" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1438,7 +1504,7 @@ name = "num_cpus" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1468,7 +1534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1483,7 +1549,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1512,7 +1578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "ppv-lite86" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1551,7 +1617,7 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1609,7 +1675,7 @@ name = "quote" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1617,7 +1683,7 @@ name = "rand" version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1627,7 +1693,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1638,8 +1704,8 @@ name = "rand" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1656,8 +1722,8 @@ name = "rand" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1668,7 +1734,7 @@ name = "rand_chacha" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1677,7 +1743,7 @@ name = "rand_chacha" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1699,7 +1765,7 @@ name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1731,7 +1797,7 @@ name = "rand_jitter" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1743,7 +1809,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1754,7 +1820,7 @@ name = "rand_pcg" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1859,7 +1925,7 @@ version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1959,7 +2025,7 @@ name = "serde_derive" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2031,7 +2097,7 @@ name = "signal-hook" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "signal-hook-registry 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2041,7 +2107,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "arc-swap 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2143,7 +2209,7 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2161,6 +2227,11 @@ dependencies = [ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "strsim" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "syn" version = "0.15.44" @@ -2176,7 +2247,7 @@ name = "syn" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2186,7 +2257,7 @@ name = "synstructure" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2203,7 +2274,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2249,7 +2320,7 @@ name = "time" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2349,7 +2420,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", "signal-hook 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2430,7 +2501,7 @@ dependencies = [ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2578,113 +2649,113 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "wasm-bindgen" -version = "0.2.51" +version = "0.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.51" +version = "0.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.29 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.29 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.51" +version = "0.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro-support 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro-support 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.51" +version = "0.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-backend 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.51" +version = "0.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "wasm-bindgen-test" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.29 (registry+https://github.com/rust-lang/crates.io-index)", "scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-futures 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-test-macro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-test-macro 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "wasm-bindgen-webidl" -version = "0.2.51" +version = "0.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-backend 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", "weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "web-sys" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.29 (registry+https://github.com/rust-lang/crates.io-index)", "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-webidl 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-webidl 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2693,7 +2764,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "memory_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2816,10 +2887,10 @@ dependencies = [ "checksum arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" "checksum ascii 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" -"checksum autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" +"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" "checksum awc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "364537de6ac9f996780f9dd097d6c4ca7c91dd5735153e9fb545037479becd10" -"checksum backtrace 0.3.38 (registry+https://github.com/rust-lang/crates.io-index)" = "690a62be8920ccf773ee00ef0968649b0e724cda8bd5b12286302b4ae955fdf5" -"checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" +"checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea" +"checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" "checksum bb8 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0ac73ee3406f475415bba792942016291b87bac08839c38a032de56085a73b2c" "checksum bb8-redis 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39945a33c03edfba8c353d330b921961e2137d4fc4215331c1b2397f32acff80" @@ -2830,7 +2901,7 @@ dependencies = [ "checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -"checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101" +"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" "checksum cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)" = "0213d356d3c4ea2c18c40b037c3be23cd639825c18f25ee670ac7813beeef99c" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e8493056968583b0193c1bb04d6f7684586f3726992d6c573261941a895dbd68" @@ -2848,7 +2919,12 @@ dependencies = [ "checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9" "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +"checksum darling 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe629a532efad5526454efb0700f86d5ad7ff001acb37e431c8bf017a432a8e" +"checksum darling_core 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ee54512bec54b41cf2337a22ddfadb53c7d4c738494dc2a186d7b037ad683b85" +"checksum darling_macro 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0cd3e432e52c0810b72898296a69d66b1d78d1517dff6cde7a130557a55a62c1" "checksum derive-new 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)" = "71f31892cd5c62e414316f2963c5689242c43d8e7bbcaaeca97e5e28c95d91d9" +"checksum derive_builder 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfbc80e9c3b77201f32c9541b13d9678388782272554af7e65103310265462e6" +"checksum derive_builder_core 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8b68a4fe580fdda1bf3860b2360b9020696d42fc58f2343cf4be2faedf74c5a9" "checksum derive_more 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6d944ac6003ed268757ef1ee686753b57efc5fcf0ebe7b64c9fc81e7e32ff839" "checksum derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a141330240c921ec6d074a3e188a7c7ef95668bb95e7d44fa0e5778ec2a7afe" "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" @@ -2878,7 +2954,7 @@ dependencies = [ "checksum futures-sink-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "86f148ef6b69f75bb610d4f9a2336d4fc88c4b5b67129d1a340dd0fd362efeec" "checksum futures-util-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "5ce968633c17e5f97936bd2797b6e38fb56cf16a7422319f7ec2e30d3c470e8d" "checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" -"checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571" +"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" "checksum grpcio 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "cf45c98940795d83354880f073f3a2a2a995f50d3a3e43247ea23eca978d6574" "checksum grpcio-compiler 0.5.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd8b3213a332a2865a307e553f43d632bc4a81f0e0f5a90d194dee5b9c02d8a7" "checksum grpcio-sys 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f6a31d8b4769d18e20de167e3c0ccae6b7dd506dfff78d323c2166e76efbe408" @@ -2891,18 +2967,19 @@ dependencies = [ "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum humantime-serde 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8f59e8a805c18bc9ded3f4e596cb5f0157d88a235e875480a7593b5926f95065" +"checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" -"checksum indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3" +"checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" -"checksum js-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)" = "2cc9a97d7cec30128fd8b28a7c1f9df1c001ceb9b441e2b755e24130a6b43c79" +"checksum js-sys 0.3.29 (registry+https://github.com/rust-lang/crates.io-index)" = "5061eb59a5afd4f6ff96dc565963e4e2737b915d070233cb26b88e3f58af41b4" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)" = "74dfca3d9957906e8d1e6a0b641dc9a59848e793f1da2165889fd4f62d10d79c" +"checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" "checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" "checksum lock_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed946d4529956a20f2d63ebe1b69996d5a2137c91913fe3ebbeff957f5bca7ff" @@ -2940,12 +3017,12 @@ dependencies = [ "checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" "checksum pkg-config 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "72d5370d90f49f70bd033c3d75e87fc529fbfff9d6f7cccef07d6170079d91ea" -"checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" +"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" "checksum predicates 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "53e09015b0d3f5a0ec2d4428f7559bb7b3fff341b4e159fedd1d57fac8b939ff" "checksum predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" "checksum predicates-tree 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -"checksum proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90cf5f418035b98e655e9cdb225047638296b862b42411c4e45bb88d700f7fc0" +"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" "checksum protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40361836defdd5871ff7e84096c6f6444af7fc157f8ef1789f54f147687caa20" "checksum protobuf-codegen 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12c6abd78435445fc86898ebbd0521a68438063d4a73e23527b7134e6bf58b4a" "checksum protoc 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3998c4bc0af8ccbd3cc68245ee9f72663c5ae2fb78bc48ff7719aef11562edea" @@ -3016,6 +3093,7 @@ dependencies = [ "checksum socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85" "checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" +"checksum strsim 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "032c03039aae92b350aad2e3779c352e104d919cb192ba2fabbd7b831ce4f0f6" "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" "checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" "checksum synstructure 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f085a5855930c0441ca1288cf044ea4aecf4f43a91668abdb870b4ba546a203" @@ -3056,16 +3134,16 @@ dependencies = [ "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" -"checksum wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "cd34c5ba0d228317ce388e87724633c57edca3e7531feb4e25e35aaa07a656af" -"checksum wasm-bindgen-backend 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "927196b315c23eed2748442ba675a4c54a1a079d90d9bdc5ad16ce31cf90b15b" -"checksum wasm-bindgen-futures 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fb4410bcecc9a1c38c3021d95e2d99536cd6c426e2c424f307a3ff326edcb48" -"checksum wasm-bindgen-macro 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "92c2442bf04d89792816650820c3fb407af8da987a9f10028d5317f5b04c2b4a" -"checksum wasm-bindgen-macro-support 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "9c075d27b7991c68ca0f77fe628c3513e64f8c477d422b859e03f28751b46fc5" -"checksum wasm-bindgen-shared 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "83d61fe986a7af038dd8b5ec660e5849cbd9f38e7492b9404cc48b2b4df731d1" -"checksum wasm-bindgen-test 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2c3d30c1e43ebb4c4835f8163456d16f83dd6c1831424cb22680c680ef5f8ea8" -"checksum wasm-bindgen-test-macro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5f093012630c0c14be061ac7a8d99f82a94e2b1cfd74619fa71090705d2c91be" -"checksum wasm-bindgen-webidl 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "9b979afb0535fe4749906a674082db1211de8aef466331d43232f63accb7c07c" -"checksum web-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)" = "c84440699cd02ca23bed6f045ffb1497bc18a3c2628bd13e2093186faaaacf6b" +"checksum wasm-bindgen 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)" = "637353fd57864c20f1968dc21680fe03985ca3a7ef6a5ce027777513bdecc282" +"checksum wasm-bindgen-backend 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)" = "c85481ca7d1aad8cf40e0140830b2197ce89184a80e54e307b55fd64d78ed63e" +"checksum wasm-bindgen-futures 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6c2c779d3f8b19acb09a8ae1e7f3f6de309819dfb1333b7a0c0e6cc4de26b9d" +"checksum wasm-bindgen-macro 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)" = "9f627667b5f4f8bd923c93107b96907c60e7e8eb2636802499fce468c87e3689" +"checksum wasm-bindgen-macro-support 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)" = "a48f5147b0c049bc306d5b9e53c891056a1fd8c4e7311fffbce233e4f200d45e" +"checksum wasm-bindgen-shared 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)" = "1e272b0d31b78cdcaf5ad440d28276546d99b059a953e5afb387aefce66c3c5a" +"checksum wasm-bindgen-test 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b255415d7471abc1633c00141d9ee90dfc306932dbdbc319bd8e23ba2eb7b01a" +"checksum wasm-bindgen-test-macro 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93ae97a6a9f9a7fcb8c155b93048e60869e1dd17e5714b62f1e63ef0aa4539c5" +"checksum wasm-bindgen-webidl 0.2.52 (registry+https://github.com/rust-lang/crates.io-index)" = "6965845db6189148d8b26387aee0bbf1c84f3da78f57ac543f364fc8ff7ab6e9" +"checksum web-sys 0.3.29 (registry+https://github.com/rust-lang/crates.io-index)" = "0a8b4b06314fd2ce36977e9487607ccff4030779129813f89d0e618710910146" "checksum wee_alloc 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" "checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" "checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6" diff --git a/Cargo.toml b/Cargo.toml index 92cafe0e5..28c4d94da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,14 +32,16 @@ bb8 = "0.3" bb8-redis = "0.3" chrono = "0.4" config = "0.9" -dotenv = "0.14" derive_more = "0.15" +dotenv = "0.14" failure = "0.1" futures = "0.1" -humantime = "1.2" +grpcio = { version = "0.4", features = ["openssl"] } humantime-serde = "0.1" medea-client-api-proto = { path = "proto/client-api", features = ["medea"] } +medea-control-api-proto = { path = "proto/control-api" } medea-macro = "0.1" +protobuf = "2.7" rand = "0.7" redis = "0.11" rust-crypto = "0.2" @@ -63,5 +65,7 @@ actix-codec = "0.1" actix-http = "0.2" actix-http-test = "0.2" awc = "0.2" +derive_builder = "0.8" serial_test = "0.2" serial_test_derive = "0.2" +tempfile = "3.1" diff --git a/Dockerfile b/Dockerfile index 410303c66..37435f4f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,11 @@ RUN mkdir -p /out/etc/ \ && echo 'nobody:x:65534:65534:nobody:/:' > /out/etc/passwd \ && echo 'nobody:x:65534:' > /out/etc/group +# Install required system packages for building. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + cmake + # Prepare Cargo workspace for building dependencies only. COPY crates/medea-macro/Cargo.toml /app/crates/medea-macro/ COPY proto/client-api/Cargo.toml /app/proto/client-api/ diff --git a/config.toml b/config.toml index 977635414..d874768ac 100644 --- a/config.toml +++ b/config.toml @@ -1,20 +1,44 @@ -[server] -# IP address to bind HTTP server to. +[server.client.http] +# Client API HTTP server's public URL. +# It's assumed that HTTP server can be reached via this URL externally. # +# Env var: MEDEA_SERVER__CLIENT__HTTP__PUBLIC_URL +# Default: +# public_url = "ws://127.0.0.1:8080/ws" + +# IP address to bind Client API HTTP server to. +# +# Env var: MEDEA_SERVER__CLIENT__HTTP__BIND_IP # Default: # bind_ip = "0.0.0.0" -# Port to bind HTTP server to. +# Port to bind Client API HTTP server to. # +# Env var: MEDEA_SERVER__CLIENT__HTTP__BIND_PORT # Default: # bind_port = 8080 +[server.control.grpc] +# IP address to bind Control API gRPC server to. +# +# Env var: MEDEA_SERVER__CONTROL__GRPC__BIND_IP +# Default: +# bind_ip = "0.0.0.0" + +# Port to bind Control API gRPC server to. +# +# Env var: MEDEA_SERVER__CONTROL__GRPC__BIND_PORT +# Default: +# bind_port = 6565 + + [control] # Path to directory with static Сontrol API specs. # +# Env var: MEDEA_CONTROL__STATIC_SPECS_DIR # Default: # static_specs_dir = "specs/" @@ -25,12 +49,14 @@ # Duration, after which remote RPC client will be considered idle if no # heartbeat messages received. # +# Env var: MEDEA_RPC__IDLE_TIMEOUT # Default: # idle_timeout = "10s" -# Duration, after which the server deletes the client session if -# the remote RPC client does not reconnect after it is idle. +# Duration, after which the server deletes client session if remote RPC client +# does not reconnect after it is idle. # +# Env var: MEDEA_RPC__RECONNECT_TIMEOUT # Default: # reconnect_timeout = "10s" @@ -40,47 +66,56 @@ [turn] # Turn server host. # +# Env var: MEDEA_TURN__HOST # Default: # host = "localhost" # Turn server port. # +# Env var: MEDEA_TURN__PORT # Default: # port = 3478 # Static user on Turn server. # +# Env var: MEDEA_TURN__USER # Default: # user = "USER" # Static user password on Turn server. # +# Env var: MEDEA_TURN__PASS # Default: # pass = "PASS" [turn.db.redis] # Host of Coturn's Redis database. # +# Env var: MEDEA_TURN__DB__REDIS__HOST # Default: # host = "127.0.0.1" # Port of Coturn's Redis database for client connections. # +# Env var: MEDEA_TURN__DB__REDIS__PORT # Default: # port = 6379 # Password to connect to Coturn's Redis database with. # +# Env var: MEDEA_TURN__DB__REDIS__PASS # Default: # pass = "turn" # Number of Coturn's database in Redis. # +# Env var: MEDEA_TURN__DB__REDIS__DB_NUMBER # Default: # db_number = 0 # Timeout for establishing connection with Coturn's Redis database. # +# Env var: MEDEA_TURN__DB__REDIS__CONNECTION_TIMEOUT # Default: # connection_timeout = "5s" @@ -90,6 +125,7 @@ [log] # Maximum allowed level of application log entries. # +# Env var: MEDEA_LOG__LEVEL # Possible values: # "OFF", "CRITICAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE" # @@ -102,5 +138,6 @@ [shutdown] # Maximum duration given to shutdown the whole application gracefully. # +# Env var: MEDEA_SHUTDOWN__TIMEOUT # Default: # timeout = "5s" diff --git a/jason/Dockerfile b/jason/Dockerfile index 2b41211bc..35cb02f4f 100644 --- a/jason/Dockerfile +++ b/jason/Dockerfile @@ -8,7 +8,7 @@ # # https://hub.docker.com/_/rust -FROM rust:stretch AS dist +FROM alexlapa/rust-beta:1.39-beta.5 AS dist RUN cargo install wasm-pack \ && rustup target add wasm32-unknown-unknown diff --git a/jason/demo/chart/medea-demo/Chart.yaml b/jason/demo/chart/medea-demo/Chart.yaml index 23dffdd0a..2e1caae1c 100644 --- a/jason/demo/chart/medea-demo/Chart.yaml +++ b/jason/demo/chart/medea-demo/Chart.yaml @@ -1,4 +1,4 @@ apiVersion: v1 name: medea-demo -version: 0.1.1 +version: 0.2.0 appVersion: 0.1.0 diff --git a/jason/demo/chart/medea-demo/conf/nginx.vh.conf b/jason/demo/chart/medea-demo/conf/nginx.vh.conf index 2d096c38f..065fbd39e 100644 --- a/jason/demo/chart/medea-demo/conf/nginx.vh.conf +++ b/jason/demo/chart/medea-demo/conf/nginx.vh.conf @@ -11,7 +11,10 @@ server { } location ^~ /ws/ { - proxy_pass http://127.0.0.1:8080/; + proxy_pass http://127.0.0.1:8080/ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; } # Disable unnecessary access logs. diff --git a/jason/demo/chart/medea-demo/templates/deployment.server.yaml b/jason/demo/chart/medea-demo/templates/deployment.server.yaml index 91ef757e0..d7b45024f 100644 --- a/jason/demo/chart/medea-demo/templates/deployment.server.yaml +++ b/jason/demo/chart/medea-demo/templates/deployment.server.yaml @@ -69,7 +69,7 @@ spec: name: {{ template "medea-demo.fullname" . }}.server.cred ports: - name: http - containerPort: {{ .Values.server.conf.server.bind_port }} + containerPort: {{ .Values.server.conf.server.client.http.bind_port }} volumeMounts: - name: conf subPath: medea.toml @@ -80,11 +80,11 @@ spec: {{- end }} livenessProbe: tcpSocket: - port: {{ .Values.server.conf.server.bind_port }} + port: {{ .Values.server.conf.server.client.http.bind_port }} initialDelaySeconds: 3 readinessProbe: tcpSocket: - port: {{ .Values.server.conf.server.bind_port }} + port: {{ .Values.server.conf.server.client.http.bind_port }} initialDelaySeconds: 5 - name: coturn image: "{{ $coturn.image.repository }}:{{ $coturn.image.tag }}" diff --git a/jason/demo/chart/medea-demo/values.yaml b/jason/demo/chart/medea-demo/values.yaml index ce1628bc2..a7dbb200f 100644 --- a/jason/demo/chart/medea-demo/values.yaml +++ b/jason/demo/chart/medea-demo/values.yaml @@ -16,7 +16,10 @@ server: # configuration will have no effect. conf: server: - bind_port: 8080 + client: + http: + public_url: ws://127.0.0.1:8080/ws + bind_port: 8080 turn: user: USER pass: PASS diff --git a/jason/demo/minikube.vals.yaml b/jason/demo/minikube.vals.yaml index 2db89efdf..94b805556 100644 --- a/jason/demo/minikube.vals.yaml +++ b/jason/demo/minikube.vals.yaml @@ -8,6 +8,11 @@ server: tag: dev pullPolicy: IfNotPresent conf: + server: + client: + http: + public_url: wss://medea-demo.test/ws + turn: host: medea-demo.test diff --git a/jason/demo/staging.vals.yaml b/jason/demo/staging.vals.yaml index 3589ac3f3..86fbb2f62 100644 --- a/jason/demo/staging.vals.yaml +++ b/jason/demo/staging.vals.yaml @@ -9,7 +9,10 @@ server: pullPolicy: Always conf: server: - bind_port: 9980 + client: + http: + public_url: wss://demo.medea.stg.t11913.org/ws + bind_port: 9980 turn: host: demo.medea.stg.t11913.org pass: changeme diff --git a/jason/src/api/room.rs b/jason/src/api/room.rs index 4905e1f08..1d994ae6d 100644 --- a/jason/src/api/room.rs +++ b/jason/src/api/room.rs @@ -286,12 +286,12 @@ impl EventHandler for InnerRoom { }; Result::<_, WasmErr>::Ok(()) } - .then(|result| { - if let Err(err) = result { - err.log_err(); - }; - future::ready(()) - }), + .then(|result| { + if let Err(err) = result { + err.log_err(); + }; + future::ready(()) + }), ); } diff --git a/jason/src/media/manager.rs b/jason/src/media/manager.rs index d9d1f2d0b..06a926ff7 100644 --- a/jason/src/media/manager.rs +++ b/jason/src/media/manager.rs @@ -194,12 +194,12 @@ impl MediaManager { Ok(stream) } - .await - .map(Rc::new) - .map_err(move |err: WasmErr| { - inner.on_local_stream.call2(err.clone()); - err - }) + .await + .map(Rc::new) + .map_err(move |err: WasmErr| { + inner.on_local_stream.call2(err.clone()); + err + }) } /// Obtains [MediaStream][1] basing on provided [`MediaStreamConstraints`]. diff --git a/proto/client-api/src/lib.rs b/proto/client-api/src/lib.rs index acb15fab5..1f02e3f9d 100644 --- a/proto/client-api/src/lib.rs +++ b/proto/client-api/src/lib.rs @@ -121,6 +121,9 @@ pub enum CloseReason { /// /// This close reason is similar to 500 HTTP status code. InternalError, + + /// Client was evicted on the server side. + Evicted, } /// Description which is sent in [Close] WebSocket frame from Media Server diff --git a/proto/control-api/src/grpc/api.proto b/proto/control-api/src/grpc/api.proto index 23308c004..4bdf05bf8 100644 --- a/proto/control-api/src/grpc/api.proto +++ b/proto/control-api/src/grpc/api.proto @@ -23,10 +23,10 @@ service ControlApi { rpc Get (IdRequest) returns (GetResponse); } -// Request of creating new Element with a given ID. +// Request of creating new Element with in element with a given FID (full ID). message CreateRequest { - // ID of the Element to be created.. - string id = 1; + // FID (full ID) of the Element in which the provided Element will be created. + string parent_fid = 1; // Spec of the created Element. oneof el { Member member = 2; @@ -36,10 +36,10 @@ message CreateRequest { } } -// Request with many Elements IDs. +// Request with many FIDs (full IDs) of Elements. message IdRequest { - // List of Elements IDs. - repeated string id = 1; + // List of Elements FIDs. + repeated string fid = 1; } // Response which doesn't return anything on successful result, @@ -112,8 +112,10 @@ message Element { // Media element which represents a single space where multiple Members can // interact with each other. message Room { + // ID of this Room. + string id = 1; // Pipeline of this Room. - map pipeline = 1; + map pipeline = 2; // Elements which Room's pipeline can contain. message Element { @@ -128,16 +130,18 @@ message Room { // Media element which represents a client authorized to participate // in a some bigger media pipeline. message Member { + // ID of this Member. + string id = 1; // Callback which fires when the Member establishes persistent connection // with a media server via Client API. - string on_join = 1; + string on_join = 2; // Callback which fires when the Member finishes persistent connection // with a media server via Client API. - string on_leave = 2; + string on_leave = 3; // Credentials of the Member to authorize via Client API with. - string credentials = 3; + string credentials = 4; // Pipeline of this Member. - map pipeline = 4; + map pipeline = 5; // Elements which Member's pipeline can contain. message Element { @@ -151,12 +155,14 @@ message Member { // Media element which is able to receive media data from a client via WebRTC // (allows to publish media data). message WebRtcPublishEndpoint { + // ID of this WebRtcPublishEndpoint. + string id = 1; // P2P mode for this element. - P2P p2p = 1; + P2P p2p = 2; // Callback which fires when a client starts publishing media data. - string on_start = 2; + string on_start = 3; // Callback which fires when a client stops publishing media data. - string on_stop = 3; + string on_stop = 4; // P2P mode of WebRTC interaction. enum P2P { @@ -172,12 +178,14 @@ message WebRtcPublishEndpoint { // Media element which is able to play media data for a client via WebRTC. message WebRtcPlayEndpoint { + // ID of this WebRtcPlayEndpoint. + string id = 1; // The source to get media data from. - string src = 1; + string src = 2; // Callback which fires when a client starts playing media data // from the source. - string on_start = 2; + string on_start = 3; // Callback which fires when a client stops playing media data // from the source. - string on_stop = 3; + string on_stop = 4; } diff --git a/proto/control-api/src/grpc/api.rs b/proto/control-api/src/grpc/api.rs index 1988f92d5..ba225e60a 100644 --- a/proto/control-api/src/grpc/api.rs +++ b/proto/control-api/src/grpc/api.rs @@ -29,7 +29,7 @@ const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_8_1; #[derive(PartialEq,Clone,Default)] pub struct CreateRequest { // message fields - pub id: ::std::string::String, + pub parent_fid: ::std::string::String, // message oneof groups pub el: ::std::option::Option, // special fields @@ -56,30 +56,30 @@ impl CreateRequest { ::std::default::Default::default() } - // string id = 1; + // string parent_fid = 1; - pub fn get_id(&self) -> &str { - &self.id + pub fn get_parent_fid(&self) -> &str { + &self.parent_fid } - pub fn clear_id(&mut self) { - self.id.clear(); + pub fn clear_parent_fid(&mut self) { + self.parent_fid.clear(); } // Param is passed by value, moved - pub fn set_id(&mut self, v: ::std::string::String) { - self.id = v; + pub fn set_parent_fid(&mut self, v: ::std::string::String) { + self.parent_fid = v; } // Mutable pointer to the field. // If field is not initialized, it is initialized with default value first. - pub fn mut_id(&mut self) -> &mut ::std::string::String { - &mut self.id + pub fn mut_parent_fid(&mut self) -> &mut ::std::string::String { + &mut self.parent_fid } // Take field - pub fn take_id(&mut self) -> ::std::string::String { - ::std::mem::replace(&mut self.id, ::std::string::String::new()) + pub fn take_parent_fid(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.parent_fid, ::std::string::String::new()) } // .medea.Member member = 2; @@ -309,7 +309,7 @@ impl ::protobuf::Message for CreateRequest { let (field_number, wire_type) = is.read_tag_unpack()?; match field_number { 1 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?; + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.parent_fid)?; }, 2 => { if wire_type != ::protobuf::wire_format::WireTypeLengthDelimited { @@ -347,8 +347,8 @@ impl ::protobuf::Message for CreateRequest { #[allow(unused_variables)] fn compute_size(&self) -> u32 { let mut my_size = 0; - if !self.id.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.id); + if !self.parent_fid.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.parent_fid); } if let ::std::option::Option::Some(ref v) = self.el { match v { @@ -376,8 +376,8 @@ impl ::protobuf::Message for CreateRequest { } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { - if !self.id.is_empty() { - os.write_string(1, &self.id)?; + if !self.parent_fid.is_empty() { + os.write_string(1, &self.parent_fid)?; } if let ::std::option::Option::Some(ref v) = self.el { match v { @@ -446,9 +446,9 @@ impl ::protobuf::Message for CreateRequest { descriptor.get(|| { let mut fields = ::std::vec::Vec::new(); fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( - "id", - |m: &CreateRequest| { &m.id }, - |m: &mut CreateRequest| { &mut m.id }, + "parent_fid", + |m: &CreateRequest| { &m.parent_fid }, + |m: &mut CreateRequest| { &mut m.parent_fid }, )); fields.push(::protobuf::reflect::accessor::make_singular_message_accessor::<_, Member>( "member", @@ -492,7 +492,7 @@ impl ::protobuf::Message for CreateRequest { impl ::protobuf::Clear for CreateRequest { fn clear(&mut self) { - self.id.clear(); + self.parent_fid.clear(); self.el = ::std::option::Option::None; self.el = ::std::option::Option::None; self.el = ::std::option::Option::None; @@ -516,7 +516,7 @@ impl ::protobuf::reflect::ProtobufValue for CreateRequest { #[derive(PartialEq,Clone,Default)] pub struct IdRequest { // message fields - pub id: ::protobuf::RepeatedField<::std::string::String>, + pub fid: ::protobuf::RepeatedField<::std::string::String>, // special fields pub unknown_fields: ::protobuf::UnknownFields, pub cached_size: ::protobuf::CachedSize, @@ -533,29 +533,29 @@ impl IdRequest { ::std::default::Default::default() } - // repeated string id = 1; + // repeated string fid = 1; - pub fn get_id(&self) -> &[::std::string::String] { - &self.id + pub fn get_fid(&self) -> &[::std::string::String] { + &self.fid } - pub fn clear_id(&mut self) { - self.id.clear(); + pub fn clear_fid(&mut self) { + self.fid.clear(); } // Param is passed by value, moved - pub fn set_id(&mut self, v: ::protobuf::RepeatedField<::std::string::String>) { - self.id = v; + pub fn set_fid(&mut self, v: ::protobuf::RepeatedField<::std::string::String>) { + self.fid = v; } // Mutable pointer to the field. - pub fn mut_id(&mut self) -> &mut ::protobuf::RepeatedField<::std::string::String> { - &mut self.id + pub fn mut_fid(&mut self) -> &mut ::protobuf::RepeatedField<::std::string::String> { + &mut self.fid } // Take field - pub fn take_id(&mut self) -> ::protobuf::RepeatedField<::std::string::String> { - ::std::mem::replace(&mut self.id, ::protobuf::RepeatedField::new()) + pub fn take_fid(&mut self) -> ::protobuf::RepeatedField<::std::string::String> { + ::std::mem::replace(&mut self.fid, ::protobuf::RepeatedField::new()) } } @@ -569,7 +569,7 @@ impl ::protobuf::Message for IdRequest { let (field_number, wire_type) = is.read_tag_unpack()?; match field_number { 1 => { - ::protobuf::rt::read_repeated_string_into(wire_type, is, &mut self.id)?; + ::protobuf::rt::read_repeated_string_into(wire_type, is, &mut self.fid)?; }, _ => { ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; @@ -583,7 +583,7 @@ impl ::protobuf::Message for IdRequest { #[allow(unused_variables)] fn compute_size(&self) -> u32 { let mut my_size = 0; - for value in &self.id { + for value in &self.fid { my_size += ::protobuf::rt::string_size(1, &value); }; my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); @@ -592,7 +592,7 @@ impl ::protobuf::Message for IdRequest { } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { - for v in &self.id { + for v in &self.fid { os.write_string(1, &v)?; }; os.write_unknown_fields(self.get_unknown_fields())?; @@ -638,9 +638,9 @@ impl ::protobuf::Message for IdRequest { descriptor.get(|| { let mut fields = ::std::vec::Vec::new(); fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( - "id", - |m: &IdRequest| { &m.id }, - |m: &mut IdRequest| { &mut m.id }, + "fid", + |m: &IdRequest| { &m.fid }, + |m: &mut IdRequest| { &mut m.fid }, )); ::protobuf::reflect::MessageDescriptor::new::( "IdRequest", @@ -664,7 +664,7 @@ impl ::protobuf::Message for IdRequest { impl ::protobuf::Clear for IdRequest { fn clear(&mut self) { - self.id.clear(); + self.fid.clear(); self.unknown_fields.clear(); } } @@ -2042,6 +2042,7 @@ impl ::protobuf::reflect::ProtobufValue for Element { #[derive(PartialEq,Clone,Default)] pub struct Room { // message fields + pub id: ::std::string::String, pub pipeline: ::std::collections::HashMap<::std::string::String, Room_Element>, // special fields pub unknown_fields: ::protobuf::UnknownFields, @@ -2059,7 +2060,33 @@ impl Room { ::std::default::Default::default() } - // repeated .medea.Room.PipelineEntry pipeline = 1; + // string id = 1; + + + pub fn get_id(&self) -> &str { + &self.id + } + pub fn clear_id(&mut self) { + self.id.clear(); + } + + // Param is passed by value, moved + pub fn set_id(&mut self, v: ::std::string::String) { + self.id = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_id(&mut self) -> &mut ::std::string::String { + &mut self.id + } + + // Take field + pub fn take_id(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.id, ::std::string::String::new()) + } + + // repeated .medea.Room.PipelineEntry pipeline = 2; pub fn get_pipeline(&self) -> &::std::collections::HashMap<::std::string::String, Room_Element> { @@ -2095,6 +2122,9 @@ impl ::protobuf::Message for Room { let (field_number, wire_type) = is.read_tag_unpack()?; match field_number { 1 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?; + }, + 2 => { ::protobuf::rt::read_map_into::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(wire_type, is, &mut self.pipeline)?; }, _ => { @@ -2109,14 +2139,20 @@ impl ::protobuf::Message for Room { #[allow(unused_variables)] fn compute_size(&self) -> u32 { let mut my_size = 0; - my_size += ::protobuf::rt::compute_map_size::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(1, &self.pipeline); + if !self.id.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.id); + } + my_size += ::protobuf::rt::compute_map_size::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(2, &self.pipeline); my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); self.cached_size.set(my_size); my_size } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { - ::protobuf::rt::write_map_with_cached_sizes::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(1, &self.pipeline, os)?; + if !self.id.is_empty() { + os.write_string(1, &self.id)?; + } + ::protobuf::rt::write_map_with_cached_sizes::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(2, &self.pipeline, os)?; os.write_unknown_fields(self.get_unknown_fields())?; ::std::result::Result::Ok(()) } @@ -2159,6 +2195,11 @@ impl ::protobuf::Message for Room { unsafe { descriptor.get(|| { let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "id", + |m: &Room| { &m.id }, + |m: &mut Room| { &mut m.id }, + )); fields.push(::protobuf::reflect::accessor::make_map_accessor::<_, ::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>( "pipeline", |m: &Room| { &m.pipeline }, @@ -2186,6 +2227,7 @@ impl ::protobuf::Message for Room { impl ::protobuf::Clear for Room { fn clear(&mut self) { + self.id.clear(); self.pipeline.clear(); self.unknown_fields.clear(); } @@ -2574,6 +2616,7 @@ impl ::protobuf::reflect::ProtobufValue for Room_Element { #[derive(PartialEq,Clone,Default)] pub struct Member { // message fields + pub id: ::std::string::String, pub on_join: ::std::string::String, pub on_leave: ::std::string::String, pub credentials: ::std::string::String, @@ -2594,7 +2637,33 @@ impl Member { ::std::default::Default::default() } - // string on_join = 1; + // string id = 1; + + + pub fn get_id(&self) -> &str { + &self.id + } + pub fn clear_id(&mut self) { + self.id.clear(); + } + + // Param is passed by value, moved + pub fn set_id(&mut self, v: ::std::string::String) { + self.id = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_id(&mut self) -> &mut ::std::string::String { + &mut self.id + } + + // Take field + pub fn take_id(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.id, ::std::string::String::new()) + } + + // string on_join = 2; pub fn get_on_join(&self) -> &str { @@ -2620,7 +2689,7 @@ impl Member { ::std::mem::replace(&mut self.on_join, ::std::string::String::new()) } - // string on_leave = 2; + // string on_leave = 3; pub fn get_on_leave(&self) -> &str { @@ -2646,7 +2715,7 @@ impl Member { ::std::mem::replace(&mut self.on_leave, ::std::string::String::new()) } - // string credentials = 3; + // string credentials = 4; pub fn get_credentials(&self) -> &str { @@ -2672,7 +2741,7 @@ impl Member { ::std::mem::replace(&mut self.credentials, ::std::string::String::new()) } - // repeated .medea.Member.PipelineEntry pipeline = 4; + // repeated .medea.Member.PipelineEntry pipeline = 5; pub fn get_pipeline(&self) -> &::std::collections::HashMap<::std::string::String, Member_Element> { @@ -2708,15 +2777,18 @@ impl ::protobuf::Message for Member { let (field_number, wire_type) = is.read_tag_unpack()?; match field_number { 1 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_join)?; + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?; }, 2 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_leave)?; + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_join)?; }, 3 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.credentials)?; + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_leave)?; }, 4 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.credentials)?; + }, + 5 => { ::protobuf::rt::read_map_into::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(wire_type, is, &mut self.pipeline)?; }, _ => { @@ -2731,32 +2803,38 @@ impl ::protobuf::Message for Member { #[allow(unused_variables)] fn compute_size(&self) -> u32 { let mut my_size = 0; + if !self.id.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.id); + } if !self.on_join.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.on_join); + my_size += ::protobuf::rt::string_size(2, &self.on_join); } if !self.on_leave.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.on_leave); + my_size += ::protobuf::rt::string_size(3, &self.on_leave); } if !self.credentials.is_empty() { - my_size += ::protobuf::rt::string_size(3, &self.credentials); + my_size += ::protobuf::rt::string_size(4, &self.credentials); } - my_size += ::protobuf::rt::compute_map_size::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(4, &self.pipeline); + my_size += ::protobuf::rt::compute_map_size::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(5, &self.pipeline); my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); self.cached_size.set(my_size); my_size } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.id.is_empty() { + os.write_string(1, &self.id)?; + } if !self.on_join.is_empty() { - os.write_string(1, &self.on_join)?; + os.write_string(2, &self.on_join)?; } if !self.on_leave.is_empty() { - os.write_string(2, &self.on_leave)?; + os.write_string(3, &self.on_leave)?; } if !self.credentials.is_empty() { - os.write_string(3, &self.credentials)?; + os.write_string(4, &self.credentials)?; } - ::protobuf::rt::write_map_with_cached_sizes::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(4, &self.pipeline, os)?; + ::protobuf::rt::write_map_with_cached_sizes::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeMessage>(5, &self.pipeline, os)?; os.write_unknown_fields(self.get_unknown_fields())?; ::std::result::Result::Ok(()) } @@ -2799,6 +2877,11 @@ impl ::protobuf::Message for Member { unsafe { descriptor.get(|| { let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "id", + |m: &Member| { &m.id }, + |m: &mut Member| { &mut m.id }, + )); fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( "on_join", |m: &Member| { &m.on_join }, @@ -2841,6 +2924,7 @@ impl ::protobuf::Message for Member { impl ::protobuf::Clear for Member { fn clear(&mut self) { + self.id.clear(); self.on_join.clear(); self.on_leave.clear(); self.credentials.clear(); @@ -3156,6 +3240,7 @@ impl ::protobuf::reflect::ProtobufValue for Member_Element { #[derive(PartialEq,Clone,Default)] pub struct WebRtcPublishEndpoint { // message fields + pub id: ::std::string::String, pub p2p: WebRtcPublishEndpoint_P2P, pub on_start: ::std::string::String, pub on_stop: ::std::string::String, @@ -3175,7 +3260,33 @@ impl WebRtcPublishEndpoint { ::std::default::Default::default() } - // .medea.WebRtcPublishEndpoint.P2P p2p = 1; + // string id = 1; + + + pub fn get_id(&self) -> &str { + &self.id + } + pub fn clear_id(&mut self) { + self.id.clear(); + } + + // Param is passed by value, moved + pub fn set_id(&mut self, v: ::std::string::String) { + self.id = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_id(&mut self) -> &mut ::std::string::String { + &mut self.id + } + + // Take field + pub fn take_id(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.id, ::std::string::String::new()) + } + + // .medea.WebRtcPublishEndpoint.P2P p2p = 2; pub fn get_p2p(&self) -> WebRtcPublishEndpoint_P2P { @@ -3190,7 +3301,7 @@ impl WebRtcPublishEndpoint { self.p2p = v; } - // string on_start = 2; + // string on_start = 3; pub fn get_on_start(&self) -> &str { @@ -3216,7 +3327,7 @@ impl WebRtcPublishEndpoint { ::std::mem::replace(&mut self.on_start, ::std::string::String::new()) } - // string on_stop = 3; + // string on_stop = 4; pub fn get_on_stop(&self) -> &str { @@ -3253,12 +3364,15 @@ impl ::protobuf::Message for WebRtcPublishEndpoint { let (field_number, wire_type) = is.read_tag_unpack()?; match field_number { 1 => { - ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.p2p, 1, &mut self.unknown_fields)? + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?; }, 2 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_start)?; + ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.p2p, 2, &mut self.unknown_fields)? }, 3 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_start)?; + }, + 4 => { ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_stop)?; }, _ => { @@ -3273,14 +3387,17 @@ impl ::protobuf::Message for WebRtcPublishEndpoint { #[allow(unused_variables)] fn compute_size(&self) -> u32 { let mut my_size = 0; + if !self.id.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.id); + } if self.p2p != WebRtcPublishEndpoint_P2P::NEVER { - my_size += ::protobuf::rt::enum_size(1, self.p2p); + my_size += ::protobuf::rt::enum_size(2, self.p2p); } if !self.on_start.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.on_start); + my_size += ::protobuf::rt::string_size(3, &self.on_start); } if !self.on_stop.is_empty() { - my_size += ::protobuf::rt::string_size(3, &self.on_stop); + my_size += ::protobuf::rt::string_size(4, &self.on_stop); } my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); self.cached_size.set(my_size); @@ -3288,14 +3405,17 @@ impl ::protobuf::Message for WebRtcPublishEndpoint { } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.id.is_empty() { + os.write_string(1, &self.id)?; + } if self.p2p != WebRtcPublishEndpoint_P2P::NEVER { - os.write_enum(1, self.p2p.value())?; + os.write_enum(2, self.p2p.value())?; } if !self.on_start.is_empty() { - os.write_string(2, &self.on_start)?; + os.write_string(3, &self.on_start)?; } if !self.on_stop.is_empty() { - os.write_string(3, &self.on_stop)?; + os.write_string(4, &self.on_stop)?; } os.write_unknown_fields(self.get_unknown_fields())?; ::std::result::Result::Ok(()) @@ -3339,6 +3459,11 @@ impl ::protobuf::Message for WebRtcPublishEndpoint { unsafe { descriptor.get(|| { let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "id", + |m: &WebRtcPublishEndpoint| { &m.id }, + |m: &mut WebRtcPublishEndpoint| { &mut m.id }, + )); fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum>( "p2p", |m: &WebRtcPublishEndpoint| { &m.p2p }, @@ -3376,6 +3501,7 @@ impl ::protobuf::Message for WebRtcPublishEndpoint { impl ::protobuf::Clear for WebRtcPublishEndpoint { fn clear(&mut self) { + self.id.clear(); self.p2p = WebRtcPublishEndpoint_P2P::NEVER; self.on_start.clear(); self.on_stop.clear(); @@ -3456,6 +3582,7 @@ impl ::protobuf::reflect::ProtobufValue for WebRtcPublishEndpoint_P2P { #[derive(PartialEq,Clone,Default)] pub struct WebRtcPlayEndpoint { // message fields + pub id: ::std::string::String, pub src: ::std::string::String, pub on_start: ::std::string::String, pub on_stop: ::std::string::String, @@ -3475,7 +3602,33 @@ impl WebRtcPlayEndpoint { ::std::default::Default::default() } - // string src = 1; + // string id = 1; + + + pub fn get_id(&self) -> &str { + &self.id + } + pub fn clear_id(&mut self) { + self.id.clear(); + } + + // Param is passed by value, moved + pub fn set_id(&mut self, v: ::std::string::String) { + self.id = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_id(&mut self) -> &mut ::std::string::String { + &mut self.id + } + + // Take field + pub fn take_id(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.id, ::std::string::String::new()) + } + + // string src = 2; pub fn get_src(&self) -> &str { @@ -3501,7 +3654,7 @@ impl WebRtcPlayEndpoint { ::std::mem::replace(&mut self.src, ::std::string::String::new()) } - // string on_start = 2; + // string on_start = 3; pub fn get_on_start(&self) -> &str { @@ -3527,7 +3680,7 @@ impl WebRtcPlayEndpoint { ::std::mem::replace(&mut self.on_start, ::std::string::String::new()) } - // string on_stop = 3; + // string on_stop = 4; pub fn get_on_stop(&self) -> &str { @@ -3564,12 +3717,15 @@ impl ::protobuf::Message for WebRtcPlayEndpoint { let (field_number, wire_type) = is.read_tag_unpack()?; match field_number { 1 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.src)?; + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?; }, 2 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_start)?; + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.src)?; }, 3 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_start)?; + }, + 4 => { ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.on_stop)?; }, _ => { @@ -3584,14 +3740,17 @@ impl ::protobuf::Message for WebRtcPlayEndpoint { #[allow(unused_variables)] fn compute_size(&self) -> u32 { let mut my_size = 0; + if !self.id.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.id); + } if !self.src.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.src); + my_size += ::protobuf::rt::string_size(2, &self.src); } if !self.on_start.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.on_start); + my_size += ::protobuf::rt::string_size(3, &self.on_start); } if !self.on_stop.is_empty() { - my_size += ::protobuf::rt::string_size(3, &self.on_stop); + my_size += ::protobuf::rt::string_size(4, &self.on_stop); } my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); self.cached_size.set(my_size); @@ -3599,14 +3758,17 @@ impl ::protobuf::Message for WebRtcPlayEndpoint { } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.id.is_empty() { + os.write_string(1, &self.id)?; + } if !self.src.is_empty() { - os.write_string(1, &self.src)?; + os.write_string(2, &self.src)?; } if !self.on_start.is_empty() { - os.write_string(2, &self.on_start)?; + os.write_string(3, &self.on_start)?; } if !self.on_stop.is_empty() { - os.write_string(3, &self.on_stop)?; + os.write_string(4, &self.on_stop)?; } os.write_unknown_fields(self.get_unknown_fields())?; ::std::result::Result::Ok(()) @@ -3650,6 +3812,11 @@ impl ::protobuf::Message for WebRtcPlayEndpoint { unsafe { descriptor.get(|| { let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "id", + |m: &WebRtcPlayEndpoint| { &m.id }, + |m: &mut WebRtcPlayEndpoint| { &mut m.id }, + )); fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( "src", |m: &WebRtcPlayEndpoint| { &m.src }, @@ -3687,6 +3854,7 @@ impl ::protobuf::Message for WebRtcPlayEndpoint { impl ::protobuf::Clear for WebRtcPlayEndpoint { fn clear(&mut self) { + self.id.clear(); self.src.clear(); self.on_start.clear(); self.on_stop.clear(); @@ -3707,57 +3875,60 @@ impl ::protobuf::reflect::ProtobufValue for WebRtcPlayEndpoint { } static file_descriptor_proto_data: &'static [u8] = b"\ - \n\tapi.proto\x12\x05medea\"\xee\x01\n\rCreateRequest\x12\x0e\n\x02id\ - \x18\x01\x20\x01(\tR\x02id\x12'\n\x06member\x18\x02\x20\x01(\x0b2\r.mede\ - a.MemberH\0R\x06member\x12!\n\x04room\x18\x03\x20\x01(\x0b2\x0b.medea.Ro\ - omH\0R\x04room\x12<\n\x0bwebrtc_play\x18\x04\x20\x01(\x0b2\x19.medea.Web\ - RtcPlayEndpointH\0R\nwebrtcPlay\x12=\n\nwebrtc_pub\x18\x05\x20\x01(\x0b2\ - \x1c.medea.WebRtcPublishEndpointH\0R\twebrtcPubB\x04\n\x02el\"\x1b\n\tId\ - Request\x12\x0e\n\x02id\x18\x01\x20\x03(\tR\x02id\".\n\x08Response\x12\"\ - \n\x05error\x18\x01\x20\x01(\x0b2\x0c.medea.ErrorR\x05error\"\x9e\x01\n\ - \x0eCreateResponse\x120\n\x03sid\x18\x01\x20\x03(\x0b2\x1e.medea.CreateR\ - esponse.SidEntryR\x03sid\x12\"\n\x05error\x18\x02\x20\x01(\x0b2\x0c.mede\ - a.ErrorR\x05error\x1a6\n\x08SidEntry\x12\x10\n\x03key\x18\x01\x20\x01(\t\ - R\x03key\x12\x14\n\x05value\x18\x02\x20\x01(\tR\x05value:\x028\x01\"\xbc\ - \x01\n\x0bGetResponse\x12<\n\x08elements\x18\x01\x20\x03(\x0b2\x20.medea\ - .GetResponse.ElementsEntryR\x08elements\x12\"\n\x05error\x18\x02\x20\x01\ - (\x0b2\x0c.medea.ErrorR\x05error\x1aK\n\rElementsEntry\x12\x10\n\x03key\ - \x18\x01\x20\x01(\tR\x03key\x12$\n\x05value\x18\x02\x20\x01(\x0b2\x0e.me\ - dea.ElementR\x05value:\x028\x01\"[\n\x05Error\x12\x12\n\x04code\x18\x01\ - \x20\x01(\rR\x04code\x12\x12\n\x04text\x18\x02\x20\x01(\tR\x04text\x12\ - \x10\n\x03doc\x18\x03\x20\x01(\tR\x03doc\x12\x18\n\x07element\x18\x04\ - \x20\x01(\tR\x07element\"\xd8\x01\n\x07Element\x12'\n\x06member\x18\x01\ - \x20\x01(\x0b2\r.medea.MemberH\0R\x06member\x12!\n\x04room\x18\x02\x20\ - \x01(\x0b2\x0b.medea.RoomH\0R\x04room\x12<\n\x0bwebrtc_play\x18\x03\x20\ - \x01(\x0b2\x19.medea.WebRtcPlayEndpointH\0R\nwebrtcPlay\x12=\n\nwebrtc_p\ - ub\x18\x04\x20\x01(\x0b2\x1c.medea.WebRtcPublishEndpointH\0R\twebrtcPubB\ - \x04\n\x02el\"\xc7\x02\n\x04Room\x125\n\x08pipeline\x18\x01\x20\x03(\x0b\ - 2\x19.medea.Room.PipelineEntryR\x08pipeline\x1aP\n\rPipelineEntry\x12\ - \x10\n\x03key\x18\x01\x20\x01(\tR\x03key\x12)\n\x05value\x18\x02\x20\x01\ - (\x0b2\x13.medea.Room.ElementR\x05value:\x028\x01\x1a\xb5\x01\n\x07Eleme\ - nt\x12'\n\x06member\x18\x01\x20\x01(\x0b2\r.medea.MemberH\0R\x06member\ - \x12<\n\x0bwebrtc_play\x18\x02\x20\x01(\x0b2\x19.medea.WebRtcPlayEndpoin\ - tH\0R\nwebrtcPlay\x12=\n\nwebrtc_pub\x18\x03\x20\x01(\x0b2\x1c.medea.Web\ - RtcPublishEndpointH\0R\twebrtcPubB\x04\n\x02el\"\xfa\x02\n\x06Member\x12\ - \x17\n\x07on_join\x18\x01\x20\x01(\tR\x06onJoin\x12\x19\n\x08on_leave\ - \x18\x02\x20\x01(\tR\x07onLeave\x12\x20\n\x0bcredentials\x18\x03\x20\x01\ - (\tR\x0bcredentials\x127\n\x08pipeline\x18\x04\x20\x03(\x0b2\x1b.medea.M\ - ember.PipelineEntryR\x08pipeline\x1aR\n\rPipelineEntry\x12\x10\n\x03key\ - \x18\x01\x20\x01(\tR\x03key\x12+\n\x05value\x18\x02\x20\x01(\x0b2\x15.me\ - dea.Member.ElementR\x05value:\x028\x01\x1a\x8c\x01\n\x07Element\x12<\n\ - \x0bwebrtc_play\x18\x01\x20\x01(\x0b2\x19.medea.WebRtcPlayEndpointH\0R\n\ - webrtcPlay\x12=\n\nwebrtc_pub\x18\x02\x20\x01(\x0b2\x1c.medea.WebRtcPubl\ - ishEndpointH\0R\twebrtcPubB\x04\n\x02el\"\xae\x01\n\x15WebRtcPublishEndp\ - oint\x122\n\x03p2p\x18\x01\x20\x01(\x0e2\x20.medea.WebRtcPublishEndpoint\ - .P2PR\x03p2p\x12\x19\n\x08on_start\x18\x02\x20\x01(\tR\x07onStart\x12\ - \x17\n\x07on_stop\x18\x03\x20\x01(\tR\x06onStop\"-\n\x03P2P\x12\t\n\x05N\ - EVER\x10\0\x12\x0f\n\x0bIF_POSSIBLE\x10\x01\x12\n\n\x06ALWAYS\x10\x02\"Z\ - \n\x12WebRtcPlayEndpoint\x12\x10\n\x03src\x18\x01\x20\x01(\tR\x03src\x12\ - \x19\n\x08on_start\x18\x02\x20\x01(\tR\x07onStart\x12\x17\n\x07on_stop\ - \x18\x03\x20\x01(\tR\x06onStop2\x9d\x01\n\nControlApi\x125\n\x06Create\ - \x12\x14.medea.CreateRequest\x1a\x15.medea.CreateResponse\x12+\n\x06Dele\ - te\x12\x10.medea.IdRequest\x1a\x0f.medea.Response\x12+\n\x03Get\x12\x10.\ - medea.IdRequest\x1a\x12.medea.GetResponseb\x06proto3\ + \n\tapi.proto\x12\x05medea\"\xfd\x01\n\rCreateRequest\x12\x1d\n\nparent_\ + fid\x18\x01\x20\x01(\tR\tparentFid\x12'\n\x06member\x18\x02\x20\x01(\x0b\ + 2\r.medea.MemberH\0R\x06member\x12!\n\x04room\x18\x03\x20\x01(\x0b2\x0b.\ + medea.RoomH\0R\x04room\x12<\n\x0bwebrtc_play\x18\x04\x20\x01(\x0b2\x19.m\ + edea.WebRtcPlayEndpointH\0R\nwebrtcPlay\x12=\n\nwebrtc_pub\x18\x05\x20\ + \x01(\x0b2\x1c.medea.WebRtcPublishEndpointH\0R\twebrtcPubB\x04\n\x02el\"\ + \x1d\n\tIdRequest\x12\x10\n\x03fid\x18\x01\x20\x03(\tR\x03fid\".\n\x08Re\ + sponse\x12\"\n\x05error\x18\x01\x20\x01(\x0b2\x0c.medea.ErrorR\x05error\ + \"\x9e\x01\n\x0eCreateResponse\x120\n\x03sid\x18\x01\x20\x03(\x0b2\x1e.m\ + edea.CreateResponse.SidEntryR\x03sid\x12\"\n\x05error\x18\x02\x20\x01(\ + \x0b2\x0c.medea.ErrorR\x05error\x1a6\n\x08SidEntry\x12\x10\n\x03key\x18\ + \x01\x20\x01(\tR\x03key\x12\x14\n\x05value\x18\x02\x20\x01(\tR\x05value:\ + \x028\x01\"\xbc\x01\n\x0bGetResponse\x12<\n\x08elements\x18\x01\x20\x03(\ + \x0b2\x20.medea.GetResponse.ElementsEntryR\x08elements\x12\"\n\x05error\ + \x18\x02\x20\x01(\x0b2\x0c.medea.ErrorR\x05error\x1aK\n\rElementsEntry\ + \x12\x10\n\x03key\x18\x01\x20\x01(\tR\x03key\x12$\n\x05value\x18\x02\x20\ + \x01(\x0b2\x0e.medea.ElementR\x05value:\x028\x01\"[\n\x05Error\x12\x12\n\ + \x04code\x18\x01\x20\x01(\rR\x04code\x12\x12\n\x04text\x18\x02\x20\x01(\ + \tR\x04text\x12\x10\n\x03doc\x18\x03\x20\x01(\tR\x03doc\x12\x18\n\x07ele\ + ment\x18\x04\x20\x01(\tR\x07element\"\xd8\x01\n\x07Element\x12'\n\x06mem\ + ber\x18\x01\x20\x01(\x0b2\r.medea.MemberH\0R\x06member\x12!\n\x04room\ + \x18\x02\x20\x01(\x0b2\x0b.medea.RoomH\0R\x04room\x12<\n\x0bwebrtc_play\ + \x18\x03\x20\x01(\x0b2\x19.medea.WebRtcPlayEndpointH\0R\nwebrtcPlay\x12=\ + \n\nwebrtc_pub\x18\x04\x20\x01(\x0b2\x1c.medea.WebRtcPublishEndpointH\0R\ + \twebrtcPubB\x04\n\x02el\"\xd7\x02\n\x04Room\x12\x0e\n\x02id\x18\x01\x20\ + \x01(\tR\x02id\x125\n\x08pipeline\x18\x02\x20\x03(\x0b2\x19.medea.Room.P\ + ipelineEntryR\x08pipeline\x1aP\n\rPipelineEntry\x12\x10\n\x03key\x18\x01\ + \x20\x01(\tR\x03key\x12)\n\x05value\x18\x02\x20\x01(\x0b2\x13.medea.Room\ + .ElementR\x05value:\x028\x01\x1a\xb5\x01\n\x07Element\x12'\n\x06member\ + \x18\x01\x20\x01(\x0b2\r.medea.MemberH\0R\x06member\x12<\n\x0bwebrtc_pla\ + y\x18\x02\x20\x01(\x0b2\x19.medea.WebRtcPlayEndpointH\0R\nwebrtcPlay\x12\ + =\n\nwebrtc_pub\x18\x03\x20\x01(\x0b2\x1c.medea.WebRtcPublishEndpointH\0\ + R\twebrtcPubB\x04\n\x02el\"\x8a\x03\n\x06Member\x12\x0e\n\x02id\x18\x01\ + \x20\x01(\tR\x02id\x12\x17\n\x07on_join\x18\x02\x20\x01(\tR\x06onJoin\ + \x12\x19\n\x08on_leave\x18\x03\x20\x01(\tR\x07onLeave\x12\x20\n\x0bcrede\ + ntials\x18\x04\x20\x01(\tR\x0bcredentials\x127\n\x08pipeline\x18\x05\x20\ + \x03(\x0b2\x1b.medea.Member.PipelineEntryR\x08pipeline\x1aR\n\rPipelineE\ + ntry\x12\x10\n\x03key\x18\x01\x20\x01(\tR\x03key\x12+\n\x05value\x18\x02\ + \x20\x01(\x0b2\x15.medea.Member.ElementR\x05value:\x028\x01\x1a\x8c\x01\ + \n\x07Element\x12<\n\x0bwebrtc_play\x18\x01\x20\x01(\x0b2\x19.medea.WebR\ + tcPlayEndpointH\0R\nwebrtcPlay\x12=\n\nwebrtc_pub\x18\x02\x20\x01(\x0b2\ + \x1c.medea.WebRtcPublishEndpointH\0R\twebrtcPubB\x04\n\x02el\"\xbe\x01\n\ + \x15WebRtcPublishEndpoint\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x122\ + \n\x03p2p\x18\x02\x20\x01(\x0e2\x20.medea.WebRtcPublishEndpoint.P2PR\x03\ + p2p\x12\x19\n\x08on_start\x18\x03\x20\x01(\tR\x07onStart\x12\x17\n\x07on\ + _stop\x18\x04\x20\x01(\tR\x06onStop\"-\n\x03P2P\x12\t\n\x05NEVER\x10\0\ + \x12\x0f\n\x0bIF_POSSIBLE\x10\x01\x12\n\n\x06ALWAYS\x10\x02\"j\n\x12WebR\ + tcPlayEndpoint\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x12\x10\n\x03sr\ + c\x18\x02\x20\x01(\tR\x03src\x12\x19\n\x08on_start\x18\x03\x20\x01(\tR\ + \x07onStart\x12\x17\n\x07on_stop\x18\x04\x20\x01(\tR\x06onStop2\x9d\x01\ + \n\nControlApi\x125\n\x06Create\x12\x14.medea.CreateRequest\x1a\x15.mede\ + a.CreateResponse\x12+\n\x06Delete\x12\x10.medea.IdRequest\x1a\x0f.medea.\ + Response\x12+\n\x03Get\x12\x10.medea.IdRequest\x1a\x12.medea.GetResponse\ + b\x06proto3\ "; static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy { diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index c1157a9e5..92972d0c9 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -1,6 +1,6 @@ //! Implementation of [Client API]. //! -//! [Client API]: http://tiny.cc/c80uaz +//! [Client API]: https://tinyurl.com/yx9thsnr mod session; diff --git a/src/api/client/server.rs b/src/api/client/server.rs index a720cddb2..ad5bab92f 100644 --- a/src/api/client/server.rs +++ b/src/api/client/server.rs @@ -2,7 +2,7 @@ use std::io; -use actix::{Actor, Addr, Handler, ResponseActFuture, WrapFuture as _}; +use actix::{Actor, Addr, Handler, ResponseFuture}; use actix_web::{ dev::Server as ActixServer, middleware, @@ -98,7 +98,7 @@ pub struct Server(ActixServer); impl Server { /// Starts Client API HTTP server. pub fn run(rooms: RoomRepository, config: Conf) -> io::Result> { - let server_addr = config.server.bind_addr(); + let server_addr = config.server.client.http.bind_addr(); let server = HttpServer::new(move || { App::new() @@ -127,7 +127,7 @@ impl Actor for Server { } impl Handler for Server { - type Result = ResponseActFuture; + type Result = ResponseFuture<(), ()>; fn handle( &mut self, @@ -135,7 +135,7 @@ impl Handler for Server { _: &mut Self::Context, ) -> Self::Result { info!("Server received ShutdownGracefully message so shutting down"); - Box::new(self.0.stop(true).into_actor(self)) + Box::new(self.0.stop(true)) } } @@ -150,29 +150,26 @@ mod test { use crate::{ api::control, conf::Conf, signalling::Room, - turn::new_turn_auth_service_mock, + turn::new_turn_auth_service_mock, AppContext, }; use super::*; - /// Creates [`RoomsRepository`] for tests filled with a single [`Room`]. - fn room(conf: Rpc) -> RoomRepository { + /// Creates [`RoomRepository`] for tests filled with a single [`Room`]. + fn room(conf: Conf) -> RoomRepository { let room_spec = control::load_from_yaml_file("tests/specs/pub-sub-video-call.yml") .unwrap(); - let client_room = Room::new( - &room_spec, - conf.reconnect_timeout, - new_turn_auth_service_mock(), - ) - .unwrap() - .start(); - let rooms = hashmap! { - room_spec.id => client_room, + let app = AppContext::new(conf, new_turn_auth_service_mock()); + + let room_id = room_spec.id.clone(); + let client_room = Room::new(&room_spec, &app).unwrap().start(); + let room_hash_map = hashmap! { + room_id => client_room, }; - RoomRepository::new(rooms) + RoomRepository::new(room_hash_map) } /// Creates test WebSocket server of Client API which can handle requests. @@ -181,8 +178,8 @@ mod test { HttpService::new( App::new() .data(Context { - rooms: room(conf.rpc.clone()), config: conf.rpc.clone(), + rooms: room(conf.clone()), }) .service( resource("/ws/{room_id}/{member_id}/{credentials}") @@ -194,10 +191,12 @@ mod test { #[test] fn ping_pong_and_disconnects_on_idle() { - let mut conf = Conf::default(); - conf.rpc = Rpc { - idle_timeout: Duration::new(2, 0), - reconnect_timeout: Default::default(), + let conf = Conf { + rpc: Rpc { + idle_timeout: Duration::new(2, 0), + ..Rpc::default() + }, + ..Conf::default() }; let mut server = ws_server(conf.clone()); diff --git a/src/api/control/endpoint.rs b/src/api/control/endpoint.rs deleted file mode 100644 index 234a6b6f9..000000000 --- a/src/api/control/endpoint.rs +++ /dev/null @@ -1,214 +0,0 @@ -//! Definitions and implementations of [Control API]'s `Endpoint`s elements. -//! -//! [Control API]: http://tiny.cc/380uaz - -use std::{convert::TryFrom, fmt, str::FromStr}; - -use serde::{ - de::{self, Deserializer, Error, Visitor}, - Deserialize, -}; -use url::Url; - -use crate::api::control::MemberId; - -use super::{member::MemberElement, TryFromElementError}; - -/// Media element that one or more media data streams flow through. -#[derive(Debug)] -pub enum Endpoint { - WebRtcPublish(WebRtcPublishEndpoint), - WebRtcPlay(WebRtcPlayEndpoint), -} - -/// Possible schemes of media elements URIs. -#[derive(Clone, Debug)] -pub enum Scheme { - /// `local://` scheme which refers to a local in-memory media element. - Local, -} - -impl FromStr for Scheme { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "local" => Ok(Self::Local), - _ => Err(format!("cannot parse \"{}\" to Scheme", s)), - } - } -} - -impl TryFrom<&MemberElement> for Endpoint { - type Error = TryFromElementError; - - fn try_from(from: &MemberElement) -> Result { - match from { - MemberElement::WebRtcPlayEndpoint { spec } => { - Ok(Self::WebRtcPlay(spec.clone())) - } - MemberElement::WebRtcPublishEndpoint { spec } => { - Ok(Self::WebRtcPublish(spec.clone())) - } - } - } -} - -/// Peer-to-peer mode of [`WebRtcPublishEndpoint`]. -#[derive(Clone, Deserialize, Debug)] -pub enum P2pMode { - /// Always connect peer-to-peer. - Always, -} - -/// Media element which is able to publish media data for another client via -/// WebRTC. -#[allow(clippy::module_name_repetitions)] -#[derive(Clone, Deserialize, Debug)] -pub struct WebRtcPublishEndpoint { - /// Peer-to-peer mode. - pub p2p: P2pMode, -} - -/// Media element which is able to play media data for client via WebRTC. -#[allow(clippy::module_name_repetitions)] -#[derive(Clone, Deserialize, Debug)] -pub struct WebRtcPlayEndpoint { - /// Source URI in format `local://{room_id}/{member_id}/{endpoint_id}`. - pub src: SrcUri, -} - -/// Special uri with pattern `local://{room_id}/{member_id}/{endpoint_id}`. -#[derive(Clone, Debug)] -pub struct SrcUri { - /// Scheme of media element URI. - pub scheme: Scheme, - - /// ID of [`Room`] - /// - /// [`Room`]: crate::signalling::room::Room - pub room_id: String, - - /// ID of `Member` - pub member_id: MemberId, - - /// Control API ID of [`Endpoint`] - pub endpoint_id: String, -} - -/// Deserialization for [`SrcUri`] with pattern -/// `local://{room_id}/{member_id}/{endpoint_id}`. -impl<'de> Deserialize<'de> for SrcUri { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct SrcUriVisitor; - - impl<'de> Visitor<'de> for SrcUriVisitor { - type Value = SrcUri; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str( - "URI in format local://room_id/member_id/endpoint_id", - ) - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - let uri = Url::parse(value).map_err(|_| { - Error::custom(format!("'{}' is not URI", value)) - })?; - let scheme = FromStr::from_str(uri.scheme()).map_err(|_| { - Error::custom(format!( - "cannot parse URI scheme '{}'", - value - )) - })?; - let room_id = uri - .host() - .ok_or_else(|| { - Error::custom(format!( - "cannot parse room ID from URI '{}'", - value - )) - })? - .to_string(); - let mut path = uri.path_segments().ok_or_else(|| { - Error::custom(format!( - "cannot parse member and endpoint IDs from URI '{}'", - value - )) - })?; - let member_id = path - .next() - .map(|id| MemberId(id.to_owned())) - .ok_or_else(|| { - Error::custom(format!( - "cannot parse member ID from URI '{}'", - value - )) - })?; - let endpoint_id = - path.next().map(ToOwned::to_owned).ok_or_else(|| { - Error::custom(format!( - "cannot parse endpoint ID from URI '{}'", - value - )) - })?; - Ok(SrcUri { - scheme, - room_id, - member_id, - endpoint_id, - }) - } - } - - deserializer.deserialize_identifier(SrcUriVisitor) - } -} - -#[cfg(test)] -mod src_uri_deserialization_tests { - use serde::Deserialize; - - use super::*; - - #[derive(Deserialize)] - struct SrcUriTest { - src: SrcUri, - } - - #[test] - fn deserializes() { - let uri: SrcUriTest = serde_json::from_str( - r#"{ "src": "local://room_id/member_id/endpoint_id" }"#, - ) - .unwrap(); - - assert_eq!(uri.src.member_id, MemberId("member_id".into())); - assert_eq!(uri.src.room_id, "room_id".to_string()); - assert_eq!(uri.src.endpoint_id, "endpoint_id".to_string()); - } - - #[test] - fn errors_on_incorrect_scheme() { - let res = serde_json::from_str::( - r#"{ "src": "not_local://room_id/member_id/endpoint_id" }"#, - ); - - assert!(res.is_err()) - } - - #[test] - fn errors_when_endpoint_is_absent() { - let res = serde_json::from_str::( - r#"{ "src": "local://room_id/member_id" }"#, - ); - - assert!(res.is_err()) - } -} diff --git a/src/api/control/endpoints/mod.rs b/src/api/control/endpoints/mod.rs new file mode 100644 index 000000000..08f913ade --- /dev/null +++ b/src/api/control/endpoints/mod.rs @@ -0,0 +1,115 @@ +//! Endpoint elements of [Medea] spec. +//! +//! [Medea]: https://github.com/instrumentisto/medea + +pub mod webrtc_play_endpoint; +pub mod webrtc_publish_endpoint; + +use std::convert::TryFrom; + +use derive_more::{Display, From, Into}; +use serde::Deserialize; + +use medea_control_api_proto::grpc::api::{ + CreateRequest_oneof_el as ElementProto, + Member_Element_oneof_el as MemberElementProto, +}; + +use super::{member::MemberElement, TryFromProtobufError}; + +#[doc(inline)] +pub use webrtc_play_endpoint::{WebRtcPlayEndpoint, WebRtcPlayId}; +#[doc(inline)] +pub use webrtc_publish_endpoint::{WebRtcPublishEndpoint, WebRtcPublishId}; + +/// ID of `Endpoint`. +#[derive( + Clone, Debug, Deserialize, Display, Eq, From, Hash, Into, PartialEq, +)] +pub struct Id(pub String); + +macro_rules! impl_from_into { + ($id:ty) => { + impl std::convert::From for $id { + fn from(id: Id) -> Self { + String::from(id).into() + } + } + + impl std::convert::From<$id> for Id { + fn from(id: $id) -> Self { + String::from(id).into() + } + } + }; +} + +impl_from_into!(WebRtcPublishId); +impl_from_into!(WebRtcPlayId); + +/// Media element that one or more media data streams flow through. +#[derive(Debug, From)] +pub enum EndpointSpec { + /// [`WebRtcPublishEndpoint`] element. + WebRtcPublish(WebRtcPublishEndpoint), + + /// [`WebRtcPlayEndpoint`] element. + WebRtcPlay(WebRtcPlayEndpoint), +} + +impl Into for EndpointSpec { + fn into(self) -> MemberElement { + match self { + Self::WebRtcPublish(e) => { + MemberElement::WebRtcPublishEndpoint { spec: e } + } + Self::WebRtcPlay(e) => { + MemberElement::WebRtcPlayEndpoint { spec: e } + } + } + } +} + +impl TryFrom<(Id, MemberElementProto)> for EndpointSpec { + type Error = TryFromProtobufError; + + fn try_from( + (_, proto): (Id, MemberElementProto), + ) -> Result { + use MemberElementProto::*; + match proto { + webrtc_play(elem) => { + let play = WebRtcPlayEndpoint::try_from(&elem)?; + Ok(Self::WebRtcPlay(play)) + } + webrtc_pub(elem) => { + let publish = WebRtcPublishEndpoint::from(&elem); + Ok(Self::WebRtcPublish(publish)) + } + } + } +} + +impl TryFrom<(Id, ElementProto)> for EndpointSpec { + type Error = TryFromProtobufError; + + fn try_from((id, proto): (Id, ElementProto)) -> Result { + use ElementProto::*; + match proto { + webrtc_play(elem) => { + let play = WebRtcPlayEndpoint::try_from(&elem)?; + Ok(Self::WebRtcPlay(play)) + } + webrtc_pub(elem) => { + let publish = WebRtcPublishEndpoint::from(&elem); + Ok(Self::WebRtcPublish(publish)) + } + member(_) | room(_) => { + Err(TryFromProtobufError::ExpectedOtherElement( + String::from("Endpoint"), + id.0, + )) + } + } + } +} diff --git a/src/api/control/endpoints/webrtc_play_endpoint.rs b/src/api/control/endpoints/webrtc_play_endpoint.rs new file mode 100644 index 000000000..57be07b14 --- /dev/null +++ b/src/api/control/endpoints/webrtc_play_endpoint.rs @@ -0,0 +1,36 @@ +//! `WebRtcPlayEndpoint` [Control API]'s element implementation. +//! +//! [Control API]: https://tinyurl.com/yxsqplq7 + +use std::convert::TryFrom; + +use derive_more::{Display, From, Into}; +use medea_control_api_proto::grpc::api as medea_grpc_control_api; +use medea_grpc_control_api::WebRtcPlayEndpoint as WebRtcPlayEndpointProto; +use serde::Deserialize; + +use crate::api::control::{refs::SrcUri, TryFromProtobufError}; + +/// ID of [`WebRtcPlayEndpoint`]. +#[derive( + Clone, Debug, Deserialize, Display, Eq, Hash, PartialEq, From, Into, +)] +pub struct WebRtcPlayId(String); + +/// Media element which is able to play media data for client via WebRTC. +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, Deserialize, Debug)] +pub struct WebRtcPlayEndpoint { + /// Source URI in format `local://{room_id}/{member_id}/{endpoint_id}`. + pub src: SrcUri, +} + +impl TryFrom<&WebRtcPlayEndpointProto> for WebRtcPlayEndpoint { + type Error = TryFromProtobufError; + + fn try_from(value: &WebRtcPlayEndpointProto) -> Result { + Ok(Self { + src: SrcUri::try_from(value.get_src().to_owned())?, + }) + } +} diff --git a/src/api/control/endpoints/webrtc_publish_endpoint.rs b/src/api/control/endpoints/webrtc_publish_endpoint.rs new file mode 100644 index 000000000..475a3af25 --- /dev/null +++ b/src/api/control/endpoints/webrtc_publish_endpoint.rs @@ -0,0 +1,67 @@ +//! `WebRtcPublishEndpoint` [Control API]'s element implementation. +//! +//! [Control API]: https://tinyurl.com/yxsqplq7 + +use derive_more::{Display, From, Into}; +use serde::Deserialize; + +use medea_control_api_proto::grpc::api::{ + WebRtcPublishEndpoint as WebRtcPublishEndpointProto, + WebRtcPublishEndpoint_P2P as WebRtcPublishEndpointP2pProto, +}; + +/// ID of [`WebRtcPublishEndpoint`]. +#[derive( + Clone, Debug, Deserialize, Display, Eq, Hash, PartialEq, From, Into, +)] +pub struct WebRtcPublishId(String); + +/// Peer-to-peer mode of [`WebRtcPublishEndpoint`]. +#[derive(Clone, Deserialize, Debug)] +pub enum P2pMode { + /// Always connect peer-to-peer. + Always, + + /// Never connect peer-to-peer. + Never, + + /// Connect peer-to-peer if it possible. + IfPossible, +} + +impl From for P2pMode { + fn from(value: WebRtcPublishEndpointP2pProto) -> Self { + match value { + WebRtcPublishEndpointP2pProto::ALWAYS => Self::Always, + WebRtcPublishEndpointP2pProto::IF_POSSIBLE => Self::IfPossible, + WebRtcPublishEndpointP2pProto::NEVER => Self::Never, + } + } +} + +impl Into for P2pMode { + fn into(self) -> WebRtcPublishEndpointP2pProto { + match self { + Self::Always => WebRtcPublishEndpointP2pProto::ALWAYS, + Self::IfPossible => WebRtcPublishEndpointP2pProto::IF_POSSIBLE, + Self::Never => WebRtcPublishEndpointP2pProto::NEVER, + } + } +} + +/// Media element which is able to publish media data for another client via +/// WebRTC. +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, Deserialize, Debug)] +pub struct WebRtcPublishEndpoint { + /// Peer-to-peer mode of this [`WebRtcPublishEndpoint`]. + pub p2p: P2pMode, +} + +impl From<&WebRtcPublishEndpointProto> for WebRtcPublishEndpoint { + fn from(value: &WebRtcPublishEndpointProto) -> Self { + Self { + p2p: P2pMode::from(value.get_p2p()), + } + } +} diff --git a/src/api/control/error_codes.rs b/src/api/control/error_codes.rs new file mode 100644 index 000000000..61e675576 --- /dev/null +++ b/src/api/control/error_codes.rs @@ -0,0 +1,469 @@ +//! All errors which Medea can return to Control API user. +//! +//! # Error codes ranges +//! - `1000` ... `1999` Client errors +//! - `2000` ... `2999` Server errors + +use std::string::ToString; + +use derive_more::Display; +use medea_control_api_proto::grpc::api::Error as ErrorProto; + +use crate::{ + api::control::{ + grpc::server::GrpcControlApiError, + refs::{ + fid::ParseFidError, local_uri::LocalUriParseError, + src_uri::SrcParseError, + }, + TryFromElementError, TryFromProtobufError, + }, + signalling::{ + elements::{member::MemberError, MembersLoadError}, + participants::ParticipantServiceErr, + room::RoomError, + room_service::RoomServiceError, + }, +}; + +/// Medea's Control API error response. +pub struct ErrorResponse { + /// [`ErrorCode`] which will be returned with code and message. + error_code: ErrorCode, + + /// Element ID where some error happened. May be empty. + element_id: Option, + + /// All [`ErrorCode`]s have [`Display`] implementation. And this + /// implementation will be used if this field is [`None`]. But + /// sometimes we want to add some error explanation. Then we set this + /// field to [`Some`] and this text will be added to + /// [`Display`] implementation's text. + /// + /// By default this field should be [`None`]. + /// + /// For providing error explanation use [`ErrorResponse::with_explanation`] + /// method. + /// + /// [`Display`]: std::fmt::Display + explanation: Option, +} + +impl ErrorResponse { + /// New [`ErrorResponse`] with [`ErrorCode`] and element ID. + pub fn new(error_code: ErrorCode, element_id: &T) -> Self { + Self { + error_code, + element_id: Some(element_id.to_string()), + explanation: None, + } + } + + /// New [`ErrorResponse`] only with [`ErrorCode`]. + pub fn without_id(error_code: ErrorCode) -> Self { + Self { + error_code, + element_id: None, + explanation: None, + } + } + + /// [`ErrorResponse`] for all unexpected errors. + /// + /// Provide unexpected `Error` to this function. + /// This error will be printed with [`Display`] implementation + /// of provided `Error` as error explanation. + /// + /// [`Display`]: std::fmt::Display + pub fn unexpected(unknown_error: &B) -> Self { + Self { + error_code: ErrorCode::UnexpectedError, + explanation: Some(unknown_error.to_string()), + element_id: None, + } + } + + /// [`ErrorResponse`] with some additional info. + /// + /// With this method you can add additional text to error message of + /// [`ErrorCode`]. + pub fn with_explanation( + error_code: ErrorCode, + explanation: String, + id: Option, + ) -> Self { + Self { + error_code, + explanation: Some(explanation), + element_id: id.map(|s| s.to_string()), + } + } +} + +impl Into for ErrorResponse { + fn into(self) -> ErrorProto { + let mut error = ErrorProto::new(); + + if let Some(additional_text) = &self.explanation { + error.set_text(format!( + "{} {}", + self.error_code.to_string(), + additional_text + )); + } else { + error.set_text(self.error_code.to_string()); + } + + if let Some(id) = self.element_id { + error.set_element(id); + } + error.set_code(self.error_code as u32); + + error + } +} + +/// [Medea]'s [Control API] errors. +/// +/// [Medea]: https://github.com/instrumentisto/medea +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[derive(Debug, Display)] +pub enum ErrorCode { + /// Unimplemented API call. + /// + /// This code should be with additional text which explains what + /// exactly unimplemented (you can do it with + /// [`ErrorResponse::with_explanation`] function). + /// + /// Code: __1000__. + #[display(fmt = "Unimplemented API call.")] + UnimplementedCall = 1000, + + /// Request doesn't contain any elements. + /// + /// Code: __1001__. + #[display(fmt = "Request doesn't contain any elements")] + NoElement = 1001, + + /// Provided fid can't point to provided element. + /// + /// Code: __1002__. + #[display(fmt = "Provided fid can't point to provided element")] + ElementIdMismatch = 1002, + + /// Room not found. + /// + /// Code: __1003__. + #[display(fmt = "Room not found.")] + RoomNotFound = 1003, + + /// Member not found. + /// + /// Code: __1004__. + #[display(fmt = "Member not found.")] + MemberNotFound = 1004, + + /// Endpoint not found. + /// + /// Code: __1005__. + #[display(fmt = "Endpoint not found.")] + EndpointNotFound = 1005, + + /// Medea expects `Room` element in pipeline but received not him. + /// + /// Code: __1006__. + #[display(fmt = "Expecting Room element but it's not.")] + NotRoomInSpec = 1006, + + /// Medea expects `Member` element in pipeline but received not him. + /// + /// Code: __1007__. + #[display(fmt = "Expected Member element but it's not.")] + NotMemberInSpec = 1007, + + /// Invalid source URI in [`WebRtcPlayEndpoint`]. + /// + /// Code: __1008__. + /// + /// [`WebRtcPlayEndpoint`]: + /// crate::signalling::elements::endpoints::webrtc::WebRtcPlayEndpoint + #[display(fmt = "Invalid source URI in 'WebRtcPlayEndpoint'.")] + InvalidSrcUri = 1008, + + /// Provided not source URI in [`WebRtcPlayEndpoint`]. + /// + /// Code: __1009__. + /// + /// [`WebRtcPlayEndpoint`]: + /// crate::signalling::elements::endpoints::webrtc::WebRtcPlayEndpoint + #[display(fmt = "Provided not source URI in 'WebRtcPlayEndpoint'.")] + NotSourceUri = 1009, + + /// Element's URI don't have `local://` prefix. + /// + /// Code: __1010__. + #[display(fmt = "Element's URI don't have 'local://' prefix.")] + ElementIdIsNotLocal = 1010, + + /// Provided element's FID/URI with too many paths. + /// + /// Code: __1011__. + #[display(fmt = "You provided element's FID/URI with too many paths.")] + ElementIdIsTooLong = 1011, + + /// Missing some fields in source URI of WebRtcPublishEndpoint. + /// + /// Code: __1012__. + #[display( + fmt = "Missing some fields in source URI of WebRtcPublishEndpoint." + )] + MissingFieldsInSrcUri = 1012, + + /// Empty element ID. + /// + /// Code: __1013__. + #[display(fmt = "Provided empty element ID.")] + EmptyElementId = 1013, + + /// Provided empty elements FIDs list. + /// + /// Code: __1014__. + #[display(fmt = "Provided empty elements FIDs list.")] + EmptyElementsList = 1014, + + /// Provided not the same Room IDs in elements IDs. Probably you try use + /// `Delete` method for elements with different Room IDs + /// + /// Code: __1015__. + /// + /// [`RoomId`]: crate::api::control::room::Id + #[display(fmt = "Provided not the same Room IDs in elements IDs. \ + Probably you try use 'Delete' method for elements with \ + different Room IDs")] + ProvidedNotSameRoomIds = 1015, + + /// Room with provided fid already exists. + /// + /// Code: __1016__. + #[display(fmt = "Room with provided FID already exists.")] + RoomAlreadyExists = 1016, + + /// Member with provided FID already exists. + /// + /// Code: __1017__. + #[display(fmt = "Member with provided FID already exists.")] + MemberAlreadyExists = 1017, + + /// Endpoint with provided FID already exists. + /// + /// Code: __1018__. + #[display(fmt = "Endpoint with provided FID already exists.")] + EndpointAlreadyExists = 1018, + + /// Missing path in some reference to the Medea element. + /// + /// Code: __1019__. + #[display(fmt = "Missing path in some reference to the Medea element.")] + MissingPath = 1019, + + /// Unexpected server error. + /// + /// Use this [`ErrorCode`] only with [`ErrorResponse::unexpected`] + /// function. In error text with this code should be error message + /// which explain what exactly goes wrong + /// ([`ErrorResponse::unexpected`] do this). + /// + /// Code: __2000__. + #[display(fmt = "Unexpected error happened.")] + UnexpectedError = 2000, +} + +impl From for ErrorResponse { + fn from(err: ParticipantServiceErr) -> Self { + use ParticipantServiceErr::*; + + match err { + EndpointNotFound(id) => Self::new(ErrorCode::EndpointNotFound, &id), + ParticipantNotFound(id) => { + Self::new(ErrorCode::MemberNotFound, &id) + } + TurnServiceErr(_) | MemberError(_) => Self::unexpected(&err), + } + } +} + +impl From for ErrorResponse { + fn from(err: TryFromProtobufError) -> Self { + use TryFromProtobufError::*; + + match err { + SrcUriError(e) => e.into(), + NotMemberElementInRoomElement(id) => Self::with_explanation( + ErrorCode::UnimplementedCall, + String::from( + "Not Member elements in Room element currently is \ + unimplemented.", + ), + Some(id), + ), + UnimplementedEndpoint(id) => Self::with_explanation( + ErrorCode::UnimplementedCall, + String::from("Endpoint is not implemented."), + Some(id), + ), + ExpectedOtherElement(element, id) => Self::with_explanation( + ErrorCode::ElementIdMismatch, + format!( + "Provided fid can not point to element of type [{}]", + element + ), + Some(id), + ), + EmptyElement(id) => Self::with_explanation( + ErrorCode::NoElement, + String::from("No element was provided"), + Some(id), + ), + } + } +} + +impl From for ErrorResponse { + fn from(err: LocalUriParseError) -> Self { + use LocalUriParseError::*; + + match err { + NotLocal(text) => Self::new(ErrorCode::ElementIdIsNotLocal, &text), + TooManyPaths(text) => { + Self::new(ErrorCode::ElementIdIsTooLong, &text) + } + Empty => Self::without_id(ErrorCode::EmptyElementId), + MissingPaths(text) => { + Self::new(ErrorCode::MissingFieldsInSrcUri, &text) + } + UrlParseErr(id, _) => Self::new(ErrorCode::InvalidSrcUri, &id), + } + } +} + +impl From for ErrorResponse { + fn from(err: ParseFidError) -> Self { + use ParseFidError::*; + + match err { + TooManyPaths(text) => { + Self::new(ErrorCode::ElementIdIsTooLong, &text) + } + Empty => Self::without_id(ErrorCode::EmptyElementId), + MissingPath(text) => Self::new(ErrorCode::MissingPath, &text), + } + } +} + +impl From for ErrorResponse { + fn from(err: RoomError) -> Self { + use RoomError::*; + + match err { + MemberError(e) => e.into(), + MembersLoadError(e) => e.into(), + ParticipantServiceErr(e) => e.into(), + MemberAlreadyExists(id) => { + Self::new(ErrorCode::MemberAlreadyExists, &id) + } + EndpointAlreadyExists(id) => { + Self::new(ErrorCode::EndpointAlreadyExists, &id) + } + WrongRoomId(_, _) + | PeerNotFound(_) + | NoTurnCredentials(_) + | ConnectionNotExists(_) + | UnableToSendEvent(_) + | PeerError(_) + | TryFromElementError(_) + | BadRoomSpec(_) + | TurnServiceError(_) + | ClientError(_) => Self::unexpected(&err), + } + } +} + +impl From for ErrorResponse { + fn from(err: MembersLoadError) -> Self { + use MembersLoadError::*; + + match err { + TryFromError(e, id) => match e { + TryFromElementError::NotMember => { + Self::new(ErrorCode::NotMemberInSpec, &id) + } + TryFromElementError::NotRoom => { + Self::new(ErrorCode::NotRoomInSpec, &id) + } + }, + MemberNotFound(id) => Self::new(ErrorCode::MemberNotFound, &id), + EndpointNotFound(id) => Self::new(ErrorCode::EndpointNotFound, &id), + } + } +} + +impl From for ErrorResponse { + fn from(err: MemberError) -> Self { + match err { + MemberError::EndpointNotFound(id) => { + Self::new(ErrorCode::EndpointNotFound, &id) + } + } + } +} + +impl From for ErrorResponse { + fn from(err: SrcParseError) -> Self { + use SrcParseError::*; + + match err { + NotSrcUri(text) => Self::new(ErrorCode::NotSourceUri, &text), + LocalUriParseError(err) => err.into(), + } + } +} + +impl From for ErrorResponse { + fn from(err: RoomServiceError) -> Self { + use RoomServiceError::*; + + match err { + RoomNotFound(id) => Self::new(ErrorCode::RoomNotFound, &id), + RoomAlreadyExists(id) => { + Self::new(ErrorCode::RoomAlreadyExists, &id) + } + RoomError(e) => e.into(), + EmptyUrisList => Self::without_id(ErrorCode::EmptyElementsList), + NotSameRoomIds(id1, id2) => Self::with_explanation( + ErrorCode::ProvidedNotSameRoomIds, + format!( + "All FID's must have equal room_id. Provided Id's are \ + different: [{}] != [{}]", + id1, id2 + ), + None, + ), + RoomMailboxErr(_) + | FailedToLoadStaticSpecs(_) + | TryFromElement(_) => Self::unexpected(&err), + } + } +} + +impl From for ErrorResponse { + fn from(err: GrpcControlApiError) -> Self { + use GrpcControlApiError::*; + + match err { + Fid(e) => e.into(), + TryFromProtobuf(e) => e.into(), + RoomServiceError(e) => e.into(), + RoomServiceMailboxError(_) => Self::unexpected(&err), + } + } +} diff --git a/src/api/control/grpc/mod.rs b/src/api/control/grpc/mod.rs new file mode 100644 index 000000000..faabc3c40 --- /dev/null +++ b/src/api/control/grpc/mod.rs @@ -0,0 +1,5 @@ +//! Implementation of [Control API] gRPC server. +//! +//! [Control API]: https://tinyurl.com/yxsqplq7 + +pub mod server; diff --git a/src/api/control/grpc/server.rs b/src/api/control/grpc/server.rs new file mode 100644 index 000000000..034af56bc --- /dev/null +++ b/src/api/control/grpc/server.rs @@ -0,0 +1,445 @@ +//! Implementation of [Control API] gRPC server. +//! +//! [Control API]: https://tinyurl.com/yxsqplq7 + +use std::{ + collections::HashMap, + convert::{From, TryFrom}, + sync::Arc, +}; + +use actix::{ + Actor, Addr, Arbiter, Context, Handler, MailboxError, ResponseFuture, +}; +use derive_more::{Display, From}; +use failure::Fail; +use futures::future::{self, Future, IntoFuture}; +use grpcio::{Environment, RpcContext, Server, ServerBuilder, UnarySink}; +use medea_control_api_proto::grpc::{ + api::{ + CreateRequest, CreateRequest_oneof_el as CreateRequestOneof, + CreateResponse, Element, GetResponse, IdRequest, Response, + }, + api_grpc::{create_control_api, ControlApi}, +}; + +use crate::{ + api::control::{ + endpoints::{WebRtcPlayEndpoint, WebRtcPublishEndpoint}, + error_codes::{ + ErrorCode, + ErrorCode::{ElementIdIsTooLong, ElementIdMismatch}, + ErrorResponse, + }, + refs::{fid::ParseFidError, Fid, StatefulFid, ToMember, ToRoom}, + EndpointId, EndpointSpec, MemberId, MemberSpec, RoomSpec, + TryFromProtobufError, + }, + log::prelude::*, + shutdown::ShutdownGracefully, + signalling::room_service::{ + CreateEndpointInRoom, CreateMemberInRoom, CreateRoom, DeleteElements, + Get, RoomService, RoomServiceError, Sids, + }, + AppContext, +}; + +/// Errors which can happen while processing requests to gRPC [Control API]. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[derive(Debug, Display, Fail, From)] +pub enum GrpcControlApiError { + /// Error while parsing [`Fid`] of element. + Fid(ParseFidError), + + /// Error which can happen while converting protobuf objects into interior + /// [medea] [Control API] objects. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + /// [medea]: https://github.com/instrumentisto/medea + TryFromProtobuf(TryFromProtobufError), + + /// [`MailboxError`] for [`RoomService`]. + #[display(fmt = "Room service mailbox error: {:?}", _0)] + RoomServiceMailboxError(MailboxError), + + /// Wrapper around [`RoomServiceError`]. + RoomServiceError(RoomServiceError), +} + +/// Service which provides gRPC [Control API] implementation. +#[derive(Clone)] +struct ControlApiService { + /// [`Addr`] of [`RoomService`]. + room_service: Addr, + + /// Public URL of server. Address for exposed [Client API]. + /// + /// [Client API]: https://tinyurl.com/yx9thsnr + public_url: String, +} + +impl ControlApiService { + /// Implementation of `Create` method for [`Room`]. + fn create_room( + &self, + spec: RoomSpec, + ) -> impl Future { + self.room_service + .send(CreateRoom { spec }) + .map_err(GrpcControlApiError::RoomServiceMailboxError) + .and_then(move |r| r.map_err(GrpcControlApiError::from)) + } + + /// Implementation of `Create` method for [`Member`] element. + fn create_member( + &self, + id: MemberId, + parent_fid: Fid, + spec: MemberSpec, + ) -> impl Future { + self.room_service + .send(CreateMemberInRoom { + id, + parent_fid, + spec, + }) + .map_err(GrpcControlApiError::RoomServiceMailboxError) + .and_then(|r| r.map_err(GrpcControlApiError::from)) + } + + /// Implementation of `Create` method for [`Endpoint`] element. + fn create_endpoint( + &self, + id: EndpointId, + parent_fid: Fid, + spec: EndpointSpec, + ) -> impl Future { + self.room_service + .send(CreateEndpointInRoom { + id, + parent_fid, + spec, + }) + .map_err(GrpcControlApiError::RoomServiceMailboxError) + .and_then(|r| r.map_err(GrpcControlApiError::from)) + } + + /// Creates element based on provided [`CreateRequest`]. + pub fn create_element( + &self, + mut req: CreateRequest, + ) -> Box + Send> { + let unparsed_parent_fid = req.take_parent_fid(); + let elem = if let Some(elem) = req.el { + elem + } else { + return Box::new(future::err(ErrorResponse::new( + ErrorCode::NoElement, + &unparsed_parent_fid, + ))); + }; + + if unparsed_parent_fid.is_empty() { + return Box::new( + RoomSpec::try_from(elem) + .map_err(ErrorResponse::from) + .map(|spec| { + self.create_room(spec).map_err(ErrorResponse::from) + }) + .into_future() + .and_then(|create_result| create_result), + ); + } + + let parent_fid = match StatefulFid::try_from(unparsed_parent_fid) { + Ok(parent_fid) => parent_fid, + Err(e) => { + return Box::new(future::err(e.into())); + } + }; + + match parent_fid { + StatefulFid::Room(parent_fid) => match elem { + CreateRequestOneof::member(mut member) => { + let id: MemberId = member.take_id().into(); + Box::new( + MemberSpec::try_from(member) + .map_err(ErrorResponse::from) + .map(|spec| { + self.create_member(id, parent_fid, spec) + .map_err(ErrorResponse::from) + }) + .into_future() + .and_then(|create_result| create_result), + ) + } + _ => Box::new(future::err(ErrorResponse::new( + ElementIdMismatch, + &parent_fid, + ))), + }, + StatefulFid::Member(parent_fid) => { + let (endpoint, id) = match elem { + CreateRequestOneof::webrtc_play(mut play) => ( + WebRtcPlayEndpoint::try_from(&play) + .map(EndpointSpec::from), + play.take_id().into(), + ), + CreateRequestOneof::webrtc_pub(mut publish) => ( + Ok(WebRtcPublishEndpoint::from(&publish)) + .map(EndpointSpec::from), + publish.take_id().into(), + ), + _ => { + return Box::new(future::err(ErrorResponse::new( + ElementIdMismatch, + &parent_fid, + ))) + } + }; + Box::new( + endpoint + .map_err(ErrorResponse::from) + .map(move |spec| { + self.create_endpoint(id, parent_fid, spec) + .map_err(ErrorResponse::from) + }) + .into_future() + .and_then(|create_res| create_res), + ) + } + StatefulFid::Endpoint(_) => Box::new(future::err( + ErrorResponse::new(ElementIdIsTooLong, &parent_fid), + )), + } + } + + /// Deletes element by [`IdRequest`]. + pub fn delete_element( + &self, + mut req: IdRequest, + ) -> impl Future { + let mut delete_elements_msg = DeleteElements::new(); + for id in req.take_fid().into_iter() { + match StatefulFid::try_from(id) { + Ok(fid) => { + delete_elements_msg.add_fid(fid); + } + Err(e) => { + return future::Either::A(future::err(e.into())); + } + } + } + + future::Either::B( + delete_elements_msg + .validate() + .map_err(ErrorResponse::from) + .map(|msg| self.room_service.send(msg)) + .into_future() + .and_then(move |delete_result| { + delete_result.map_err(|err| { + ErrorResponse::from( + GrpcControlApiError::RoomServiceMailboxError(err), + ) + }) + }) + .and_then(|result| result.map_err(ErrorResponse::from)), + ) + } + + /// Returns requested by [`IdRequest`] [`Element`]s serialized to protobuf. + pub fn get_element( + &self, + mut req: IdRequest, + ) -> impl Future, Error = ErrorResponse> + { + let mut fids = Vec::new(); + for id in req.take_fid().into_iter() { + match StatefulFid::try_from(id) { + Ok(fid) => { + fids.push(fid); + } + Err(e) => { + return future::Either::A(future::err(e.into())); + } + } + } + + future::Either::B( + self.room_service + .send(Get(fids)) + .map_err(GrpcControlApiError::RoomServiceMailboxError) + .and_then(|r| r.map_err(GrpcControlApiError::from)) + .map(|elements: HashMap| { + elements + .into_iter() + .map(|(id, value)| (id.to_string(), value)) + .collect() + }) + .map_err(ErrorResponse::from), + ) + } +} + +impl ControlApi for ControlApiService { + /// Implementation for `Create` method of gRPC [Control API]. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + fn create( + &mut self, + ctx: RpcContext, + req: CreateRequest, + sink: UnarySink, + ) { + ctx.spawn( + self.create_element(req) + .then(move |result| { + let mut response = CreateResponse::new(); + match result { + Ok(sid) => { + response.set_sid(sid); + } + Err(e) => response.set_error(e.into()), + } + sink.success(response) + }) + .map_err(|e| { + warn!( + "Error while sending Create response by gRPC. {:?}", + e + ) + }), + ); + } + + /// Implementation for `Delete` method of gRPC [Control API]. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + fn delete( + &mut self, + ctx: RpcContext, + req: IdRequest, + sink: UnarySink, + ) { + ctx.spawn( + self.delete_element(req) + .then(move |result| { + let mut response = Response::new(); + if let Err(e) = result { + response.set_error(e.into()); + } + sink.success(response) + }) + .map_err(|e| { + warn!( + "Error while sending response on 'Delete' request by \ + gRPC: {:?}", + e + ) + }), + ); + } + + /// Implementation for `Get` method of gRPC [Control API]. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + fn get( + &mut self, + ctx: RpcContext, + req: IdRequest, + sink: UnarySink, + ) { + ctx.spawn( + self.get_element(req) + .then(|result| { + let mut response = GetResponse::new(); + match result { + Ok(elements) => { + response.set_elements( + elements + .into_iter() + .map(|(id, value)| (id.to_string(), value)) + .collect(), + ); + } + Err(e) => { + response.set_error(e.into()); + } + } + sink.success(response) + }) + .map_err(|e| { + warn!( + "Error while sending response on 'Get' request by \ + gRPC: {:?}", + e + ) + }), + ); + } +} + +/// Actor wrapper for [`grpcio`] gRPC server which provides dynamic [Control +/// API]. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[allow(clippy::module_name_repetitions)] +pub struct GrpcServer(Server); + +impl Actor for GrpcServer { + type Context = Context; + + fn started(&mut self, _ctx: &mut Self::Context) { + self.0.start(); + info!("gRPC Control API server started."); + } +} + +impl Handler for GrpcServer { + type Result = ResponseFuture<(), ()>; + + fn handle( + &mut self, + _: ShutdownGracefully, + _: &mut Self::Context, + ) -> Self::Result { + info!( + "gRPC Control API server received ShutdownGracefully message so \ + shutting down.", + ); + Box::new(self.0.shutdown().map_err(|e| { + warn!( + "Error while graceful shutdown of gRPC Control API server: \ + {:?}", + e + ) + })) + } +} + +/// Run gRPC [Control API] server in actix actor. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +pub fn run(room_repo: Addr, app: &AppContext) -> Addr { + let bind_ip = app.config.server.control.grpc.bind_ip.to_string(); + let bind_port = app.config.server.control.grpc.bind_port; + let cq_count = 2; + + let service = create_control_api(ControlApiService { + public_url: app.config.server.client.http.public_url.clone(), + room_service: room_repo, + }); + let env = Arc::new(Environment::new(cq_count)); + + info!("Starting gRPC server on {}:{}", bind_ip, bind_port); + + let server = ServerBuilder::new(env) + .register_service(service) + .bind(bind_ip, bind_port) + .build() + .unwrap(); + + GrpcServer::start_in_arbiter(&Arbiter::new(), move |_| GrpcServer(server)) +} diff --git a/src/api/control/member.rs b/src/api/control/member.rs index 05c99dcea..16ff55061 100644 --- a/src/api/control/member.rs +++ b/src/api/control/member.rs @@ -1,19 +1,30 @@ //! Definitions and implementations of [Control API]'s `Member` element. //! -//! [Control API]: http://tiny.cc/380uaz +//! [Control API]: https://tinyurl.com/yxsqplq7 -use std::convert::TryFrom; +use std::{collections::HashMap, convert::TryFrom}; use derive_more::{Display, From}; +use medea_control_api_proto::grpc::api::{ + CreateRequest_oneof_el as ElementProto, Member as MemberProto, + Room_Element_oneof_el as RoomElementProto, +}; +use rand::{distributions::Alphanumeric, Rng}; use serde::Deserialize; -use super::{ - endpoint::{WebRtcPlayEndpoint, WebRtcPublishEndpoint}, +use crate::api::control::{ + endpoints::{ + webrtc_play_endpoint::WebRtcPlayEndpoint, + webrtc_publish_endpoint::{WebRtcPublishEndpoint, WebRtcPublishId}, + }, pipeline::Pipeline, room::RoomElement, - TryFromElementError, + EndpointId, EndpointSpec, TryFromElementError, TryFromProtobufError, + WebRtcPlayId, }; +const CREDENTIALS_LEN: usize = 32; + /// ID of `Member`. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, From, Display)] pub struct Id(pub String); @@ -26,15 +37,15 @@ pub struct Id(pub String); #[serde(tag = "kind")] pub enum MemberElement { /// Represent [`WebRtcPublishEndpoint`]. - /// Can transform into [`Endpoint`] enum by `Endpoint::try_from`. + /// Can transform into [`EndpointSpec`] enum by `EndpointSpec::try_from`. /// - /// [`Endpoint`]: crate::api::control::endpoint::Endpoint + /// [`EndpointSpec`]: crate::api::control::endpoints::EndpointSpec WebRtcPublishEndpoint { spec: WebRtcPublishEndpoint }, /// Represent [`WebRtcPlayEndpoint`]. - /// Can transform into [`Endpoint`] enum by `Endpoint::try_from`. + /// Can transform into [`EndpointSpec`] enum by `EndpointSpec::try_from`. /// - /// [`Endpoint`]: crate::api::control::endpoint::Endpoint + /// [`EndpointSpec`]: crate::api::control::endpoints::EndpointSpec WebRtcPlayEndpoint { spec: WebRtcPlayEndpoint }, } @@ -43,19 +54,30 @@ pub enum MemberElement { #[derive(Clone, Debug)] pub struct MemberSpec { /// Spec of this `Member`. - pipeline: Pipeline, + pipeline: Pipeline, /// Credentials to authorize `Member` with. credentials: String, } +impl Into for MemberSpec { + fn into(self) -> RoomElement { + RoomElement::Member { + spec: self.pipeline, + credentials: self.credentials, + } + } +} + impl MemberSpec { /// Returns all [`WebRtcPlayEndpoint`]s of this [`MemberSpec`]. pub fn play_endpoints( &self, - ) -> impl Iterator { + ) -> impl Iterator { self.pipeline.iter().filter_map(|(id, e)| match e { - MemberElement::WebRtcPlayEndpoint { spec } => Some((id, spec)), + MemberElement::WebRtcPlayEndpoint { spec } => { + Some((id.clone().into(), spec)) + } _ => None, }) } @@ -63,9 +85,9 @@ impl MemberSpec { /// Lookups [`WebRtcPublishEndpoint`] by ID. pub fn get_publish_endpoint_by_id( &self, - id: &str, + id: WebRtcPublishId, ) -> Option<&WebRtcPublishEndpoint> { - let e = self.pipeline.get(id)?; + let e = self.pipeline.get(&id.into())?; if let MemberElement::WebRtcPublishEndpoint { spec } = e { Some(spec) } else { @@ -76,9 +98,11 @@ impl MemberSpec { /// Returns all [`WebRtcPublishEndpoint`]s of this [`MemberSpec`]. pub fn publish_endpoints( &self, - ) -> impl Iterator { + ) -> impl Iterator { self.pipeline.iter().filter_map(|(id, e)| match e { - MemberElement::WebRtcPublishEndpoint { spec } => Some((id, spec)), + MemberElement::WebRtcPublishEndpoint { spec } => { + Some((id.clone().into(), spec)) + } _ => None, }) } @@ -89,6 +113,71 @@ impl MemberSpec { } } +/// Generates alphanumeric credentials for [`Member`] with +/// [`CREDENTIALS_LEN`] length. +/// +/// This credentials will be generated if in dynamic [Control API] spec not +/// provided credentials for [`Member`]. This logic you can find in [`TryFrom`] +/// [`MemberProto`] implemented for [`MemberSpec`]. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +fn generate_member_credentials() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(CREDENTIALS_LEN) + .collect() +} + +impl TryFrom for MemberSpec { + type Error = TryFromProtobufError; + + fn try_from(mut member: MemberProto) -> Result { + let mut pipeline = HashMap::new(); + for (id, member_element) in member.take_pipeline() { + if let Some(elem) = member_element.el { + let endpoint = + EndpointSpec::try_from((EndpointId(id.clone()), elem))?; + pipeline.insert(id.into(), endpoint.into()); + } else { + return Err(TryFromProtobufError::EmptyElement(id)); + } + } + + let mut credentials = member.take_credentials(); + if credentials.is_empty() { + credentials = generate_member_credentials(); + } + + Ok(Self { + pipeline: Pipeline::new(pipeline), + credentials, + }) + } +} + +macro_rules! impl_try_from_proto_for_member { + ($proto:tt) => { + impl TryFrom<(Id, $proto)> for MemberSpec { + type Error = TryFromProtobufError; + + fn try_from( + (id, proto): (Id, $proto), + ) -> Result { + match proto { + $proto::member(member) => Self::try_from(member), + _ => Err(TryFromProtobufError::ExpectedOtherElement( + String::from("Member"), + id.to_string(), + )), + } + } + } + }; +} + +impl_try_from_proto_for_member!(RoomElementProto); +impl_try_from_proto_for_member!(ElementProto); + impl TryFrom<&RoomElement> for MemberSpec { type Error = TryFromElementError; diff --git a/src/api/control/mod.rs b/src/api/control/mod.rs index 79ec7522e..41bb5cfbc 100644 --- a/src/api/control/mod.rs +++ b/src/api/control/mod.rs @@ -1,42 +1,93 @@ //! Implementation and definitions of [Control API] specs. //! -//! [Control API]: http://tiny.cc/380uaz +//! [Control API]: https://tinyurl.com/yxsqplq7 -pub mod endpoint; +pub mod endpoints; +pub mod error_codes; +pub mod grpc; pub mod member; pub mod pipeline; +pub mod refs; pub mod room; -use std::{ - convert::TryFrom as _, - fs::{File, ReadDir}, - io::Read as _, - path::Path, -}; +use std::{convert::TryFrom as _, fs::File, io::Read as _, path::Path}; +use actix::Addr; use derive_more::Display; -use failure::{Error, Fail}; +use failure::Fail; +use futures::Future; use serde::Deserialize; -use self::pipeline::Pipeline; +use crate::{ + log::prelude::*, + signalling::room_service::{ + RoomService, RoomServiceError, StartStaticRooms, + }, +}; +use self::{pipeline::Pipeline, refs::src_uri::SrcParseError}; + +#[doc(inline)] pub use self::{ - endpoint::Endpoint, + endpoints::{ + webrtc_play_endpoint::WebRtcPlayId, + webrtc_publish_endpoint::WebRtcPublishId, EndpointSpec, + Id as EndpointId, + }, member::{Id as MemberId, MemberSpec}, room::{Id as RoomId, RoomElement, RoomSpec}, }; +/// Errors which may occur while deserializing protobuf spec. +#[derive(Debug, Fail, Display)] +pub enum TryFromProtobufError { + /// Error while parsing [`SrcUri`] of [`WebRtcPlayEndpoint`]. + /// + /// [`WebRtcPlayEndpoint`]: + /// crate::api::control::endpoints::WebRtcPlayEndpoint + /// [`SrcUri`]: + /// crate::api::control::endpoints::webrtc_play_endpoint::SrcUri + #[display(fmt = "Src uri parse error: {:?}", _0)] + SrcUriError(SrcParseError), + + /// `Room` element doesn't have `Member` element. Currently this is + /// unimplemented. + #[display( + fmt = "Room element [id = {}] doesn't have Member element. Currently \ + this is unimplemented.", + _0 + )] + NotMemberElementInRoomElement(String), + + /// `Room` element doesn't have `Member` element. Currently this is + /// unimplemented. + #[display(fmt = "Expected element of type [{}]. Id [{}]", _0, _1)] + ExpectedOtherElement(String, String), + + #[display(fmt = "Element is None, expected Some. Id [{}]", _0)] + EmptyElement(String), + + #[display(fmt = "Endpoint is unimplemented. Id [{}]", _0)] + UnimplementedEndpoint(String), +} + +impl From for TryFromProtobufError { + fn from(from: SrcParseError) -> Self { + Self::SrcUriError(from) + } +} + /// Root elements of [Control API] spec. /// -/// [Control API]: http://tiny.cc/380uaz +/// [Control API]: https://tinyurl.com/yxsqplq7 #[derive(Clone, Deserialize, Debug)] #[serde(tag = "kind")] pub enum RootElement { - /// Represent [`RoomSpec`]. + /// Represents [`RoomSpec`]. /// Can transform into [`RoomSpec`] by `RoomSpec::try_from`. Room { id: RoomId, - spec: Pipeline, + spec: Pipeline, }, } @@ -45,16 +96,78 @@ pub enum RootElement { /// /// [`TryFrom`]: std::convert::TryFrom #[allow(clippy::pub_enum_variant_names)] -#[derive(Debug, Display, Fail)] +#[derive(Clone, Debug, Display, Fail)] pub enum TryFromElementError { + /// Element is not `Room`. #[display(fmt = "Element is not Room")] NotRoom, - #[display(fmt = "Element is not Room")] + + /// Element is not `Member`. + #[display(fmt = "Element is not Member")] NotMember, } +/// Errors which can happen while loading static [Control API] specs. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[allow(clippy::pub_enum_variant_names)] +#[derive(Debug, Display, Fail)] +pub enum LoadStaticControlSpecsError { + /// Error while reading default or provided in config + /// (`MEDEA_CONTROL_API.STATIC_SPECS_DIR` environment variable) static + /// [Control API] specs dir. + /// + /// Atm we only should print `warn!` message to log which prints that + /// static specs not loaded. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + #[display(fmt = "Error while reading static control API specs dir.")] + SpecDirReadError(std::io::Error), + + /// I/O error while reading static [Control API] specs. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + #[display(fmt = "I/O error while reading specs. {:?}", _0)] + IoError(std::io::Error), + + /// Conflict in static [Control API] specs. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + #[display( + fmt = "Try from element error while loading static specs. {:?}", + _0 + )] + TryFromElementError(TryFromElementError), + + /// Error while deserialization static [Control API] specs from YAML file. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + #[display(fmt = "Error while deserialization static spec. {:?}", _0)] + YamlDeserializationError(serde_yaml::Error), +} + +impl From for LoadStaticControlSpecsError { + fn from(err: std::io::Error) -> Self { + Self::IoError(err) + } +} + +impl From for LoadStaticControlSpecsError { + fn from(err: TryFromElementError) -> Self { + Self::TryFromElementError(err) + } +} + +impl From for LoadStaticControlSpecsError { + fn from(err: serde_yaml::Error) -> Self { + Self::YamlDeserializationError(err) + } +} + /// Loads [`RoomSpec`] from file with YAML format. -pub fn load_from_yaml_file>(path: P) -> Result { +pub fn load_from_yaml_file>( + path: P, +) -> Result { let mut file = File::open(path)?; let mut buf = String::new(); file.read_to_string(&mut buf)?; @@ -63,15 +176,46 @@ pub fn load_from_yaml_file>(path: P) -> Result { Ok(room) } -/// Loads all [`RoomSpec`] from YAML files from provided [`ReadDir`]. -pub fn load_static_specs_from_dir( - dir: ReadDir, -) -> Result, Error> { +/// Loads all [`RoomSpec`] from YAML files from provided path. +pub fn load_static_specs_from_dir>( + path: P, +) -> Result, LoadStaticControlSpecsError> { let mut specs = Vec::new(); - for entry in dir { + for entry in std::fs::read_dir(path) + .map_err(LoadStaticControlSpecsError::SpecDirReadError)? + { let entry = entry?; let spec = load_from_yaml_file(entry.path())?; specs.push(spec) } Ok(specs) } + +/// Starts all [`Room`]s from static [Control API] specs. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +/// [`Room`]: crate::signalling::room::Room +pub fn start_static_rooms( + room_service: &Addr, +) -> impl Future { + room_service + .send(StartStaticRooms) + .map_err(|e| error!("StartStaticRooms mailbox error: {:?}", e)) + .map(|result| { + if let Err(e) = result { + match e { + RoomServiceError::FailedToLoadStaticSpecs(e) => match e { + LoadStaticControlSpecsError::SpecDirReadError(e) => { + warn!( + "Error while reading static control API specs \ + dir. Control API specs not loaded. {}", + e + ); + } + _ => panic!("{}", e), + }, + _ => panic!("{}", e), + } + } + }) +} diff --git a/src/api/control/pipeline.rs b/src/api/control/pipeline.rs index 7e812c23c..d94d1ad52 100644 --- a/src/api/control/pipeline.rs +++ b/src/api/control/pipeline.rs @@ -1,9 +1,10 @@ //! Definitions and implementations of [Control API]'s `Pipeline`. //! -//! [Control API]: http://tiny.cc/380uaz +//! [Control API]: https://tinyurl.com/yxsqplq7 use std::{ collections::{hash_map::Iter, HashMap}, + hash::Hash, iter::IntoIterator, }; @@ -11,27 +12,32 @@ use serde::Deserialize; /// Entity that represents some pipeline of spec. #[derive(Clone, Deserialize, Debug)] -pub struct Pipeline { - pipeline: HashMap, +pub struct Pipeline { + pipeline: HashMap, } -impl Pipeline { +impl Pipeline { + /// Creates new [`Pipeline`] from provided [`HashMap`]. + pub fn new(pipeline: HashMap) -> Self { + Self { pipeline } + } + /// Iterates over pipeline by reference. #[inline] - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.into_iter() } /// Lookups element of [`Pipeline`] by ID. #[inline] - pub fn get(&self, id: &str) -> Option<&T> { + pub fn get(&self, id: &K) -> Option<&V> { self.pipeline.get(id) } } -impl<'a, T> IntoIterator for &'a Pipeline { - type IntoIter = Iter<'a, String, T>; - type Item = (&'a String, &'a T); +impl<'a, K: Eq + Hash, V> IntoIterator for &'a Pipeline { + type IntoIter = Iter<'a, K, V>; + type Item = (&'a K, &'a V); #[inline] fn into_iter(self) -> Self::IntoIter { diff --git a/src/api/control/refs/fid.rs b/src/api/control/refs/fid.rs new file mode 100644 index 000000000..9071fec3a --- /dev/null +++ b/src/api/control/refs/fid.rs @@ -0,0 +1,252 @@ +//! Implementation of Full ID (`fid` in dynamic Control API specs). + +use std::{ + convert::{From, TryFrom}, + fmt::{Display, Error, Formatter}, +}; + +use derive_more::{Display, From}; +use failure::Fail; + +use crate::{api::control::RoomId, impls_for_stateful_refs}; + +use super::{ToEndpoint, ToMember, ToRoom}; + +/// Errors which can happen while parsing [`Fid`]. +#[derive(Display, Debug, Fail)] +pub enum ParseFidError { + #[display(fmt = "Fid is empty.")] + Empty, + + #[display(fmt = "Too many paths [fid = {}].", _0)] + TooManyPaths(String), + + #[display(fmt = "Missing paths [fid = {}]", _0)] + MissingPath(String), +} + +/// FID (full ID, or `fid` in Control API specs) is a composition of +/// media elements IDs, which refers to some media element on a whole server +/// uniquely. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct Fid { + state: T, +} + +impls_for_stateful_refs!(Fid); + +impl From for Fid { + fn from(from: StatefulFid) -> Self { + match from { + StatefulFid::Room(uri) => uri, + StatefulFid::Member(uri) => { + let (_, uri) = uri.take_member_id(); + uri + } + StatefulFid::Endpoint(uri) => { + let (_, uri) = uri.take_endpoint_id(); + let (_, uri) = uri.take_member_id(); + uri + } + } + } +} + +impl Display for Fid { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + write!(f, "{}", self.state.0) + } +} + +impl Display for Fid { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + write!(f, "{}/{}", self.state.0, self.state.1) + } +} + +impl Display for Fid { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + write!(f, "{}/{}/{}", self.state.0, self.state.1, self.state.2) + } +} + +/// Enum for storing [`Fid`]s in all states. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Display, From)] +pub enum StatefulFid { + Room(Fid), + Member(Fid), + Endpoint(Fid), +} + +impl StatefulFid { + /// Returns reference to [`RoomId`]. + /// + /// This is possible in any [`StatefulFid`] state. + pub fn room_id(&self) -> &RoomId { + match self { + StatefulFid::Room(uri) => uri.room_id(), + StatefulFid::Member(uri) => uri.room_id(), + StatefulFid::Endpoint(uri) => uri.room_id(), + } + } +} + +impl TryFrom for StatefulFid { + type Error = ParseFidError; + + fn try_from(value: String) -> Result { + if value.is_empty() { + return Err(ParseFidError::Empty); + } + + let mut splitted = value.split('/'); + let room_id = if let Some(room_id) = splitted.next() { + if room_id.is_empty() { + return Err(ParseFidError::MissingPath(value)); + } else { + room_id + } + } else { + return Err(ParseFidError::Empty); + }; + + let member_id = if let Some(member_id) = splitted.next() { + if member_id.is_empty() { + return Err(ParseFidError::MissingPath(value)); + } else { + member_id + } + } else { + return Ok(Fid::::new(room_id.to_string().into()).into()); + }; + + let endpoint_id = if let Some(endpoint_id) = splitted.next() { + if endpoint_id.is_empty() { + return Err(ParseFidError::MissingPath(value)); + } else { + endpoint_id + } + } else { + return Ok(Fid::::new( + room_id.to_string().into(), + member_id.to_string().into(), + ) + .into()); + }; + + if splitted.next().is_some() { + Err(ParseFidError::TooManyPaths(value)) + } else { + Ok(Fid::::new( + room_id.to_string().into(), + member_id.to_string().into(), + endpoint_id.to_string().into(), + ) + .into()) + } + } +} + +#[cfg(test)] +mod specs { + use crate::api::control::{EndpointId, MemberId}; + + use super::*; + + #[test] + fn returns_error_on_missing_path() { + for fid_str in &[ + "room_id//endpoint_id", + "//endpoint_id", + "//member_id/endpoint_id", + "/member_id", + ] { + match StatefulFid::try_from(fid_str.to_string()) { + Ok(f) => unreachable!("Unexpected successful parse: {}", f), + Err(e) => match e { + ParseFidError::MissingPath(_) => (), + _ => unreachable!("Throwed some unexpected error {:?}.", e), + }, + } + } + } + + #[test] + fn returns_error_on_too_many_paths() { + for fid_str in &[ + "room_id/member_id/endpoint_id/something_else", + "room_id/member_id/endpoint_id/", + "room_id/member_id/endpoint_id////", + ] { + match StatefulFid::try_from(fid_str.to_string()) { + Ok(f) => unreachable!("Unexpected successful parse: {}", f), + Err(e) => match e { + ParseFidError::TooManyPaths(_) => (), + _ => unreachable!("Throwed some unexpected error {:?}.", e), + }, + } + } + } + + #[test] + fn successful_parse_to_room() { + let room_id: RoomId = "room_id".to_string().into(); + let fid = StatefulFid::try_from(format!("{}", room_id)).unwrap(); + match fid { + StatefulFid::Room(room_fid) => { + assert_eq!(room_fid.room_id(), &room_id); + } + _ => unreachable!("Fid parsed not to Room. {}", fid), + } + } + + #[test] + fn successful_parse_to_member() { + let room_id: RoomId = "room_id".to_string().into(); + let member_id: MemberId = "member_id".to_string().into(); + let fid = StatefulFid::try_from(format!("{}/{}", room_id, member_id)) + .unwrap(); + + match fid { + StatefulFid::Member(member_fid) => { + assert_eq!(member_fid.room_id(), &room_id); + assert_eq!(member_fid.member_id(), &member_id); + } + _ => unreachable!("Fid parsed not to Member. {}", fid), + } + } + + #[test] + fn successful_parse_to_endpoint() { + let room_id: RoomId = "room_id".to_string().into(); + let member_id: MemberId = "member_id".to_string().into(); + let endpoint_id: EndpointId = "endpoint_id".to_string().into(); + let fid = StatefulFid::try_from(format!( + "{}/{}/{}", + room_id, member_id, endpoint_id + )) + .unwrap(); + + match fid { + StatefulFid::Endpoint(endpoint_fid) => { + assert_eq!(endpoint_fid.room_id(), &room_id); + assert_eq!(endpoint_fid.member_id(), &member_id); + assert_eq!(endpoint_fid.endpoint_id(), &endpoint_id); + } + _ => unreachable!("Fid parsed not to Member. {}", fid), + } + } + + #[test] + fn serializes_into_original_fid() { + for fid_str in &[ + "room_id", + "room_id/member_id", + "room_id/member_id/endpoint_id", + ] { + let fid = StatefulFid::try_from(fid_str.to_string()).unwrap(); + assert_eq!(fid_str.to_string(), fid.to_string()); + } + } +} diff --git a/src/api/control/refs/local_uri.rs b/src/api/control/refs/local_uri.rs new file mode 100644 index 000000000..490496c22 --- /dev/null +++ b/src/api/control/refs/local_uri.rs @@ -0,0 +1,381 @@ +//! URI for pointing to some Medea element in spec. + +// Fix clippy's wrong errors for `Self` in `LocalUri`s with states as generics. +#![allow(clippy::use_self)] + +use std::{convert::TryFrom, fmt, string::ToString}; + +use derive_more::{Display, From}; +use failure::Fail; +use url::Url; + +use crate::{ + api::control::{MemberId, RoomId}, + impls_for_stateful_refs, +}; + +use super::{SrcUri, ToEndpoint, ToMember, ToRoom}; + +/// URI in format `local://room_id/member_id/endpoint_id`. +/// +/// This kind of URI used for pointing to some element in spec ([`Room`], +/// [`Member`], [`WebRtcPlayEndpoint`], [`WebRtcPublishEndpoint`], etc) based on +/// state. +/// +/// [`LocalUri`] can be in three states: [`ToRoom`], [`ToMember`], +/// [`ToRoom`]. This is used for compile time guarantees that some +/// [`LocalUri`] have all mandatory fields. +/// +/// You also can take value from [`LocalUri`] without clone, but you have to do +/// it consistently. For example, if you wish to get [`RoomId`], [`MemberId`] +/// and [`Endpoint`] ID from [`LocalUri`] in [`ToEndpoint`] state you should +/// make this steps: +/// +/// ``` +/// # use crate::api::control::local_uri::{LocalUri, ToEndpoint}; +/// # use crate::api::control::{RoomId, MemberId, EndpointId}; +/// # +/// let orig_room_id = RoomId("room".to_string()); +/// let orig_member_id = MemberId("member".to_string()); +/// let orig_endpoint_id = EndpointId("endpoint".to_string()); +/// +/// // Create new LocalUri for endpoint. +/// let local_uri = LocalUri::::new( +/// orig_room_id.clone(), +/// orig_member_id.clone(), +/// orig_endpoint_id.clone() +/// ); +/// let local_uri_clone = local_uri.clone(); +/// +/// // We can get reference to room_id from this LocalUri +/// // without taking ownership: +/// assert_eq!(local_uri.room_id(), &orig_room_id); +/// +/// // If you want to take all IDs ownership, you should do this steps: +/// let (endpoint_id, member_uri) = local_uri.take_endpoint_id(); +/// assert_eq!(endpoint_id, orig_endpoint_id); +/// +/// let (member_id, room_uri) = member_uri.take_member_id(); +/// assert_eq!(member_id, orig_member_id); +/// +/// let room_id = room_uri.take_room_id(); +/// assert_eq!(room_id, orig_room_id); +/// +/// // Or simply +/// let (room_id, member_id, endpoint_id) = local_uri_clone.take_all(); +/// ``` +/// +/// This is necessary so that it is not possible to get the address in the +/// wrong state (`local://room_id//endpoint_id` for example). +/// +/// [`Member`]: crate::signalling::elements::member::Member +/// [`Room`]: crate::signalling::room::Room +/// [`WebRtcPlayEndpoint`]: +/// crate::signalling::elements::endpoints::webrtc::WebRtcPlayEndpoint +/// [`WebRtcPublishEndpoint`]: +/// crate::signalling::elements::endpoints::webrtc::WebRtcPublishEndpoint +/// [`Endpoint`]: crate::signalling::elements::endpoints::Endpoint +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct LocalUri { + state: T, +} + +impls_for_stateful_refs!(LocalUri); + +impl From for LocalUri { + fn from(from: StatefulLocalUri) -> Self { + match from { + StatefulLocalUri::Room(uri) => uri, + StatefulLocalUri::Member(uri) => { + let (_, uri) = uri.take_member_id(); + uri + } + StatefulLocalUri::Endpoint(uri) => { + let (_, uri) = uri.take_endpoint_id(); + let (_, uri) = uri.take_member_id(); + uri + } + } + } +} + +impl From for LocalUri { + fn from(uri: SrcUri) -> Self { + LocalUri::::new( + uri.room_id, + uri.member_id, + uri.endpoint_id.into(), + ) + } +} + +impl fmt::Display for LocalUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "local://{}", self.state.0) + } +} + +impl fmt::Display for LocalUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "local://{}/{}", self.state.0, self.state.1) + } +} + +impl fmt::Display for LocalUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "local://{}/{}/{}", + self.state.0, self.state.1, self.state.2 + ) + } +} + +/// Error which can happen while [`LocalUri`] parsing. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Fail, Display)] +pub enum LocalUriParseError { + /// Protocol of provided URI is not "local://". + #[display(fmt = "Provided URIs protocol is not 'local://'.")] + NotLocal(String), + + /// Too many paths in provided URI. + /// + /// `local://room_id/member_id/endpoint_id/redundant_path` for example. + #[display(fmt = "Too many paths in provided URI ({}).", _0)] + TooManyPaths(String), + + /// Some paths is missing in URI. + /// + /// `local://room_id//qwerty` for example. + #[display(fmt = "Missing fields. {}", _0)] + MissingPaths(String), + + /// Error while parsing URI by [`url::Url`]. + #[display(fmt = "Error while parsing URL. {:?}", _0)] + UrlParseErr(String, url::ParseError), + + /// Provided empty URI. + #[display(fmt = "You provided empty local uri.")] + Empty, +} + +/// Enum for storing [`LocalUri`]s in all states. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Display, From)] +pub enum StatefulLocalUri { + /// Stores [`LocalUri`] in [`ToRoom`] state. + Room(LocalUri), + + /// Stores [`LocalUri`] in [`ToMember`] state. + Member(LocalUri), + + /// Stores [`LocalUri`] in [`ToEndpoint`] state. + Endpoint(LocalUri), +} + +impl StatefulLocalUri { + /// Returns reference to [`RoomId`]. + /// + /// This is possible in any [`LocalUri`] state. + pub fn room_id(&self) -> &RoomId { + match self { + StatefulLocalUri::Room(uri) => uri.room_id(), + StatefulLocalUri::Member(uri) => uri.room_id(), + StatefulLocalUri::Endpoint(uri) => uri.room_id(), + } + } +} + +impl TryFrom for StatefulLocalUri { + type Error = LocalUriParseError; + + fn try_from(value: String) -> Result { + if value.is_empty() { + return Err(LocalUriParseError::Empty); + } + + let url = match Url::parse(&value) { + Ok(url) => url, + Err(err) => { + return Err(LocalUriParseError::UrlParseErr(value, err)) + } + }; + + if url.scheme() != "local" { + return Err(LocalUriParseError::NotLocal(value)); + } + + let room_uri = match url.host() { + Some(host) => { + let host = host.to_string(); + if host.is_empty() { + return Err(LocalUriParseError::MissingPaths(value)); + } else { + LocalUri::::new(host.into()) + } + } + None => return Err(LocalUriParseError::MissingPaths(value)), + }; + + let mut path = match url.path_segments() { + Some(path) => path, + None => return Ok(room_uri.into()), + }; + + let member_id = path + .next() + .filter(|id| !id.is_empty()) + .map(|id| MemberId(id.to_string())); + + let endpoint_id = path + .next() + .filter(|id| !id.is_empty()) + .map(ToString::to_string); + + if path.next().is_some() { + return Err(LocalUriParseError::TooManyPaths(value)); + } + + if let Some(member_id) = member_id { + let member_uri = room_uri.push_member_id(member_id); + if let Some(endpoint_id) = endpoint_id { + Ok(member_uri.push_endpoint_id(endpoint_id.into()).into()) + } else { + Ok(member_uri.into()) + } + } else if endpoint_id.is_some() { + Err(LocalUriParseError::MissingPaths(value)) + } else { + Ok(room_uri.into()) + } + } +} + +#[cfg(test)] +mod specs { + use super::*; + + #[test] + fn parse_local_uri_to_room_element() { + let local_uri = + StatefulLocalUri::try_from(String::from("local://room_id")) + .unwrap(); + if let StatefulLocalUri::Room(room) = local_uri { + assert_eq!(room.take_room_id(), RoomId("room_id".to_string())); + } else { + unreachable!( + "Local uri '{}' parsed to {:?} state but should be in \ + IsRoomId state.", + local_uri, local_uri + ); + } + } + + #[test] + fn parse_local_uri_to_element_of_room() { + let local_uri = StatefulLocalUri::try_from(String::from( + "local://room_id/room_element_id", + )) + .unwrap(); + if let StatefulLocalUri::Member(member) = local_uri { + let (element_id, room_uri) = member.take_member_id(); + assert_eq!(element_id, MemberId("room_element_id".to_string())); + let room_id = room_uri.take_room_id(); + assert_eq!(room_id, RoomId("room_id".to_string())); + } else { + unreachable!( + "Local URI '{}' parsed to {:?} state but should be in \ + IsMemberId state.", + local_uri, local_uri + ); + } + } + + #[test] + fn parse_local_uri_to_endpoint() { + let local_uri = StatefulLocalUri::try_from(String::from( + "local://room_id/room_element_id/endpoint_id", + )) + .unwrap(); + if let StatefulLocalUri::Endpoint(endpoint) = local_uri { + let (endpoint_id, member_uri) = endpoint.take_endpoint_id(); + assert_eq!(endpoint_id, String::from("endpoint_id").into()); + let (member_id, room_uri) = member_uri.take_member_id(); + assert_eq!(member_id, MemberId("room_element_id".to_string())); + let room_id = room_uri.take_room_id(); + assert_eq!(room_id, RoomId("room_id".to_string())); + } else { + unreachable!( + "Local URI '{}' parsed to {:?} state but should be in \ + IsEndpointId state.", + local_uri, local_uri + ); + } + } + + #[test] + fn returns_parse_error_if_local_uri_not_local() { + match StatefulLocalUri::try_from(String::from("not-local://room_id")) { + Ok(_) => unreachable!(), + Err(e) => match e { + LocalUriParseError::NotLocal(_) => (), + _ => unreachable!("Unreachable LocalUriParseError: {:?}", e), + }, + } + } + + #[test] + fn returns_parse_error_if_local_uri_empty() { + match StatefulLocalUri::try_from(String::from("")) { + Ok(_) => unreachable!(), + Err(e) => match e { + LocalUriParseError::Empty => (), + _ => unreachable!(), + }, + } + } + + #[test] + fn returns_error_if_local_uri_have_too_many_paths() { + match StatefulLocalUri::try_from(String::from( + "local://room/member/endpoint/too_many", + )) { + Ok(_) => unreachable!(), + Err(e) => match e { + LocalUriParseError::TooManyPaths(_) => (), + _ => unreachable!(), + }, + } + } + + #[test] + fn properly_serialize() { + for local_uri_str in &[ + "local://room_id", + "local://room_id/member_id", + "local://room_id/member_id/endpoint_id", + ] { + let local_uri = + StatefulLocalUri::try_from(local_uri_str.to_string()).unwrap(); + assert_eq!(local_uri_str.to_string(), local_uri.to_string()); + } + } + + #[test] + fn return_error_when_local_uri_not_full() { + for local_uri_str in &[ + "local://room_id//endpoint_id", + "local:////endpoint_id", + "local:///member_id/endpoint_id", + ] { + match StatefulLocalUri::try_from(local_uri_str.to_string()) { + Ok(_) => unreachable!(), + Err(e) => match e { + LocalUriParseError::MissingPaths(_) => (), + _ => unreachable!(), + }, + } + } + } +} diff --git a/src/api/control/refs/mod.rs b/src/api/control/refs/mod.rs new file mode 100644 index 000000000..111782674 --- /dev/null +++ b/src/api/control/refs/mod.rs @@ -0,0 +1,253 @@ +//! Implementation of all kinds of references to some resource used in Medea's +//! Control API. + +#![allow(clippy::use_self)] + +pub mod fid; +pub mod local_uri; +pub mod src_uri; + +use super::{EndpointId, MemberId, RoomId}; + +#[doc(inline)] +pub use self::{ + fid::{Fid, StatefulFid}, + local_uri::{LocalUri, StatefulLocalUri}, + src_uri::SrcUri, +}; + +/// State of reference which points to [`Room`]. +/// +/// [`Room`]: crate::signalling::room::Room +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct ToRoom(RoomId); + +/// State of reference which points to [`Member`]. +/// +/// [`Member`]: crate::signalling::elements::member::Member +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct ToMember(RoomId, MemberId); + +/// State of reference which points to [`Endpoint`]. +/// +/// [`Endpoint`]: crate::signalling::elements::endpoints::Endpoint +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct ToEndpoint(RoomId, MemberId, EndpointId); + +/// Generates functions for transition between [`ToRoom`], +/// [`ToMember`] and [`ToEndpoint`] states of Medea references and handy getters +/// for data of this references. +/// +/// Supposed that container for which you want to implement all this methods +/// is something like: +/// +/// ```rust +/// pub struct SomeReference { +/// state: T +/// } +/// ``` +/// +/// This is necessary so that you can write different implementations of +/// serializing and deserializing for references, but at the same time have some +/// standard API for working with them. +/// +/// [`ToRoom`]: crate::api::control::refs::ToRoom +/// [`ToMember`]: crate::api::control::refs::ToMember +/// [`ToEndpoint`]: crate::api::control::refs::ToEndpoint +#[macro_export] +macro_rules! impls_for_stateful_refs { + ($container:tt) => { + impl $container { + #[doc = "Create new reference in [`ToRoom`] state."] + pub fn new(room_id: $crate::api::control::RoomId) -> Self { + Self { + state: ToRoom(room_id), + } + } + + /// Returns borrowed [`RoomId`]. + /// + /// [`RoomId`]: crate::api::control::RoomId + pub fn room_id(&self) -> &$crate::api::control::RoomId { + &self.state.0 + } + + /// Returns [`RoomId`]. + /// + /// [`RoomId`]: crate::api::control::RoomId + pub fn take_room_id(self) -> $crate::api::control::RoomId { + self.state.0 + } + + /// Pushes [`MemberId`] to the end of URI and returns + /// reference in [`ToMember`] state. + /// + /// [`MemberId`]: crate::api::control::MemberId + /// [`ToMember`]: crate::api::control::refs::ToMember + pub fn push_member_id( + self, + member_id: $crate::api::control::MemberId, + ) -> $container { + $container::<$crate::api::control::refs::ToMember>::new( + self.state.0, + member_id, + ) + } + } + + impl $container<$crate::api::control::refs::ToMember> { + /// Create new reference in [`ToMember`] state. + /// + /// [`ToMember`]: crate::api::control::refs::ToMember + pub fn new( + room_id: $crate::api::control::RoomId, + member_id: $crate::api::control::MemberId, + ) -> Self { + Self { + state: $crate::api::control::refs::ToMember( + room_id, member_id, + ), + } + } + + /// Returns borrowed [`RoomId`]. + /// + /// [`RoomId`]: crate::api::control::RoomId + pub fn room_id(&self) -> &$crate::api::control::RoomId { + &self.state.0 + } + + /// Returns borrowed [`MemberId`]. + /// + /// [`MemberId`]: crate::api::control::MemberId + pub fn member_id(&self) -> &$crate::api::control::MemberId { + &self.state.1 + } + + /// Return [`MemberId`] and reference in state [`ToRoom`]. + /// + /// [`MemberId`]: crate::api::control::MemberId + /// [`ToRoom`]: crate::api::control::refs::ToRoom + pub fn take_member_id( + self, + ) -> ( + $crate::api::control::MemberId, + $container<$crate::api::control::refs::ToRoom>, + ) { + ( + self.state.1, + $container::<$crate::api::control::refs::ToRoom>::new( + self.state.0, + ), + ) + } + + /// Push endpoint ID to the end of URI and returns + /// reference in [`ToEndpoint`] state. + /// + /// [`ToEndpoint`]: crate::api::control::refs::ToEndpoint + pub fn push_endpoint_id( + self, + endpoint_id: $crate::api::control::EndpointId, + ) -> $container<$crate::api::control::refs::ToEndpoint> { + let (member_id, room_uri) = self.take_member_id(); + let room_id = room_uri.take_room_id(); + $container::<$crate::api::control::refs::ToEndpoint>::new( + room_id, + member_id, + endpoint_id, + ) + } + + /// Returns [`RoomId`] and [`MemberId`]. + /// + /// [`RoomId`]: crate::api::control::RoomId + /// [`MemberId`]: crate::api::control::MemberId + pub fn take_all( + self, + ) -> ($crate::api::control::RoomId, $crate::api::control::MemberId) + { + let (member_id, room_url) = self.take_member_id(); + + (room_url.take_room_id(), member_id) + } + } + + impl $container<$crate::api::control::refs::ToEndpoint> { + /// Creates new reference in [`ToEndpoint`] state. + /// + /// [`ToEndpoint`]: crate::api::control::refs::ToEndpoint + pub fn new( + room_id: $crate::api::control::RoomId, + member_id: $crate::api::control::MemberId, + endpoint_id: $crate::api::control::EndpointId, + ) -> Self { + Self { + state: $crate::api::control::refs::ToEndpoint( + room_id, + member_id, + endpoint_id, + ), + } + } + + /// Returns borrowed [`RoomId`]. + /// + /// [`RoomId`]: crate::api::control::RoomId + pub fn room_id(&self) -> &$crate::api::control::RoomId { + &self.state.0 + } + + /// Returns borrowed [`MemberId`]. + /// + /// [`MemberId`]: crate::api::control::MemberId + pub fn member_id(&self) -> &$crate::api::control::MemberId { + &self.state.1 + } + + /// Returns borrowed [`EndpointId`]. + /// + /// [`EndpointId`]: crate::api::control::EndpointId + pub fn endpoint_id(&self) -> &$crate::api::control::EndpointId { + &self.state.2 + } + + /// Returns [`Endpoint`] id and reference in [`ToMember`] state. + /// + /// [`Endpoint`]: crate::signalling::elements::endpoints::Endpoint + /// [`ToMember`]: crate::api::control::refs::ToMember + pub fn take_endpoint_id( + self, + ) -> ( + $crate::api::control::EndpointId, + $container<$crate::api::control::refs::ToMember>, + ) { + ( + self.state.2, + $container::<$crate::api::control::refs::ToMember>::new( + self.state.0, + self.state.1, + ), + ) + } + + /// Returns [`EndpointId`], [`RoomId`] and [`MemberId`]. + /// + /// [`EndpointId`]: crate::api::control::EndpointId + /// [`RoomId`]: crate::api::control::RoomId + /// [`MemberId`]: crate::api::control::MemberId + pub fn take_all( + self, + ) -> ( + $crate::api::control::RoomId, + $crate::api::control::MemberId, + $crate::api::control::EndpointId, + ) { + let (endpoint_id, member_url) = self.take_endpoint_id(); + let (member_id, room_url) = member_url.take_member_id(); + + (room_url.take_room_id(), member_id, endpoint_id) + } + } + }; +} diff --git a/src/api/control/refs/src_uri.rs b/src/api/control/refs/src_uri.rs new file mode 100644 index 000000000..272825ffe --- /dev/null +++ b/src/api/control/refs/src_uri.rs @@ -0,0 +1,151 @@ +//! Implementation of special URI with pattern +//! `local://{room_id}/{member_id}/{endpoint_id}`. This URI can point only to +//! [`WebRtcPublishEndpoint`]. +//! +//! [`WebRtcPublishEndpoint`]: +//! crate::signalling::elements::endpoints::webrtc::WebRtcPublishEndpoint + +use std::{convert::TryFrom, fmt}; + +use derive_more::Display; +use failure::Fail; +use serde::{ + de::{self, Deserializer, Error, Visitor}, + Deserialize, +}; + +use crate::api::control::{ + endpoints::webrtc_publish_endpoint::WebRtcPublishId, + refs::{ + local_uri::{LocalUriParseError, StatefulLocalUri}, + LocalUri, ToEndpoint, + }, + MemberId, RoomId, +}; + +/// Errors which can happen while parsing [`SrcUri`] from [Control API] specs. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[derive(Debug, Fail, Display)] +pub enum SrcParseError { + /// Provided not source URI. + #[display(fmt = "Provided not src uri {}", _0)] + NotSrcUri(String), + + /// Error from [`LocalUri`] parser. This is general errors for [`SrcUri`] + /// parsing because [`SrcUri`] parses with [`LocalUri`] parser. + #[display(fmt = "Local URI parse error: {:?}", _0)] + LocalUriParseError(LocalUriParseError), +} + +/// Special URI with pattern `local://{room_id}/{member_id}/{endpoint_id}`. +/// This uri can pointing only to [`WebRtcPublishEndpoint`]. +/// +/// Note that [`SrcUri`] is parsing with [`LocalUri`] parser. +/// Actually difference between [`SrcUri`] and [`LocalUri`] +/// in endpoint ID's type. In [`SrcUri`] it [`WebRtcPublishId`], and in +/// [`LocalUri`] it [`EndpointId`]. Also [`SrcUri`] can be deserialized with +/// [`serde`]. +/// +/// Atm used only in [Control API] specs. +/// +/// [`WebRtcPublishEndpoint`]: +/// crate::api::control::endpoints::WebRtcPublishEndpoint +/// [Control API]: https://tinyurl.com/yxsqplq7 +/// [`EndpointId`]: crate::api::control::EndpointId +#[derive(Clone, Debug)] +pub struct SrcUri { + /// ID of [`Room`]. + /// + /// [`Room`]: crate::signalling::room::Room + pub room_id: RoomId, + + /// ID of [`MemberSpec`]. + /// + /// [`MemberSpec`]: crate::api::control::member::MemberSpec + pub member_id: MemberId, + + /// ID of [`WebRtcPublishEndpoint`]. + /// + /// [`WebRtcPublishEndpoint`]: + /// crate::api::control::endpoints::WebRtcPublishEndpoint + pub endpoint_id: WebRtcPublishId, +} + +impl TryFrom for SrcUri { + type Error = SrcParseError; + + fn try_from(value: String) -> Result { + let local_uri = StatefulLocalUri::try_from(value) + .map_err(SrcParseError::LocalUriParseError)?; + + match local_uri { + StatefulLocalUri::Room(uri) => { + Err(SrcParseError::NotSrcUri(uri.to_string())) + } + StatefulLocalUri::Member(uri) => { + Err(SrcParseError::NotSrcUri(uri.to_string())) + } + StatefulLocalUri::Endpoint(uri) => Ok(uri.into()), + } + } +} + +impl From> for SrcUri { + fn from(uri: LocalUri) -> Self { + let (room_id, member_id, endpoint_id) = uri.take_all(); + + Self { + room_id, + member_id, + endpoint_id: endpoint_id.into(), + } + } +} + +/// [Serde] deserializer for [`SrcUri`]. +/// +/// Deserializes URIs with pattern: +/// `local://room_id/member_id/publish_endpoint_id`. +/// +/// [Serde]: serde +impl<'de> Deserialize<'de> for SrcUri { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SrcUriVisitor; + + impl<'de> Visitor<'de> for SrcUriVisitor { + type Value = SrcUri; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "Uri in format local://room_id/member_id/endpoint_id", + ) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match SrcUri::try_from(value.to_owned()) { + Ok(src_uri) => Ok(src_uri), + Err(e) => Err(Error::custom(e)), + } + } + } + + deserializer.deserialize_identifier(SrcUriVisitor) + } +} + +impl fmt::Display for SrcUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "local://{}/{}/{}", + self.room_id, self.member_id, self.endpoint_id + ) + } +} diff --git a/src/api/control/room.rs b/src/api/control/room.rs index 971335d25..b7f32079c 100644 --- a/src/api/control/room.rs +++ b/src/api/control/room.rs @@ -1,12 +1,18 @@ //! Definitions and implementations of [Control API]'s `Room` element. //! -//! [Control API]: http://tiny.cc/380uaz +//! [Control API]: https://tinyurl.com/yxsqplq7 use std::{collections::HashMap, convert::TryFrom}; use derive_more::{Display, From}; +#[rustfmt::skip] +use medea_control_api_proto::grpc::api::{ + CreateRequest_oneof_el as ElementProto, +}; use serde::Deserialize; +use crate::api::control::{EndpointId, TryFromProtobufError}; + use super::{ member::{MemberElement, MemberSpec}, pipeline::Pipeline, @@ -29,7 +35,7 @@ pub enum RoomElement { /// Represent [`MemberSpec`]. /// Can transform into [`MemberSpec`] by `MemberSpec::try_from`. Member { - spec: Pipeline, + spec: Pipeline, credentials: String, }, } @@ -38,12 +44,47 @@ pub enum RoomElement { /// /// Newtype for [`RootElement::Room`]. /// -/// [Control API]: http://tiny.cc/380uaz +/// [Control API]: https://tinyurl.com/yxsqplq7 #[allow(clippy::module_name_repetitions)] #[derive(Clone, Debug)] pub struct RoomSpec { pub id: Id, - pub pipeline: Pipeline, + pub pipeline: Pipeline, +} + +impl TryFrom for RoomSpec { + type Error = TryFromProtobufError; + + fn try_from(proto: ElementProto) -> Result { + let id = match proto { + ElementProto::room(mut room) => { + let mut pipeline = HashMap::new(); + for (id, room_element) in room.take_pipeline() { + if let Some(elem) = room_element.el { + let member = + MemberSpec::try_from((MemberId(id.clone()), elem))?; + pipeline.insert(id.into(), member.into()); + } else { + return Err(TryFromProtobufError::EmptyElement(id)); + } + } + + let pipeline = Pipeline::new(pipeline); + return Ok(Self { + id: room.take_id().into(), + pipeline, + }); + } + ElementProto::member(mut member) => member.take_id(), + ElementProto::webrtc_pub(mut webrtc_pub) => webrtc_pub.take_id(), + ElementProto::webrtc_play(mut webrtc_play) => webrtc_play.take_id(), + }; + + Err(TryFromProtobufError::ExpectedOtherElement( + String::from("Room"), + id, + )) + } } impl RoomSpec { @@ -54,9 +95,8 @@ impl RoomSpec { let mut members: HashMap = HashMap::new(); for (control_id, element) in self.pipeline.iter() { let member_spec = MemberSpec::try_from(element)?; - let member_id = MemberId(control_id.clone()); - members.insert(member_id.clone(), member_spec); + members.insert(control_id.clone(), member_spec); } Ok(members) diff --git a/src/bin/client.rs b/src/bin/client.rs new file mode 100644 index 000000000..909f53b61 --- /dev/null +++ b/src/bin/client.rs @@ -0,0 +1,156 @@ +//! This is temporary binary for testing gRPC Control API implementation +//! purposes. +//! +//! In `control-api-mock-server` branch this will be deleted. After +//! `control-api-mock-server` branch you will be able to test it with Control +//! API mock server by calling REST API endpoints. + +#![allow(dead_code)] + +use std::{collections::HashMap, sync::Arc}; + +use grpcio::{ChannelBuilder, EnvBuilder}; +use medea_control_api_proto::grpc::{ + api::{ + CreateRequest, IdRequest, Member, Member_Element, Room, Room_Element, + WebRtcPlayEndpoint, WebRtcPublishEndpoint, WebRtcPublishEndpoint_P2P, + }, + api_grpc::ControlApiClient, +}; +use protobuf::RepeatedField; + +fn main() { + let env = Arc::new(EnvBuilder::new().build()); + let ch = ChannelBuilder::new(env).connect("127.0.0.1:6565"); + let client = ControlApiClient::new(ch); + + // unimplemented_apply(&client); + create_room(&client); + create_member(&client); + create_endpoint(&client); + // delete_room(&client); + // delete_endpoint(&client); + // delete_member(&client); + // get_room(&client); +} + +fn create_room(client: &ControlApiClient) { + let mut req = CreateRequest::new(); + let mut room = Room::new(); + let mut publisher = Member::new(); + let mut responder = Member::new(); + let mut play_endpoint = WebRtcPlayEndpoint::new(); + let mut publish_endpoint = WebRtcPublishEndpoint::new(); + + play_endpoint.set_src("local://grpc-test/publisher/publish".to_string()); + let mut play_endpoint_element = Member_Element::new(); + play_endpoint_element.set_webrtc_play(play_endpoint); + let mut responder_pipeline = HashMap::new(); + responder_pipeline.insert("play".to_string(), play_endpoint_element); + responder.set_pipeline(responder_pipeline); + responder.set_credentials("test".to_string()); + + publish_endpoint.set_p2p(WebRtcPublishEndpoint_P2P::ALWAYS); + let mut publish_endpoint_element = Member_Element::new(); + publish_endpoint_element.set_webrtc_pub(publish_endpoint); + let mut publisher_pipeline = HashMap::new(); + publisher_pipeline.insert("publish".to_string(), publish_endpoint_element); + publisher.set_pipeline(publisher_pipeline); + // publisher.set_credentials("test".to_string()); + + let mut publisher_member_element = Room_Element::new(); + publisher_member_element.set_member(publisher); + let mut responder_member_element = Room_Element::new(); + responder_member_element.set_member(responder); + let mut room_pipeline = HashMap::new(); + room_pipeline.insert("publisher".to_string(), publisher_member_element); + room_pipeline.insert("responder".to_string(), responder_member_element); + room.set_pipeline(room_pipeline); + room.set_id("grpc-test".to_string()); + req.set_room(room); + + let reply = client.create(&req).expect("create room"); + println!("{:?}", reply); +} + +fn create_member(client: &ControlApiClient) { + let mut create_member_request = CreateRequest::new(); + let mut member = Member::new(); + let mut member_pipeline = HashMap::new(); + + let mut play_endpoint = WebRtcPlayEndpoint::new(); + play_endpoint.set_src("local://grpc-test/publisher/publish".to_string()); + let mut member_element = Member_Element::new(); + member_element.set_webrtc_play(play_endpoint); + member_pipeline.insert("play".to_string(), member_element); + + member.set_credentials("test".to_string()); + member.set_pipeline(member_pipeline); + member.set_id("player".to_string()); + create_member_request.set_parent_fid("grpc-test".to_string()); + create_member_request.set_member(member); + + let reply = client + .create(&create_member_request) + .expect("create member"); + println!("{:?}", reply) +} + +fn create_endpoint(client: &ControlApiClient) { + let mut create_endpoint_request = CreateRequest::new(); + let mut endpoint = WebRtcPublishEndpoint::new(); + endpoint.set_p2p(WebRtcPublishEndpoint_P2P::ALWAYS); + endpoint.set_id("publish".to_string()); + create_endpoint_request.set_parent_fid("grpc-test/responder".to_string()); + create_endpoint_request.set_webrtc_pub(endpoint); + + let reply = client + .create(&create_endpoint_request) + .expect("create endpoint"); + println!("{:?}", reply); +} + +fn delete_room(client: &ControlApiClient) { + let mut delete_request = IdRequest::new(); + let mut rooms = RepeatedField::new(); + rooms.push("video-call-1/caller".to_string()); + rooms.push("video-call-1".to_string()); + rooms.push("pub-pub-video-call/caller".to_string()); + delete_request.set_fid(rooms); + + let reply = client.delete(&delete_request).expect("delete room"); + println!("{:?}", reply); +} + +fn delete_endpoint(client: &ControlApiClient) { + let mut delete_endpoint_req = IdRequest::new(); + let mut endpoints = RepeatedField::new(); + endpoints.push("video-call-1/caller/publish".to_string()); + delete_endpoint_req.set_fid(endpoints); + + let reply = client.delete(&delete_endpoint_req).expect("delete member"); + println!("{:?}", reply); +} + +fn delete_member(client: &ControlApiClient) { + let mut delete_member_req = IdRequest::new(); + let mut members = RepeatedField::new(); + members.push("video-call-1/caller".to_string()); + delete_member_req.set_fid(members); + + let reply = client.delete(&delete_member_req).expect("delete member"); + println!("{:?}", reply); +} + +fn get_room(client: &ControlApiClient) { + let mut get_room_request = IdRequest::new(); + let mut room = RepeatedField::new(); + room.push("grpc-test".to_string()); + room.push("video-call-1/responder".to_string()); + room.push("grpc-test/publisher/publish".to_string()); + room.push("pub-pub-video-call".to_string()); + get_room_request.set_fid(room); + + let reply = client.get(&get_room_request).expect("get room"); + println!("{:#?}", reply); +} diff --git a/src/conf/control.rs b/src/conf/control.rs index ae7f049d4..d787337c1 100644 --- a/src/conf/control.rs +++ b/src/conf/control.rs @@ -1,15 +1,43 @@ +//! [Control API] settings. +//! +//! [Control API]: https://tinyurl.com/yxsqplq7 + use serde::{Deserialize, Serialize}; use smart_default::SmartDefault; /// [Control API] settings. /// -/// [Control API]: http://tiny.cc/380uaz -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, SmartDefault)] +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, Debug, Deserialize, Serialize, SmartDefault)] #[serde(default)] -pub struct Control { +pub struct ControlApi { /// Path to directory with static [Сontrol API] specs. /// - /// [Control API]: http://tiny.cc/380uaz - #[default(String::from("specs/"))] + /// [Control API]: https://tinyurl.com/yxsqplq7 + #[default = "specs/"] pub static_specs_dir: String, } + +#[cfg(test)] +mod spec { + use serial_test_derive::serial; + + use crate::{conf::Conf, overrided_by_env_conf}; + + #[test] + #[serial] + fn overrides_defaults() { + let default_conf = Conf::default(); + let env_conf = overrided_by_env_conf!( + "MEDEA_CONTROL__STATIC_SPECS_DIR" => "test/", + ); + + assert_ne!( + default_conf.control.static_specs_dir, + env_conf.control.static_specs_dir + ); + + assert_eq!(env_conf.control.static_specs_dir, "test/"); + } +} diff --git a/src/conf/log.rs b/src/conf/log.rs index 086603d08..8f6a34ff6 100644 --- a/src/conf/log.rs +++ b/src/conf/log.rs @@ -1,6 +1,6 @@ //! Logging settings. -use std::str::FromStr as _; +use std::{borrow::Cow, str::FromStr as _}; use serde::{Deserialize, Serialize}; use smart_default::SmartDefault; @@ -11,8 +11,8 @@ use smart_default::SmartDefault; pub struct Log { /// Maximum allowed level of application log entries. /// Defaults to `INFO`. - #[default(String::from("INFO"))] - pub level: String, + #[default = "INFO"] + pub level: Cow<'static, str>, } impl Log { @@ -21,3 +21,27 @@ impl Log { slog::Level::from_str(&self.level).ok() } } + +#[cfg(test)] +mod log_conf_specs { + use serial_test_derive::serial; + + use crate::{conf::Conf, overrided_by_env_conf}; + + #[test] + #[serial] + fn overrides_defaults() { + let default_conf = Conf::default(); + + let env_conf = overrided_by_env_conf!( + "MEDEA_LOG__LEVEL" => "WARN", + ); + assert_ne!(default_conf.log.level(), env_conf.log.level()); + assert_eq!(env_conf.log.level(), Some(slog::Level::Warning)); + + let none_lvl = overrided_by_env_conf!( + "MEDEA_LOG__LEVEL" => "OFF", + ); + assert_eq!(none_lvl.log.level(), None); + } +} diff --git a/src/conf/mod.rs b/src/conf/mod.rs index 585a52f89..f75778624 100644 --- a/src/conf/mod.rs +++ b/src/conf/mod.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; #[doc(inline)] pub use self::{ - control::Control, + control::ControlApi, log::Log, rpc::Rpc, server::Server, @@ -34,10 +34,10 @@ static APP_CONF_PATH_ENV_VAR_NAME: &str = "MEDEA_CONF"; #[derive(Clone, Debug, Deserialize, Serialize, Default)] #[serde(default)] pub struct Conf { - /// HTTP server settings. + /// RPC connection settings. pub rpc: Rpc, - /// RPC connection settings. + /// Servers settings. pub server: Server, /// TURN server settings. @@ -51,8 +51,8 @@ pub struct Conf { /// [Control API] settings. /// - /// [Control API]: http://tiny.cc/380uaz - pub control: Control, + /// [Control API]: https://tinyurl.com/yxsqplq7 + pub control: ControlApi, } impl Conf { @@ -96,13 +96,41 @@ where } #[cfg(test)] -mod tests { - use std::{fs, net::Ipv4Addr, time::Duration}; - +pub(crate) mod spec { use serial_test_derive::serial; use super::*; + /// Macro which overrides environment variables with provided values, + /// parses [`Conf`] and finally removes all the overrided variables. + /// + /// # Usage + /// + /// ```rust + /// # use crate::conf::Conf; + /// # + /// let default_conf = Conf::default(); + /// let env_conf = overrided_by_env_conf!( + /// "MEDEA_TURN__HOST" => "example.com", + /// "MEDEA_TURN__PORT" => "1234", + /// "MEDEA_TURN__USER" => "ferris", + /// "MEDEA_TURN__PASS" => "qwerty" + /// ); + /// + /// assert_ne!(default_conf.turn.host, env_conf.turn.host); + /// assert_ne!(default_conf.turn.port, env_conf.turn.port); + /// // ... + /// ``` + #[macro_export] + macro_rules! overrided_by_env_conf { + ($($env:expr => $value:expr),+ $(,)?) => {{ + $(::std::env::set_var($env, $value);)+ + let conf = crate::conf::Conf::parse().unwrap(); + $(::std::env::remove_var($env);)+ + conf + }}; + } + #[test] #[serial] fn get_conf_file_name_spec_none_if_nothing_is_set() { @@ -158,137 +186,4 @@ mod tests { ); env::remove_var(APP_CONF_PATH_ENV_VAR_NAME); } - - #[test] - #[serial] - fn conf_parse_spec_file_overrides_defaults() { - let defaults = Conf::default(); - let test_config_file_path = "test_config.toml"; - - let data = "[rpc]\nidle_timeout = \"45s\"".to_owned(); - fs::write(test_config_file_path, data).unwrap(); - env::set_var(APP_CONF_PATH_ENV_VAR_NAME, test_config_file_path); - - let new_config = Conf::parse().unwrap(); - - env::remove_var(APP_CONF_PATH_ENV_VAR_NAME); - fs::remove_file(test_config_file_path).unwrap(); - - assert_eq!(new_config.rpc.idle_timeout, Duration::from_secs(45)); - assert_ne!(new_config.rpc.idle_timeout, defaults.rpc.idle_timeout); - } - - #[test] - #[serial] - fn conf_parse_spec_env_overrides_defaults() { - let defaults = Conf::default(); - - env::set_var("MEDEA_RPC__IDLE_TIMEOUT", "46s"); - let new_config = Conf::parse().unwrap(); - env::remove_var("MEDEA_RPC__IDLE_TIMEOUT"); - - assert_eq!(new_config.rpc.idle_timeout, Duration::from_secs(46)); - assert_ne!(new_config.rpc.idle_timeout, defaults.rpc.idle_timeout); - } - - #[test] - #[serial] - fn conf_parse_spec_env_overrides_file() { - let test_config_file_path = "test_config.toml"; - - let data = "[rpc]\nidle_timeout = \"47s\"".to_owned(); - fs::write(test_config_file_path, data).unwrap(); - env::set_var(APP_CONF_PATH_ENV_VAR_NAME, test_config_file_path); - - let file_config = Conf::parse().unwrap(); - - env::set_var("MEDEA_RPC__IDLE_TIMEOUT", "48s"); - let file_env_config = Conf::parse().unwrap(); - - env::remove_var(APP_CONF_PATH_ENV_VAR_NAME); - fs::remove_file(test_config_file_path).unwrap(); - env::remove_var("MEDEA_RPC__IDLE_TIMEOUT"); - - assert_eq!(file_config.rpc.idle_timeout, Duration::from_secs(47)); - - assert_eq!(file_env_config.rpc.idle_timeout, Duration::from_secs(48)); - } - - #[test] - #[serial] - fn redis_conf() { - let default_conf = Conf::default(); - - env::set_var("MEDEA_TURN__DB__REDIS__IP", "5.5.5.5"); - env::set_var("MEDEA_TURN__DB__REDIS__PORT", "1234"); - env::set_var("MEDEA_TURN__DB__REDIS__CONNECTION_TIMEOUT", "10s"); - - let env_conf = Conf::parse().unwrap(); - - assert_ne!(default_conf.turn.db.redis.ip, env_conf.turn.db.redis.ip); - assert_ne!( - default_conf.turn.db.redis.connection_timeout, - env_conf.turn.db.redis.connection_timeout - ); - assert_ne!( - default_conf.turn.db.redis.connection_timeout, - env_conf.turn.db.redis.connection_timeout - ); - - assert_eq!(env_conf.turn.db.redis.ip, Ipv4Addr::new(5, 5, 5, 5)); - assert_eq!(env_conf.turn.db.redis.port, 1234); - assert_eq!( - env_conf.turn.db.redis.connection_timeout, - Duration::from_secs(10) - ) - } - - #[test] - #[serial] - fn turn_conf() { - let default_conf = Conf::default(); - - env::set_var("MEDEA_TURN__HOST", "example.com"); - env::set_var("MEDEA_TURN__PORT", "1234"); - - let env_conf = Conf::parse().unwrap(); - - assert_ne!(default_conf.turn.host, env_conf.turn.host); - assert_ne!(default_conf.turn.port, env_conf.turn.port); - - assert_eq!(env_conf.turn.host, "example.com"); - assert_eq!(env_conf.turn.port, 1234); - assert_eq!(env_conf.turn.addr(), "example.com:1234"); - } - - #[test] - #[serial] - fn log_conf() { - let default_conf = Conf::default(); - - env::set_var("MEDEA_LOG__LEVEL", "WARN"); - - let env_conf = Conf::parse().unwrap(); - - assert_ne!(default_conf.log.level(), env_conf.log.level()); - - assert_eq!(env_conf.log.level(), Some(slog::Level::Warning)); - - env::set_var("MEDEA_LOG__LEVEL", "OFF"); - - assert_eq!(Conf::parse().unwrap().log.level(), None); - } - - #[test] - #[serial] - fn shutdown_conf_test() { - let default_conf = Conf::default(); - - env::set_var("MEDEA_SHUTDOWN__TIMEOUT", "700ms"); - - let env_conf = Conf::parse().unwrap(); - - assert_ne!(default_conf.shutdown.timeout, env_conf.shutdown.timeout); - assert_eq!(env_conf.shutdown.timeout, Duration::from_millis(700)); - } } diff --git a/src/conf/rpc.rs b/src/conf/rpc.rs index dbd3bb565..0d0f26a96 100644 --- a/src/conf/rpc.rs +++ b/src/conf/rpc.rs @@ -21,3 +21,79 @@ pub struct Rpc { #[serde(with = "humantime_serde")] pub reconnect_timeout: Duration, } + +#[cfg(test)] +mod spec { + use std::{fs, time::Duration}; + + use serial_test_derive::serial; + + use crate::{ + conf::{Conf, APP_CONF_PATH_ENV_VAR_NAME}, + overrided_by_env_conf, + }; + + #[test] + #[serial] + fn overrides_defaults() { + let default_conf = Conf::default(); + let env_conf = overrided_by_env_conf!( + "MEDEA_RPC__IDLE_TIMEOUT" => "20s", + "MEDEA_RPC__RECONNECT_TIMEOUT" => "30s", + ); + + assert_ne!(default_conf.rpc.idle_timeout, env_conf.rpc.idle_timeout); + assert_ne!( + default_conf.rpc.reconnect_timeout, + env_conf.rpc.reconnect_timeout + ); + + assert_eq!(env_conf.rpc.idle_timeout, Duration::from_secs(20)); + assert_eq!(env_conf.rpc.reconnect_timeout, Duration::from_secs(30)); + } + + #[test] + #[serial] + fn conf_parse_spec_file_overrides_defaults() { + // Don't delete me! Otherwise temporary dir will be deleted. + let dir = tempfile::tempdir().unwrap(); + let conf_path = + dir.path().join("test_config.toml").display().to_string(); + + let data = "[rpc]\nidle_timeout = \"45s\"".to_owned(); + fs::write(&conf_path, data).unwrap(); + + let new_config = overrided_by_env_conf!( + APP_CONF_PATH_ENV_VAR_NAME => &conf_path, + ); + + assert_eq!(new_config.rpc.idle_timeout, Duration::from_secs(45)); + assert_ne!( + new_config.rpc.idle_timeout, + Conf::default().rpc.idle_timeout + ); + } + + #[test] + #[serial] + fn conf_parse_spec_env_overrides_file() { + // Don't delete me! Otherwise temporary dir will be deleted. + let dir = tempfile::tempdir().unwrap(); + let conf_path = + dir.path().join("test_config.toml").display().to_string(); + + let data = "[rpc]\nidle_timeout = \"47s\"".to_owned(); + fs::write(&conf_path, data).unwrap(); + + let file_config = overrided_by_env_conf!( + APP_CONF_PATH_ENV_VAR_NAME => &conf_path, + ); + let file_env_config = overrided_by_env_conf!( + APP_CONF_PATH_ENV_VAR_NAME => &conf_path, + "MEDEA_RPC__IDLE_TIMEOUT" => "48s", + ); + + assert_eq!(file_config.rpc.idle_timeout, Duration::from_secs(47)); + assert_eq!(file_env_config.rpc.idle_timeout, Duration::from_secs(48)); + } +} diff --git a/src/conf/server.rs b/src/conf/server.rs index f74032142..a88582bd8 100644 --- a/src/conf/server.rs +++ b/src/conf/server.rs @@ -1,24 +1,58 @@ -//! HTTP server settings. +//! Settings for application servers. + +#![allow(clippy::module_name_repetitions)] use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs as _}; use serde::{Deserialize, Serialize}; use smart_default::SmartDefault; -/// HTTP server settings. +/// [Client API] servers settings. +/// +/// [Client API]: https://tinyurl.com/yx9thsnr #[derive(Clone, Debug, Deserialize, Serialize, SmartDefault)] #[serde(default)] -pub struct Server { - /// IP address to bind HTTP server to. Defaults to `0.0.0.0`. +pub struct ClientApiServer { + /// [Client API] HTTP server settings. + /// + /// [Client API]: https://tinyurl.com/yx9thsnr + pub http: ClientApiHttpServer, +} + +/// [Client API] HTTP server settings. +/// +/// [Client API]: https://tinyurl.com/yx9thsnr +#[derive(Clone, Debug, Deserialize, Serialize, SmartDefault)] +#[serde(default)] +pub struct ClientApiHttpServer { + /// Public URL of HTTP server. Address for exposed [Client API]. + /// It's assumed that HTTP server can be reached via this URL externally. + /// + /// This address is returned from [Control API] in `sids` field + /// and [Jason] uses this address to start its session. + /// + /// Defaults to `ws://127.0.0.1:8080`. + /// + /// [Client API]: https://tinyurl.com/yx9thsnr + /// [Control API]: https://tinyurl.com/yxsqplq7 + /// [Jason]: https://github.com/instrumentisto/medea/tree/master/jason + #[default = "ws://127.0.0.1:8080/ws"] + pub public_url: String, + + /// IP address to bind HTTP server to. + /// + /// Defaults to `0.0.0.0`. #[default(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))] pub bind_ip: IpAddr, - /// Port to bind HTTP server to. Defaults to `8080`. - #[default(8080)] + /// Port to bind HTTP server to. + /// + /// Defaults to `8080`. + #[default = 8080] pub bind_port: u16, } -impl Server { +impl ClientApiHttpServer { /// Builds [`SocketAddr`] from `bind_ip` and `bind_port`. #[inline] pub fn bind_addr(&self) -> SocketAddr { @@ -30,37 +64,120 @@ impl Server { } } +/// [Control API] servers settings. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[derive(Clone, Debug, Deserialize, Serialize, SmartDefault)] +#[serde(default)] +pub struct ControlApiServer { + /// [Control API] gRPC server settings. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + pub grpc: ControlApiGrpcServer, +} + +/// [Control API] gRPC server settings. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[derive(Clone, Debug, Deserialize, Serialize, SmartDefault)] +#[serde(default)] +pub struct ControlApiGrpcServer { + /// IP address to bind gRPC server to. + /// + /// Defaults to `0.0.0.0`. + #[default(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))] + pub bind_ip: IpAddr, + + /// Port to bind gRPC server to. + /// + /// Defaults to `6565`. + #[default = 6565] + pub bind_port: u16, +} + +/// Settings for application servers. +#[derive(Clone, Debug, Deserialize, Serialize, SmartDefault)] +#[serde(default)] +pub struct Server { + /// [Client API] servers settings. + /// + /// [Client API]: https://tinyurl.com/yx9thsnr + pub client: ClientApiServer, + + /// [Control API] servers settings. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + pub control: ControlApiServer, +} + #[cfg(test)] -mod server_spec { - use std::env; +mod client_http_spec { + use std::net::Ipv4Addr; use serial_test_derive::serial; - use crate::conf::Conf; - - use super::*; + use crate::{conf::Conf, overrided_by_env_conf}; #[test] #[serial] fn overrides_defaults_and_gets_bind_addr() { let default_conf = Conf::default(); - env::set_var("MEDEA_SERVER__BIND_IP", "5.5.5.5"); - env::set_var("MEDEA_SERVER__BIND_PORT", "1234"); + let env_conf = overrided_by_env_conf!( + "MEDEA_SERVER__CLIENT__HTTP__BIND_IP" => "5.5.5.5", + "MEDEA_SERVER__CLIENT__HTTP__BIND_PORT" => "1234", + ); + + assert_ne!( + default_conf.server.client.http.bind_ip, + env_conf.server.client.http.bind_ip + ); + assert_ne!( + default_conf.server.client.http.bind_port, + env_conf.server.client.http.bind_port + ); + + assert_eq!( + env_conf.server.client.http.bind_ip, + Ipv4Addr::new(5, 5, 5, 5) + ); + assert_eq!(env_conf.server.client.http.bind_port, 1234); + assert_eq!( + env_conf.server.client.http.bind_addr(), + "5.5.5.5:1234".parse().unwrap(), + ); + } +} + +#[cfg(test)] +mod control_grpc_spec { + use std::net::Ipv4Addr; - let env_conf = Conf::parse().unwrap(); + use serial_test_derive::serial; - env::remove_var("MEDEA_SERVER__BIND_IP"); - env::remove_var("MEDEA_SERVER__BIND_PORT"); + use crate::{conf::Conf, overrided_by_env_conf}; - assert_ne!(default_conf.server.bind_ip, env_conf.server.bind_ip); - assert_ne!(default_conf.server.bind_port, env_conf.server.bind_port); + #[test] + #[serial] + fn overrides_defaults() { + let default_conf = Conf::default(); + let env_conf = overrided_by_env_conf!( + "MEDEA_SERVER__CONTROL__GRPC__BIND_IP" => "182.98.12.48", + "MEDEA_SERVER__CONTROL__GRPC__BIND_PORT" => "44444", + ); - assert_eq!(env_conf.server.bind_ip, Ipv4Addr::new(5, 5, 5, 5)); - assert_eq!(env_conf.server.bind_port, 1234); + assert_ne!( + default_conf.server.control.grpc.bind_ip, + env_conf.server.control.grpc.bind_ip + ); + assert_ne!( + default_conf.server.control.grpc.bind_port, + env_conf.server.control.grpc.bind_port + ); + assert_eq!(env_conf.server.control.grpc.bind_port, 44444); assert_eq!( - env_conf.server.bind_addr(), - "5.5.5.5:1234".parse().unwrap(), + env_conf.server.control.grpc.bind_ip, + Ipv4Addr::new(182, 98, 12, 48) ); } } diff --git a/src/conf/shutdown.rs b/src/conf/shutdown.rs index b0e824d7e..6900500f8 100644 --- a/src/conf/shutdown.rs +++ b/src/conf/shutdown.rs @@ -14,3 +14,24 @@ pub struct Shutdown { #[serde(with = "humantime_serde")] pub timeout: Duration, } + +#[cfg(test)] +mod spec { + use std::time::Duration; + + use serial_test_derive::serial; + + use crate::{conf::Conf, overrided_by_env_conf}; + + #[test] + #[serial] + fn overrides_defaults() { + let default_conf = Conf::default(); + let env_conf = overrided_by_env_conf!( + "MEDEA_SHUTDOWN__TIMEOUT" => "20s", + ); + + assert_ne!(default_conf.shutdown.timeout, env_conf.shutdown.timeout); + assert_eq!(env_conf.shutdown.timeout, Duration::from_secs(20)); + } +} diff --git a/src/conf/turn.rs b/src/conf/turn.rs index 762f90819..836073c60 100644 --- a/src/conf/turn.rs +++ b/src/conf/turn.rs @@ -68,3 +68,84 @@ pub struct Redis { #[serde(with = "humantime_serde")] pub connection_timeout: Duration, } + +#[cfg(test)] +mod spec { + use std::{net::Ipv4Addr, time::Duration}; + + use serial_test_derive::serial; + + use crate::{conf::Conf, overrided_by_env_conf}; + + #[test] + #[serial] + fn redis_db_overrides_defaults() { + let default_conf = Conf::default(); + let env_conf = overrided_by_env_conf!( + "MEDEA_TURN__DB__REDIS__IP" => "5.5.5.5", + "MEDEA_TURN__DB__REDIS__PORT" => "1234", + "MEDEA_TURN__DB__REDIS__PASS" => "hellofellow", + "MEDEA_TURN__DB__REDIS__DB_NUMBER" => "10", + "MEDEA_TURN__DB__REDIS__CONNECTION_TIMEOUT" => "10s", + ); + + assert_ne!(default_conf.turn.db.redis.ip, env_conf.turn.db.redis.ip); + assert_ne!( + default_conf.turn.db.redis.port, + env_conf.turn.db.redis.port + ); + assert_ne!( + default_conf.turn.db.redis.pass, + env_conf.turn.db.redis.pass + ); + assert_ne!( + default_conf.turn.db.redis.db_number, + env_conf.turn.db.redis.db_number + ); + assert_ne!( + default_conf.turn.db.redis.connection_timeout, + env_conf.turn.db.redis.connection_timeout + ); + + assert_eq!(env_conf.turn.db.redis.ip, Ipv4Addr::new(5, 5, 5, 5)); + assert_eq!(env_conf.turn.db.redis.port, 1234); + assert_eq!( + env_conf.turn.db.redis.connection_timeout, + Duration::from_secs(10) + ); + } + + #[test] + #[serial] + fn overrides_defaults() { + let default_conf = Conf::default(); + let env_conf = overrided_by_env_conf!( + "MEDEA_TURN__HOST" => "example.com", + "MEDEA_TURN__PORT" => "1234", + "MEDEA_TURN__USER" => "ferris", + "MEDEA_TURN__PASS" => "qwerty", + ); + + assert_ne!(default_conf.turn.host, env_conf.turn.host); + assert_ne!(default_conf.turn.port, env_conf.turn.port); + assert_ne!(default_conf.turn.user, env_conf.turn.user); + assert_ne!(default_conf.turn.pass, env_conf.turn.pass); + + assert_eq!(env_conf.turn.host, "example.com"); + assert_eq!(env_conf.turn.port, 1234); + assert_eq!(env_conf.turn.addr(), "example.com:1234"); + } + + #[test] + #[serial] + fn turn_conf() { + let default_conf = Conf::default(); + let env_conf = overrided_by_env_conf!( + "MEDEA_TURN__HOST" => "example.com", + "MEDEA_TURN__PORT" => "1234", + ); + + assert_ne!(default_conf.turn.host, env_conf.turn.host); + assert_ne!(default_conf.turn.port, env_conf.turn.port); + } +} diff --git a/src/lib.rs b/src/lib.rs index ea4c8de16..8e0085d48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! Medea media server application. +//! Medea media server. #[macro_use] pub mod utils; @@ -10,166 +10,29 @@ pub mod shutdown; pub mod signalling; pub mod turn; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; -use actix::prelude::*; -use derive_more::Display; -use failure::{Error, Fail}; -use futures::future::{Either, Future, IntoFuture as _}; +use crate::{conf::Conf, turn::TurnAuthService}; -use crate::{ - api::{ - client::server::Server, - control::{load_static_specs_from_dir, RoomId}, - }, - conf::Conf, - log::prelude::*, - shutdown::GracefulShutdown, - signalling::{room::RoomError, room_repo::RoomRepository, Room}, - turn::{service, TurnServiceErr}, -}; +/// Global application context. +#[derive(Clone, Debug)] +pub struct AppContext { + /// [Medea] configuration. + /// + /// [Medea]: https://github.com/instrumentisto/medea + pub config: Arc, -/// Errors which can happen while server starting. -#[derive(Debug, Fail, Display)] -pub enum ServerStartError { - /// Duplicate [`RoomId`] founded. - #[display(fmt = "Duplicate of room ID '{:?}'", _0)] - DuplicateRoomId(RoomId), - - /// Some error happened while loading Control API spec. - #[display(fmt = "Failed to load specs. {}", _0)] - LoadSpec(failure::Error), - - /// Some error happened while creating new [`Room`] from Control API spec. - #[display(fmt = "Bad room spec. {}", _0)] - BadRoomSpec(String), - - /// Unexpected error returned from [`Room`]. - #[display(fmt = "Unknown room error.")] - UnknownRoomError, + /// Reference to [`TurnAuthService`]. + pub turn_service: Arc, } -impl From for ServerStartError { - fn from(err: RoomError) -> Self { - match err { - RoomError::BadRoomSpec(m) => Self::BadRoomSpec(m), - _ => Self::UnknownRoomError, +impl AppContext { + /// Creates new [`AppContext`]. + #[inline] + pub fn new(config: Conf, turn: Arc) -> Self { + Self { + config: Arc::new(config), + turn_service: turn, } } } - -pub fn run() -> Result<(), Error> { - dotenv::dotenv().ok(); - let logger = log::new_dual_logger(std::io::stdout(), std::io::stderr()); - let _scope_guard = slog_scope::set_global_logger(logger); - slog_stdlog::init()?; - - let config = Conf::parse()?; - info!("{:?}", config); - - actix::run(|| { - start_static_rooms(&config) - .map_err(|e| error!("Turn: {:?}", e)) - .map(Result::unwrap) - .map(move |(res, graceful_shutdown)| { - (res, graceful_shutdown, config) - }) - .map(|(res, graceful_shutdown, config)| { - let rooms = res; - info!( - "Loaded rooms: {:?}", - rooms.iter().map(|(id, _)| &id.0).collect::>() - ); - let room_repo = RoomRepository::new(rooms); - - (room_repo, graceful_shutdown, config) - }) - .and_then(|(room_repo, graceful_shutdown, config)| { - Server::run(room_repo, config) - .map_err(|e| error!("Error starting server: {:?}", e)) - .map(|server| { - graceful_shutdown - .send(shutdown::Subscribe(shutdown::Subscriber { - addr: server.recipient(), - priority: shutdown::Priority(1), - })) - .map_err(|e| error!("Shutdown sub: {}", e)) - .map(|_| ()) - }) - .into_future() - .flatten() - }) - }) - .unwrap(); - - Ok(()) -} - -/// Parses static [`Room`]s from config and starts them in separate arbiters. -/// -/// Returns [`ServerStartError::DuplicateRoomId`] if find duplicated room ID. -/// -/// Returns [`ServerStartError::LoadSpec`] if some error happened -/// while loading spec. -/// -/// Returns [`ServerStartError::BadRoomSpec`] -/// if some error happened while creating room from spec. -// TODO: temporary solution, changed in 32-grpc-dynamic-control-api branch -pub fn start_static_rooms( - conf: &Conf, -) -> impl Future< - Item = Result< - (HashMap>, Addr), - ServerStartError, - >, - Error = TurnServiceErr, -> { - let graceful_shutdown = - GracefulShutdown::new(conf.shutdown.timeout).start(); - let config = conf.clone(); - let static_specs_path = config.control.static_specs_dir.clone(); - if let Ok(static_specs_dir) = std::fs::read_dir(&static_specs_path) { - Either::A(service::new_turn_auth_service(&config.turn).map( - move |turn_auth_service| { - let room_specs = - match load_static_specs_from_dir(static_specs_dir) { - Ok(r) => r, - Err(e) => return Err(ServerStartError::LoadSpec(e)), - }; - let mut rooms = HashMap::new(); - let arbiter = Arbiter::new(); - - for spec in room_specs { - if rooms.contains_key(spec.id()) { - return Err(ServerStartError::DuplicateRoomId( - spec.id().clone(), - )); - } - - let room_id = spec.id().clone(); - let rpc_reconnect_timeout = config.rpc.reconnect_timeout; - let turn_cloned = Arc::clone(&turn_auth_service); - let room = Room::start_in_arbiter(&arbiter, move |_| { - Room::new(&spec, rpc_reconnect_timeout, turn_cloned) - .unwrap() - }); - graceful_shutdown.do_send(shutdown::Subscribe( - shutdown::Subscriber { - addr: room.clone().recipient(), - priority: shutdown::Priority(2), - }, - )); - rooms.insert(room_id, room); - } - - Ok((rooms, graceful_shutdown)) - }, - )) - } else { - warn!( - "'{}' dir not found. Static Control API specs will not be loaded.", - static_specs_path - ); - Either::B(futures::future::ok(Ok((HashMap::new(), graceful_shutdown)))) - } -} diff --git a/src/log/mod.rs b/src/log/mod.rs index d2ab8bb0d..ae3c13d90 100644 --- a/src/log/mod.rs +++ b/src/log/mod.rs @@ -63,6 +63,9 @@ fn add_default_keys(logger: &Logger) -> Logger { "msg" => PushFnValue(move |record : &Record, ser| { ser.emit(record.msg()) }), + "fqn" => PushFnValue(move |record : &Record, ser| { + ser.emit(format_args!("{}:{}", record.module(), record.line())) + }), "time" => PushFnValue(move |_ : &Record, ser| { ser.emit(Local::now().to_rfc3339()) }), diff --git a/src/main.rs b/src/main.rs index 54e22b3d4..73c9ef8f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,72 @@ -fn main() -> Result<(), failure::Error> { - medea::run() +//! Medea media server application. + +use std::collections::HashMap; + +use actix::Actor; +use failure::Error; +use futures::future::Future; +use medea::{ + api::{client::server::Server, control::grpc}, + conf::Conf, + log::{self, prelude::*}, + shutdown::{self, GracefulShutdown}, + signalling::{room_repo::RoomRepository, room_service::RoomService}, + turn::new_turn_auth_service, + AppContext, +}; + +fn main() -> Result<(), Error> { + dotenv::dotenv().ok(); + let config = Conf::parse()?; + info!("{:?}", config); + if let Some(lvl) = config.log.level() { + std::env::set_var("RUST_LOG", lvl.as_str()); + } + + let logger = log::new_dual_logger(std::io::stdout(), std::io::stderr()); + let _scope_guard = slog_scope::set_global_logger(logger); + slog_stdlog::init()?; + + actix::run(move || { + new_turn_auth_service(&config.turn) + .map_err(move |e| error!("{:?}", e)) + .map(|turn_service| { + let graceful_shutdown = + GracefulShutdown::new(config.shutdown.timeout).start(); + (turn_service, graceful_shutdown, config) + }) + .and_then(move |(turn_service, graceful_shutdown, config)| { + let app_context = AppContext::new(config.clone(), turn_service); + + let room_repo = RoomRepository::new(HashMap::new()); + let room_service = RoomService::new( + room_repo.clone(), + app_context.clone(), + graceful_shutdown.clone(), + ) + .start(); + + medea::api::control::start_static_rooms(&room_service).map( + move |_| { + let grpc_addr = + grpc::server::run(room_service, &app_context); + shutdown::subscribe( + &graceful_shutdown, + grpc_addr.recipient(), + shutdown::Priority(1), + ); + + let server = Server::run(room_repo, config).unwrap(); + shutdown::subscribe( + &graceful_shutdown, + server.recipient(), + shutdown::Priority(1), + ); + }, + ) + }) + }) + .unwrap(); + + Ok(()) } diff --git a/src/shutdown.rs b/src/shutdown.rs index 44b1efd3d..e50ecfa36 100644 --- a/src/shutdown.rs +++ b/src/shutdown.rs @@ -9,7 +9,7 @@ use std::{ use actix::AsyncContext; use actix::{ prelude::{Actor, Context}, - Handler, Message, Recipient, ResponseActFuture, System, WrapFuture as _, + Addr, Handler, Message, Recipient, ResponseFuture, System, }; use derive_more::Display; use failure::Fail; @@ -100,18 +100,14 @@ impl Actor for GracefulShutdown { struct OsSignal(i32); impl Handler for GracefulShutdown { - type Result = ResponseActFuture; + type Result = ResponseFuture<(), ()>; - fn handle( - &mut self, - sig: OsSignal, - _: &mut Context, - ) -> ResponseActFuture { + fn handle(&mut self, sig: OsSignal, _: &mut Context) -> Self::Result { info!("OS signal '{}' received", sig.0); match self.state { State::InProgress => { - return Box::new(future::ok(()).into_actor(self)); + return Box::new(future::ok(())); } State::Listening => { self.state = State::InProgress; @@ -122,7 +118,7 @@ impl Handler for GracefulShutdown { if self.subs.is_empty() { System::current().stop(); - return Box::new(future::ok(()).into_actor(self)); + return Box::new(future::ok(())); } let by_priority: Vec<_> = self @@ -154,8 +150,7 @@ impl Handler for GracefulShutdown { .map(|_| { info!("Graceful shutdown succeeded, stopping system"); System::current().stop() - }) - .into_actor(self), + }), ) } } @@ -177,7 +172,7 @@ pub struct Subscriber { /// Message that [`Subscriber`] subscribes to shutdown messages with. #[derive(Message)] #[rtype(result = "Result<(), ShuttingDownError>")] -pub struct Subscribe(pub Subscriber); +struct Subscribe(pub Subscriber); impl Handler for GracefulShutdown { type Result = Result<(), ShuttingDownError>; @@ -204,7 +199,7 @@ pub struct ShuttingDownError; /// notifications with. #[derive(Message)] #[rtype(result = "()")] -pub struct Unsubscribe(pub Subscriber); +struct Unsubscribe(pub Subscriber); impl Handler for GracefulShutdown { type Result = (); @@ -223,3 +218,27 @@ impl Handler for GracefulShutdown { } } } + +/// Subscribes recipient to [`GracefulShutdown`]. +pub fn subscribe( + shutdown_addr: &Addr, + subscriber: Recipient, + priority: Priority, +) { + shutdown_addr.do_send(Subscribe(Subscriber { + priority, + addr: subscriber, + })); +} + +/// Unsubscribes recipient from [`GracefulShutdown`]. +pub fn unsubscribe( + shutdown_addr: &Addr, + subscriber: Recipient, + priority: Priority, +) { + shutdown_addr.do_send(Unsubscribe(Subscriber { + priority, + addr: subscriber, + })); +} diff --git a/src/signalling/elements/endpoints/mod.rs b/src/signalling/elements/endpoints/mod.rs index cadc86a3c..9e27c655a 100644 --- a/src/signalling/elements/endpoints/mod.rs +++ b/src/signalling/elements/endpoints/mod.rs @@ -1,3 +1,26 @@ -//! Medea endpoints implementations. +//! [Medea] endpoints implementations. +//! +//! [Medea]: https://github.com/instrumentisto/medea pub mod webrtc; + +use derive_more::From; +use medea_control_api_proto::grpc::api::Element as RootElementProto; + +/// Enum which can store all kinds of [Medea] endpoints. +/// +/// [Medea]: https://github.com/instrumentisto/medea +#[derive(Clone, Debug, From)] +pub enum Endpoint { + WebRtcPublishEndpoint(webrtc::WebRtcPublishEndpoint), + WebRtcPlayEndpoint(webrtc::WebRtcPlayEndpoint), +} + +impl Into for Endpoint { + fn into(self) -> RootElementProto { + match self { + Self::WebRtcPublishEndpoint(play) => play.into(), + Self::WebRtcPlayEndpoint(publish) => publish.into(), + } + } +} diff --git a/src/signalling/elements/endpoints/webrtc/mod.rs b/src/signalling/elements/endpoints/webrtc/mod.rs index aacfc952b..162c5ba2d 100644 --- a/src/signalling/elements/endpoints/webrtc/mod.rs +++ b/src/signalling/elements/endpoints/webrtc/mod.rs @@ -4,6 +4,6 @@ pub mod play_endpoint; pub mod publish_endpoint; #[doc(inline)] -pub use play_endpoint::{WebRtcPlayEndpoint, WebRtcPlayId}; +pub use play_endpoint::WebRtcPlayEndpoint; #[doc(inline)] -pub use publish_endpoint::{WebRtcPublishEndpoint, WebRtcPublishId}; +pub use publish_endpoint::WebRtcPublishEndpoint; diff --git a/src/signalling/elements/endpoints/webrtc/play_endpoint.rs b/src/signalling/elements/endpoints/webrtc/play_endpoint.rs index c09a004e9..b6592143c 100644 --- a/src/signalling/elements/endpoints/webrtc/play_endpoint.rs +++ b/src/signalling/elements/endpoints/webrtc/play_endpoint.rs @@ -5,11 +5,16 @@ use std::{ rc::{Rc, Weak}, }; -use derive_more::{Display, From}; use medea_client_api_proto::PeerId; +use medea_control_api_proto::grpc::api::{ + Element as RootElementProto, Member_Element as ElementProto, + WebRtcPlayEndpoint as WebRtcPlayEndpointProto, +}; use crate::{ - api::control::endpoint::SrcUri, + api::control::{ + endpoints::webrtc_play_endpoint::WebRtcPlayId as Id, refs::SrcUri, + }, signalling::elements::{ endpoints::webrtc::publish_endpoint::WeakWebRtcPublishEndpoint, member::WeakMember, Member, @@ -18,13 +23,6 @@ use crate::{ use super::publish_endpoint::WebRtcPublishEndpoint; -#[doc(inline)] -pub use Id as WebRtcPlayId; - -/// ID of endpoint. -#[derive(Clone, Debug, Eq, Hash, PartialEq, From, Display)] -pub struct Id(pub String); - #[derive(Debug, Clone)] struct WebRtcPlayEndpointInner { /// ID of this [`WebRtcPlayEndpoint`]. @@ -91,7 +89,7 @@ impl Drop for WebRtcPlayEndpointInner { /// Signalling representation of Control API's [`WebRtcPlayEndpoint`]. /// -/// [`WebRtcPlayEndpoint`]: crate::api::control::endpoint::WebRtcPlayEndpoint +/// [`WebRtcPlayEndpoint`]: crate::api::control::endpoints::WebRtcPlayEndpoint #[allow(clippy::module_name_repetitions)] #[derive(Debug, Clone)] pub struct WebRtcPlayEndpoint(Rc>); @@ -193,8 +191,31 @@ impl WeakWebRtcPlayEndpoint { /// Upgrades to [`WebRtcPlayEndpoint`] safely. /// - /// If weak pointer has been dropped. + /// Returns `None` if weak pointer has been dropped. pub fn safe_upgrade(&self) -> Option { self.0.upgrade().map(WebRtcPlayEndpoint) } } + +impl Into for WebRtcPlayEndpoint { + fn into(self) -> ElementProto { + let mut element = ElementProto::new(); + let mut play = WebRtcPlayEndpointProto::new(); + play.set_src(self.src_uri().to_string()); + play.set_id(self.id().to_string()); + element.set_webrtc_play(play); + + element + } +} + +impl Into for WebRtcPlayEndpoint { + fn into(self) -> RootElementProto { + let mut element = RootElementProto::new(); + let mut member_element: ElementProto = self.into(); + let endpoint = member_element.take_webrtc_play(); + element.set_webrtc_play(endpoint); + + element + } +} diff --git a/src/signalling/elements/endpoints/webrtc/publish_endpoint.rs b/src/signalling/elements/endpoints/webrtc/publish_endpoint.rs index 851c49987..596d540bb 100644 --- a/src/signalling/elements/endpoints/webrtc/publish_endpoint.rs +++ b/src/signalling/elements/endpoints/webrtc/publish_endpoint.rs @@ -6,11 +6,16 @@ use std::{ rc::{Rc, Weak}, }; -use derive_more::{Display, From}; use medea_client_api_proto::PeerId; +use medea_control_api_proto::grpc::api::{ + Element as RootElementProto, Member_Element as ElementProto, + WebRtcPublishEndpoint as WebRtcPublishEndpointProto, +}; use crate::{ - api::control::endpoint::P2pMode, + api::control::endpoints::webrtc_publish_endpoint::{ + P2pMode, WebRtcPublishId as Id, + }, signalling::elements::{ endpoints::webrtc::play_endpoint::WeakWebRtcPlayEndpoint, member::WeakMember, Member, @@ -19,13 +24,6 @@ use crate::{ use super::play_endpoint::WebRtcPlayEndpoint; -#[doc(inline)] -pub use Id as WebRtcPublishId; - -/// ID of [`WebRtcPublishEndpoint`]. -#[derive(Clone, Debug, Eq, Hash, PartialEq, From, Display)] -pub struct Id(pub String); - #[derive(Clone, Debug)] struct WebRtcPublishEndpointInner { /// ID of this [`WebRtcPublishEndpoint`]. @@ -105,23 +103,18 @@ impl WebRtcPublishEndpointInner { /// Signalling representation of [`WebRtcPublishEndpoint`]. /// /// [`WebRtcPublishEndpoint`]: -/// crate::api::control::endpoint::WebRtcPublishEndpoint +/// crate::api::control::endpoints::WebRtcPublishEndpoint #[allow(clippy::module_name_repetitions)] #[derive(Debug, Clone)] pub struct WebRtcPublishEndpoint(Rc>); impl WebRtcPublishEndpoint { /// Creates new [`WebRtcPublishEndpoint`]. - pub fn new( - id: Id, - p2p: P2pMode, - sinks: Vec, - owner: WeakMember, - ) -> Self { + pub fn new(id: Id, p2p: P2pMode, owner: WeakMember) -> Self { Self(Rc::new(RefCell::new(WebRtcPublishEndpointInner { id, p2p, - sinks, + sinks: Vec::new(), owner, peer_ids: HashSet::new(), }))) @@ -193,6 +186,11 @@ impl WebRtcPublishEndpoint { .retain(|e| e.safe_upgrade().is_some()); } + /// Peer-to-peer mode of this [`WebRtcPublishEndpoint`]. + pub fn p2p(&self) -> P2pMode { + self.0.borrow().p2p.clone() + } + /// Downgrades [`WebRtcPublishEndpoint`] to weak pointer /// [`WeakWebRtcPublishEndpoint`]. pub fn downgrade(&self) -> WeakWebRtcPublishEndpoint { @@ -229,3 +227,25 @@ impl WeakWebRtcPublishEndpoint { self.0.upgrade().map(WebRtcPublishEndpoint) } } + +impl Into for WebRtcPublishEndpoint { + fn into(self) -> ElementProto { + let mut element = ElementProto::new(); + let mut publish = WebRtcPublishEndpointProto::new(); + publish.set_p2p(self.p2p().into()); + publish.set_id(self.id().to_string()); + element.set_webrtc_pub(publish); + + element + } +} + +impl Into for WebRtcPublishEndpoint { + fn into(self) -> RootElementProto { + let mut element = RootElementProto::new(); + let mut member_element: ElementProto = self.into(); + let endpoint = member_element.take_webrtc_pub(); + element.set_webrtc_pub(endpoint); + element + } +} diff --git a/src/signalling/elements/member.rs b/src/signalling/elements/member.rs index 1d6144990..9796ca869 100644 --- a/src/signalling/elements/member.rs +++ b/src/signalling/elements/member.rs @@ -12,15 +12,25 @@ use std::{ use derive_more::Display; use failure::Fail; use medea_client_api_proto::{IceServer, PeerId}; +use medea_control_api_proto::grpc::api::{ + Element as RootElementProto, Member as MemberProto, + Room_Element as ElementProto, +}; use crate::{ - api::control::{MemberId, MemberSpec, RoomSpec, TryFromElementError}, + api::control::{ + endpoints::WebRtcPlayEndpoint as WebRtcPlayEndpointSpec, + refs::{Fid, StatefulFid, ToEndpoint, ToMember, ToRoom}, + EndpointId, MemberId, MemberSpec, RoomId, RoomSpec, + TryFromElementError, WebRtcPlayId, WebRtcPublishId, + }, log::prelude::*, media::IceUser, }; -use super::endpoints::webrtc::{ - WebRtcPlayEndpoint, WebRtcPlayId, WebRtcPublishEndpoint, WebRtcPublishId, +use super::endpoints::{ + webrtc::{WebRtcPlayEndpoint, WebRtcPublishEndpoint}, + Endpoint, }; /// Errors which may occur while loading [`Member`]s from [`RoomSpec`]. @@ -28,23 +38,27 @@ use super::endpoints::webrtc::{ pub enum MembersLoadError { /// Errors that can occur when we try transform some spec from `Element`. #[display(fmt = "TryFromElementError: {}", _0)] - TryFromError(TryFromElementError), + TryFromError(TryFromElementError, StatefulFid), /// [`Member`] not found. - #[display(fmt = "Member with id '{}' not found.", _0)] - MemberNotFound(MemberId), + #[display(fmt = "Member [id = {}] not found", _0)] + MemberNotFound(Fid), - /// [`Endpoint`] not found. + /// [`EndpointSpec`] not found. /// - /// [`Endpoint`]: crate::api::control::endpoint::Endpoint - #[display(fmt = "Endpoint with id '{}' not found.", _0)] + /// [`EndpointSpec`]: crate::api::control::endpoints::EndpointSpec + #[display( + fmt = "Endpoint [id = {}] was referenced but not found in spec", + _0 + )] EndpointNotFound(String), } -impl From for MembersLoadError { - fn from(err: TryFromElementError) -> Self { - Self::TryFromError(err) - } +#[allow(clippy::module_name_repetitions, clippy::pub_enum_variant_names)] +#[derive(Debug, Fail, Display)] +pub enum MemberError { + #[display(fmt = "Endpoint [id = {}] not found.", _0)] + EndpointNotFound(Fid), } /// [`Member`] is member of [`Room`]. @@ -56,6 +70,9 @@ pub struct Member(Rc>); #[allow(clippy::module_name_repetitions)] #[derive(Debug)] struct MemberInner { + /// [`RoomId`] of [`Room`] to which this [`Member`] relates. + room_id: RoomId, + /// ID of this [`Member`]. id: MemberId, @@ -73,36 +90,62 @@ struct MemberInner { } impl Member { - /// Create new empty [`Member`]. + /// Creates new empty [`Member`]. /// /// To fill this [`Member`], you need to call [`Member::load`] /// function. - fn new(id: MemberId, credentials: String) -> Self { + pub fn new(id: MemberId, credentials: String, room_id: RoomId) -> Self { Self(Rc::new(RefCell::new(MemberInner { id, srcs: HashMap::new(), sinks: HashMap::new(), credentials, ice_user: None, + room_id, }))) } + /// Lookups [`MemberSpec`] by [`MemberId`] from [`MemberSpec`]. + /// + /// Returns [`MembersLoadError::MemberNotFound`] when member not found. + /// + /// Returns [`MembersLoadError::TryFromError`] when found element which is + /// not [`MemberSpec`]. + fn get_member_from_room_spec( + &self, + room_spec: &RoomSpec, + member_id: &MemberId, + ) -> Result { + let element = room_spec.pipeline.get(member_id).map_or( + Err(MembersLoadError::MemberNotFound(Fid::::new( + self.room_id(), + member_id.clone(), + ))), + Ok, + )?; + + MemberSpec::try_from(element).map_err(|e| { + MembersLoadError::TryFromError( + e, + Fid::::new(self.room_id(), member_id.clone()).into(), + ) + }) + } + /// Loads all sources and sinks of this [`Member`]. fn load( &self, room_spec: &RoomSpec, store: &HashMap, ) -> Result<(), MembersLoadError> { - let this_member_spec = MemberSpec::try_from( - room_spec - .pipeline - .get(&self.id().0) - .ok_or_else(|| MembersLoadError::MemberNotFound(self.id()))?, - )?; + let self_id = self.id(); + + let this_member_spec = + self.get_member_from_room_spec(room_spec, &self_id)?; let this_member = store .get(&self.id()) - .ok_or_else(|| MembersLoadError::MemberNotFound(self.id()))?; + .ok_or_else(|| MembersLoadError::MemberNotFound(self.get_fid()))?; for (spec_play_name, spec_play_endpoint) in this_member_spec.play_endpoints() @@ -111,36 +154,31 @@ impl Member { MemberId(spec_play_endpoint.src.member_id.to_string()); let publisher_member = store.get(&publisher_id).ok_or_else(|| { - MembersLoadError::MemberNotFound(publisher_id) + MembersLoadError::MemberNotFound(Fid::::new( + self.room_id(), + publisher_id, + )) })?; - let publisher_spec = MemberSpec::try_from( - room_spec - .pipeline - .get(&spec_play_endpoint.src.member_id.to_string()) - .ok_or_else(|| { - MembersLoadError::MemberNotFound( - spec_play_endpoint.src.member_id.clone(), - ) - })?, + let publisher_spec = self.get_member_from_room_spec( + room_spec, + &spec_play_endpoint.src.member_id, )?; let publisher_endpoint = publisher_spec - .get_publish_endpoint_by_id(&spec_play_endpoint.src.endpoint_id) + .get_publish_endpoint_by_id( + spec_play_endpoint.src.endpoint_id.clone(), + ) .ok_or_else(|| { MembersLoadError::EndpointNotFound( - spec_play_endpoint.src.endpoint_id.clone(), + spec_play_endpoint.src.endpoint_id.to_string(), ) })?; - if let Some(publisher) = - publisher_member.get_src_by_id(&WebRtcPublishId( - spec_play_endpoint.src.endpoint_id.to_string(), - )) - { - let new_play_endpoint_id = - WebRtcPlayId(spec_play_name.to_string()); + if let Some(publisher) = publisher_member.get_src_by_id( + &spec_play_endpoint.src.endpoint_id.to_string().into(), + ) { let new_play_endpoint = WebRtcPlayEndpoint::new( - new_play_endpoint_id, + spec_play_name, spec_play_endpoint.src.clone(), publisher.downgrade(), this_member.downgrade(), @@ -150,19 +188,15 @@ impl Member { publisher.add_sink(new_play_endpoint.downgrade()); } else { - let new_publish_id = WebRtcPublishId( - spec_play_endpoint.src.endpoint_id.to_string(), - ); + let new_publish_id = spec_play_endpoint.src.endpoint_id.clone(); let new_publish = WebRtcPublishEndpoint::new( new_publish_id, publisher_endpoint.p2p.clone(), - Vec::new(), publisher_member.downgrade(), ); - let new_self_play_id = WebRtcPlayId(spec_play_name.to_string()); let new_self_play = WebRtcPlayEndpoint::new( - new_self_play_id, + spec_play_name, spec_play_endpoint.src.clone(), new_publish.downgrade(), this_member.downgrade(), @@ -178,22 +212,37 @@ impl Member { // This is necessary to create [`WebRtcPublishEndpoint`], // to which none [`WebRtcPlayEndpoint`] refers. - this_member_spec.publish_endpoints().for_each(|(name, e)| { - let endpoint_id = WebRtcPublishId(name.clone()); - if self.srcs().get(&endpoint_id).is_none() { + this_member_spec + .publish_endpoints() + .filter(|(endpoint_id, _)| self.srcs().get(endpoint_id).is_none()) + .for_each(|(endpoint_id, e)| { self.insert_src(WebRtcPublishEndpoint::new( endpoint_id, e.p2p.clone(), - Vec::new(), this_member.downgrade(), )); - } - }); + }); Ok(()) } - /// Notify [`Member`] that some [`Peer`]s removed. + /// Returns [`Fid`] to this [`Member`]. + fn get_fid(&self) -> Fid { + Fid::::new(self.room_id(), self.id()) + } + + /// Returns [`Fid`] to some endpoint from this [`Member`]. + /// + /// __Note__ this function don't check presence of `Endpoint` in this + /// [`Member`]. + pub fn get_fid_to_endpoint( + &self, + endpoint_id: EndpointId, + ) -> Fid { + Fid::::new(self.room_id(), self.id(), endpoint_id) + } + + /// Notifies [`Member`] that some [`Peer`]s removed. /// /// All [`PeerId`]s related to this [`Member`] will be removed. /// @@ -215,12 +264,12 @@ impl Member { self.0.borrow().ice_user.as_ref().map(IceUser::servers_list) } - /// Returns and set to `None` [`IceUser`] of this [`Member`]. + /// Returns and sets to `None` [`IceUser`] of this [`Member`]. pub fn take_ice_user(&self) -> Option { self.0.borrow_mut().ice_user.take() } - /// Replace and return [`IceUser`] of this [`Member`]. + /// Replaces and returns [`IceUser`] of this [`Member`]. pub fn replace_ice_user(&self, new_ice_user: IceUser) -> Option { self.0.borrow_mut().ice_user.replace(new_ice_user) } @@ -282,13 +331,75 @@ impl Member { self.0.borrow_mut().srcs.remove(id); } + /// Takes sink from [`Member`]'s `sinks`. + pub fn take_sink(&self, id: &WebRtcPlayId) -> Option { + self.0.borrow_mut().sinks.remove(id) + } + + /// Takes src from [`Member`]'s `srsc`. + pub fn take_src( + &self, + id: &WebRtcPublishId, + ) -> Option { + self.0.borrow_mut().srcs.remove(id) + } + + /// Returns [`RoomId`] of this [`Member`]. + pub fn room_id(&self) -> RoomId { + self.0.borrow().room_id.clone() + } + + /// Creates new [`WebRtcPlayEndpoint`] based on provided + /// [`WebRtcPlayEndpointSpec`]. + /// + /// This function will add created [`WebRtcPlayEndpoint`] to `src`s of + /// [`WebRtcPublishEndpoint`] and to provided [`Member`]. + pub fn create_sink( + member: &Rc, + id: WebRtcPlayId, + spec: WebRtcPlayEndpointSpec, + ) { + let src = member.get_src_by_id(&spec.src.endpoint_id).unwrap(); + + let sink = WebRtcPlayEndpoint::new( + id, + spec.src, + src.downgrade(), + member.downgrade(), + ); + + src.add_sink(sink.downgrade()); + member.insert_sink(sink); + } + + /// Lookups [`WebRtcPublishEndpoint`] and [`WebRtcPlayEndpoint`] at one + /// moment by ID. + pub fn get_endpoint_by_id( + &self, + id: String, + ) -> Result { + let webrtc_publish_id = id.into(); + if let Some(publish_endpoint) = self.get_src_by_id(&webrtc_publish_id) { + return Ok(Endpoint::WebRtcPublishEndpoint(publish_endpoint)); + } + + let webrtc_play_id = String::from(webrtc_publish_id).into(); + if let Some(play_endpoint) = self.get_sink_by_id(&webrtc_play_id) { + return Ok(Endpoint::WebRtcPlayEndpoint(play_endpoint)); + } + + Err(MemberError::EndpointNotFound( + self.get_fid_to_endpoint(webrtc_play_id.into()), + )) + } + /// Downgrades strong [`Member`]'s pointer to weak [`WeakMember`] pointer. pub fn downgrade(&self) -> WeakMember { WeakMember(Rc::downgrade(&self.0)) } /// Compares pointers. If both pointers point to the same address, then - /// returns true. + /// returns `true`. #[cfg(test)] pub fn ptr_eq(&self, another_member: &Self) -> bool { Rc::ptr_eq(&self.0, &another_member.0) @@ -301,14 +412,14 @@ impl Member { pub struct WeakMember(Weak>); impl WeakMember { - /// Upgrade weak pointer to strong pointer. + /// Upgrades weak pointer to strong pointer. /// /// This function will __panic__ if weak pointer was dropped. pub fn upgrade(&self) -> Member { Member(Weak::upgrade(&self.0).unwrap()) } - /// Safe upgrade to [`Member`]. + /// Safely upgrades to [`Member`]. pub fn safe_upgrade(&self) -> Option { Weak::upgrade(&self.0).map(Member) } @@ -321,13 +432,21 @@ impl WeakMember { pub fn parse_members( room_spec: &RoomSpec, ) -> Result, MembersLoadError> { - let members_spec = room_spec.members()?; + let members_spec = room_spec.members().map_err(|e| { + MembersLoadError::TryFromError( + e, + Fid::::new(room_spec.id.clone()).into(), + ) + })?; let members: HashMap = members_spec .iter() .map(|(id, member)| { - let new_member = - Member::new(id.clone(), member.credentials().to_string()); + let new_member = Member::new( + id.clone(), + member.credentials().to_string(), + room_spec.id.clone(), + ); (id.clone(), new_member) }) .collect(); @@ -360,6 +479,41 @@ pub fn parse_members( Ok(members) } +impl Into for Member { + fn into(self) -> ElementProto { + let mut element = ElementProto::new(); + let mut member = MemberProto::new(); + + let mut member_pipeline = HashMap::new(); + for (id, play) in self.sinks() { + member_pipeline.insert(id.to_string(), play.into()); + } + for (id, publish) in self.srcs() { + member_pipeline.insert(id.to_string(), publish.into()); + } + member.set_pipeline(member_pipeline); + + member.set_id(self.id().to_string()); + member.set_credentials(self.credentials()); + + element.set_member(member); + + element + } +} + +impl Into for Member { + fn into(self) -> RootElementProto { + let mut member_element: ElementProto = self.into(); + let member = member_element.take_member(); + + let mut element = RootElementProto::new(); + element.set_member(member); + + element + } +} + #[cfg(test)] mod tests { use crate::api::control::{MemberId, RootElement}; diff --git a/src/signalling/mod.rs b/src/signalling/mod.rs index 66f573421..085eb3995 100644 --- a/src/signalling/mod.rs +++ b/src/signalling/mod.rs @@ -8,6 +8,7 @@ pub mod participants; pub mod peers; pub mod room; pub mod room_repo; +pub mod room_service; #[doc(inline)] pub use self::room::Room; diff --git a/src/signalling/participants.rs b/src/signalling/participants.rs index a77b81799..6b7840eba 100644 --- a/src/signalling/participants.rs +++ b/src/signalling/participants.rs @@ -14,8 +14,7 @@ use std::{ }; use actix::{ - fut::wrap_future, ActorFuture, AsyncContext, Context, MailboxError, - SpawnHandle, + fut::wrap_future, ActorFuture, AsyncContext, Context, SpawnHandle, }; use derive_more::Display; use failure::Fail; @@ -23,7 +22,6 @@ use futures::{ future::{self, join_all, Either}, Future, }; - use medea_client_api_proto::{CloseDescription, CloseReason, Event}; use crate::{ @@ -32,27 +30,45 @@ use crate::{ AuthorizationError, ClosedReason, EventMessage, RpcConnection, RpcConnectionClosed, }, - control::{MemberId, RoomId, RoomSpec}, + control::{ + refs::{Fid, ToEndpoint, ToMember}, + MemberId, RoomId, RoomSpec, + }, }, log::prelude::*, media::IceUser, signalling::{ - elements::{parse_members, Member, MembersLoadError}, + elements::{ + member::MemberError, parse_members, Member, MembersLoadError, + }, room::{ActFuture, RoomError}, Room, }, turn::{TurnAuthService, TurnServiceErr, UnreachablePolicy}, + AppContext, }; #[allow(clippy::module_name_repetitions)] #[derive(Debug, Display, Fail)] pub enum ParticipantServiceErr { + /// Some error happened in [`TurnAuthService`]. + /// + /// [`TurnAuthService`]: crate::turn::service::TurnAuthService #[display(fmt = "TurnService Error in ParticipantService: {}", _0)] TurnServiceErr(TurnServiceErr), - #[display(fmt = "Mailbox error when accessing ParticipantService: {}", _0)] - MailBoxErr(MailboxError), - #[display(fmt = "Participant with Id [{}] was not found", _0)] - ParticipantNotFound(MemberId), + + /// [`Member`] with provided [`Fid`] not found. + #[display(fmt = "Participant [id = {}] not found", _0)] + ParticipantNotFound(Fid), + + /// [`Endpoint`] with provided URI not found. + /// + /// [`Endpoint`]: crate::signalling::elements::endpoints::Endpoint + #[display(fmt = "Endpoint [id = {}] not found.", _0)] + EndpointNotFound(Fid), + + /// Some error happened in [`Member`]. + MemberError(MemberError), } impl From for ParticipantServiceErr { @@ -61,9 +77,9 @@ impl From for ParticipantServiceErr { } } -impl From for ParticipantServiceErr { - fn from(err: MailboxError) -> Self { - Self::MailBoxErr(err) +impl From for ParticipantServiceErr { + fn from(err: MemberError) -> Self { + Self::MemberError(err) } } @@ -78,9 +94,6 @@ pub struct ParticipantService { /// [`Member`]s which currently are present in this [`Room`]. members: HashMap, - /// Service for managing authorization on Turn server. - turn: Arc, - /// Established [`RpcConnection`]s of [`Member`]s in this [`Room`]. /// /// [`Member`]: crate::signalling::elements::member::Member @@ -88,42 +101,74 @@ pub struct ParticipantService { // as the set of all possible RpcConnection types is not closed. connections: HashMap>, - /// Timeout for close [`RpcConnection`] after receiving - /// [`RpcConnectionClosed`] message. - reconnect_timeout: Duration, - /// Stores [`RpcConnection`] drop tasks. /// If [`RpcConnection`] is lost, [`Room`] waits for connection_timeout /// before dropping it irrevocably in case it gets reestablished. drop_connection_tasks: HashMap, + + /// Reference to [`TurnAuthService`]. + turn_service: Arc, + + /// Duration, after which the server deletes the client session if + /// the remote RPC client does not reconnect after it is idle. + rpc_reconnect_timeout: Duration, } impl ParticipantService { /// Creates new [`ParticipantService`] from [`RoomSpec`]. pub fn new( room_spec: &RoomSpec, - reconnect_timeout: Duration, - turn: Arc, + context: &AppContext, ) -> Result { Ok(Self { room_id: room_spec.id().clone(), members: parse_members(room_spec)?, - turn, connections: HashMap::new(), - reconnect_timeout, drop_connection_tasks: HashMap::new(), + turn_service: context.turn_service.clone(), + rpc_reconnect_timeout: context.config.rpc.reconnect_timeout, }) } /// Lookups [`Member`] by provided [`MemberId`]. - pub fn get_member_by_id(&self, id: &MemberId) -> Option<&Member> { - self.members.get(id) + pub fn get_member_by_id(&self, id: &MemberId) -> Option { + self.members.get(id).cloned() + } + + /// Generates [`Fid`] which point to some [`Member`] in this + /// [`ParticipantService`]'s [`Room`]. + /// + /// __Note__ this function don't check presence of [`Member`] in + /// [`ParticipantService`]. + pub fn get_fid_to_member(&self, member_id: MemberId) -> Fid { + Fid::::new(self.room_id.clone(), member_id) + } + + /// Lookups [`Member`] by [`MemberId`]. + /// + /// Returns [`ParticipantServiceErr::ParticipantNotFound`] if [`Member`] not + /// found. + pub fn get_member( + &self, + id: &MemberId, + ) -> Result { + self.members.get(id).cloned().map_or( + Err(ParticipantServiceErr::ParticipantNotFound( + self.get_fid_to_member(id.clone()), + )), + Ok, + ) + } + + /// Returns all [`Member`] from this [`ParticipantService`]. + pub fn members(&self) -> HashMap { + self.members.clone() } /// Lookups [`Member`] by provided [`MemberId`] and credentials. /// /// Returns [`AuthorizationError::MemberNotExists`] if lookup by - /// [`MemberId`] has failed. + /// [`MemberId`] failed. /// /// Returns [`AuthorizationError::InvalidCredentials`] if [`Member`] /// was found, but incorrect credentials were provided. @@ -131,7 +176,7 @@ impl ParticipantService { &self, member_id: &MemberId, credentials: &str, - ) -> Result<&Member, AuthorizationError> { + ) -> Result { match self.get_member_by_id(member_id) { Some(member) => { if member.credentials().eq(credentials) { @@ -179,7 +224,9 @@ impl ParticipantService { let member = match self.get_member_by_id(&member_id) { None => { return Box::new(wrap_future(future::err( - ParticipantServiceErr::ParticipantNotFound(member_id), + ParticipantServiceErr::ParticipantNotFound( + self.get_fid_to_member(member_id), + ), ))); } Some(member) => member.clone(), @@ -202,7 +249,7 @@ impl ParticipantService { )) } else { Box::new( - wrap_future(self.turn.create( + wrap_future(self.turn_service.create( member_id.clone(), self.room_id.clone(), UnreachablePolicy::ReturnErr, @@ -247,8 +294,11 @@ impl ParticipantService { debug!("Connection for member [id = {}] removed.", member_id); self.connections.remove(&member_id); ctx.spawn(wrap_future( - self.delete_ice_user(&member_id).map_err(|err| { - error!("Error deleting IceUser {:?}", err) + self.delete_ice_user(&member_id).map_err(move |err| { + error!( + "Error deleting IceUser of Member [id = {}]. {:?}", + member_id, err + ) }), )); // TODO: we have no way to handle absence of RpcConnection right @@ -257,17 +307,22 @@ impl ParticipantService { ClosedReason::Lost => { self.drop_connection_tasks.insert( member_id.clone(), - ctx.run_later(self.reconnect_timeout, move |_, ctx| { - info!( - "Member {} connection lost at {:?}. Room will be \ - stopped.", - member_id, closed_at - ); - ctx.notify(RpcConnectionClosed { - member_id, - reason: ClosedReason::Closed, - }) - }), + ctx.run_later( + self.rpc_reconnect_timeout, + move |room, ctx| { + info!( + "Member [id = {}] connection lost at {:?}. \ + Room [id = {}] will be be stopped.", + member_id, + closed_at, + room.id() + ); + ctx.notify(RpcConnectionClosed { + member_id, + reason: ClosedReason::Closed, + }) + }, + ), ); } } @@ -281,7 +336,7 @@ impl ParticipantService { // TODO: rewrite using `Option::flatten` when it will be in stable rust. match self.get_member_by_id(&member_id) { Some(member) => match member.take_ice_user() { - Some(ice_user) => self.turn.delete(vec![ice_user]), + Some(ice_user) => self.turn_service.delete(vec![ice_user]), None => Box::new(future::ok(())), }, None => Box::new(future::ok(())), @@ -320,7 +375,7 @@ impl ParticipantService { room_users.push(ice_user); } }); - self.turn + self.turn_service .delete(room_users) .map_err(|err| error!("Error removing IceUsers {:?}", err)) }); @@ -328,4 +383,40 @@ impl ParticipantService { join_all(close_fut).map(|_| ()) } + + /// Deletes [`Member`] from [`ParticipantService`], removes this user from + /// [`TurnAuthService`], closes RPC connection with him and removes drop + /// connection task. + /// + /// [`TurnAuthService`]: crate::turn::service::TurnAuthService + pub fn delete_member( + &mut self, + member_id: &MemberId, + ctx: &mut Context, + ) { + if let Some(drop) = self.drop_connection_tasks.remove(member_id) { + ctx.cancel_future(drop); + } + + if let Some(mut conn) = self.connections.remove(member_id) { + ctx.spawn(wrap_future( + conn.close(CloseDescription::new(CloseReason::Evicted)), + )); + } + + if let Some(member) = self.members.remove(member_id) { + if let Some(ice_user) = member.take_ice_user() { + let delete_ice_user_fut = self + .turn_service + .delete(vec![ice_user]) + .map_err(|err| error!("Error removing IceUser {:?}", err)); + ctx.spawn(wrap_future(delete_ice_user_fut)); + } + } + } + + /// Inserts given [`Member`] into [`ParticipantService`]. + pub fn insert_member(&mut self, id: MemberId, member: Member) { + self.members.insert(id, member); + } } diff --git a/src/signalling/peers.rs b/src/signalling/peers.rs index a8a4bad67..1e05eb10a 100644 --- a/src/signalling/peers.rs +++ b/src/signalling/peers.rs @@ -4,7 +4,7 @@ //! [`Peer`]: crate::media::peer::Peer use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, convert::{TryFrom, TryInto}, }; @@ -172,6 +172,38 @@ impl PeerRepository { } } + /// Deletes [`PeerStateMachine`]s from this [`PeerRepository`] and send + /// [`Event::PeersRemoved`] to [`Member`]s. + /// + /// __Note:__ this also deletes partner peers. + /// + /// [`Event::PeersRemoved`]: medea_client_api_proto::Event::PeersRemoved + pub fn remove_peers( + &mut self, + member_id: &MemberId, + peer_ids: HashSet, + ) -> HashMap> { + let mut removed_peers = HashMap::new(); + for peer_id in peer_ids { + if let Some(peer) = self.peers.remove(&peer_id) { + let partner_peer_id = peer.partner_peer_id(); + let partner_member_id = peer.partner_member_id(); + if self.peers.remove(&partner_peer_id).is_some() { + removed_peers + .entry(partner_member_id) + .or_insert_with(Vec::new) + .push(partner_peer_id); + } + removed_peers + .entry(member_id.clone()) + .or_insert_with(Vec::new) + .push(peer_id); + } + } + + removed_peers + } + /// Removes all [`Peer`]s related to given [`Member`]. /// Note, that this function will also remove all partners [`Peer`]s. /// @@ -212,6 +244,22 @@ impl PeerRepository { peers_to_remove } + + /// Deletes [`PeerStateMachine`] from this [`PeerRepository`] and send + /// [`Event::PeersRemoved`] to [`Member`]s. + /// + /// __Note:__ this also deletes partner peer. + /// + /// [`Event::PeersRemoved`]: medea_client_api_proto::Event::PeersRemoved + pub fn remove_peer( + &mut self, + member_id: &MemberId, + peer_id: PeerId, + ) -> HashMap> { + let mut peers_id_to_delete = HashSet::new(); + peers_id_to_delete.insert(peer_id); + self.remove_peers(&member_id, peers_id_to_delete) + } } impl From> for PeerRepository { diff --git a/src/signalling/room.rs b/src/signalling/room.rs index 3895f7c81..4e89dff24 100644 --- a/src/signalling/room.rs +++ b/src/signalling/room.rs @@ -3,16 +3,19 @@ //! //! [`Member`]: crate::signalling::elements::member::Member -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::collections::{HashMap, HashSet}; use actix::{ fut::wrap_future, Actor, ActorFuture, AsyncContext, Context, Handler, - ResponseActFuture, WrapFuture as _, + Message, ResponseActFuture, WrapFuture as _, }; use derive_more::Display; use failure::Fail; use futures::future; use medea_client_api_proto::{Command, Event, IceCandidate, PeerId, TrackId}; +use medea_control_api_proto::grpc::api::{ + Element as ElementProto, Room as RoomProto, +}; use crate::{ api::{ @@ -20,7 +23,16 @@ use crate::{ AuthorizationError, Authorize, ClosedReason, CommandMessage, RpcConnectionClosed, RpcConnectionEstablished, }, - control::{MemberId, RoomId, RoomSpec, TryFromElementError}, + control::{ + endpoints::{ + WebRtcPlayEndpoint as WebRtcPlayEndpointSpec, + WebRtcPublishEndpoint as WebRtcPublishEndpointSpec, + }, + refs::{Fid, StatefulFid, ToEndpoint, ToMember}, + room::RoomSpec, + EndpointId, EndpointSpec, MemberId, MemberSpec, RoomId, + TryFromElementError, WebRtcPlayId, WebRtcPublishId, + }, }, log::prelude::*, media::{ @@ -31,12 +43,13 @@ use crate::{ signalling::{ elements::{ endpoints::webrtc::{WebRtcPlayEndpoint, WebRtcPublishEndpoint}, + member::MemberError, Member, MembersLoadError, }, - participants::ParticipantService, + participants::{ParticipantService, ParticipantServiceErr}, peers::PeerRepository, }, - turn::TurnAuthService, + AppContext, }; /// Ergonomic type alias for using [`ActorFuture`] for [`Room`]. @@ -48,22 +61,50 @@ pub type ActFuture = pub enum RoomError { #[display(fmt = "Couldn't find Peer with [id = {}]", _0)] PeerNotFound(PeerId), - #[display(fmt = "Couldn't find Member with [id = {}]", _0)] - MemberNotFound(MemberId), + + MemberError(MemberError), + #[display(fmt = "Member [id = {}] does not have Turn credentials", _0)] NoTurnCredentials(MemberId), + #[display(fmt = "Couldn't find RpcConnection with Member [id = {}]", _0)] ConnectionNotExists(MemberId), + #[display(fmt = "Unable to send event to Member [id = {}]", _0)] UnableToSendEvent(MemberId), + #[display(fmt = "PeerError: {}", _0)] PeerError(PeerError), - #[display(fmt = "Generic room error {}", _0)] + + #[display(fmt = "{}", _0)] + MembersLoadError(MembersLoadError), + + #[display(fmt = "{}", _0)] + TryFromElementError(TryFromElementError), + + #[display(fmt = "Generic room error: {}", _0)] BadRoomSpec(String), + #[display(fmt = "Turn service error: {}", _0)] TurnServiceError(String), + + ParticipantServiceErr(ParticipantServiceErr), + #[display(fmt = "Client error:{}", _0)] ClientError(String), + + #[display(fmt = "Given Fid [fid = {}] to wrong Room [id = {}]", _0, _1)] + WrongRoomId(StatefulFid, RoomId), + + /// Try to create [`Member`] with ID which already exists. + #[display(fmt = "Member [id = {}] already exists.", _0)] + MemberAlreadyExists(Fid), + + /// Try to create [`Endpoint`] with ID which already exists. + /// + /// [`Endpoint`]: crate::signalling::elements::endpoints::Endpoint + #[display(fmt = "Endpoint [id = {}] already exists.", _0)] + EndpointAlreadyExists(Fid), } impl From for RoomError { @@ -74,13 +115,25 @@ impl From for RoomError { impl From for RoomError { fn from(err: TryFromElementError) -> Self { - Self::BadRoomSpec(format!("Element located in wrong place: {}", err)) + Self::TryFromElementError(err) } } impl From for RoomError { fn from(err: MembersLoadError) -> Self { - Self::BadRoomSpec(format!("Error while loading room spec: {}", err)) + Self::MembersLoadError(err) + } +} + +impl From for RoomError { + fn from(err: ParticipantServiceErr) -> Self { + Self::ParticipantServiceErr(err) + } +} + +impl From for RoomError { + fn from(err: MemberError) -> Self { + Self::MemberError(err) } } @@ -114,23 +167,18 @@ pub struct Room { } impl Room { - /// Create new instance of [`Room`]. + /// Creates new instance of [`Room`]. /// /// Returns [`RoomError::BadRoomSpec`] when errs while `Element` /// transformation happens. pub fn new( room_spec: &RoomSpec, - reconnect_timeout: Duration, - turn: Arc, + context: &AppContext, ) -> Result { Ok(Self { id: room_spec.id().clone(), peers: PeerRepository::from(HashMap::new()), - members: ParticipantService::new( - room_spec, - reconnect_timeout, - turn, - )?, + members: ParticipantService::new(room_spec, context)?, state: State::Started, }) } @@ -140,6 +188,11 @@ impl Room { self.id.clone() } + /// Returns reference to [`RoomId`] of this [`Room`]. + pub fn id(&self) -> &RoomId { + &self.id + } + /// Sends [`Event::PeerCreated`] to one of specified [`Peer`]s based on /// which of them has any outbound tracks. That [`Peer`] state will be /// changed to [`WaitLocalSdp`] state. Both provided peers must be in @@ -173,10 +226,11 @@ impl Room { let member_id = sender.member_id(); let ice_servers = self .members - .get_member_by_id(&member_id) - .ok_or_else(|| RoomError::MemberNotFound(member_id.clone()))? + .get_member(&member_id)? .servers_list() - .ok_or_else(|| RoomError::NoTurnCredentials(member_id.clone()))?; + .ok_or_else(|| { + RoomError::NoTurnCredentials(member_id.clone()) + })?; let peer_created = Event::PeerCreated { peer_id: sender.id(), sdp_offer: None, @@ -226,8 +280,7 @@ impl Room { let to_member_id = to_peer.member_id(); let ice_servers = self .members - .get_member_by_id(&to_member_id) - .ok_or_else(|| RoomError::MemberNotFound(to_member_id.clone()))? + .get_member(&to_member_id)? .servers_list() .ok_or_else(|| { RoomError::NoTurnCredentials(to_member_id.clone()) @@ -437,26 +490,26 @@ impl Room { first_peer: PeerId, second_peer: PeerId, ) { - let fut: ActFuture<(), ()> = match self - .send_peer_created(first_peer, second_peer) - { + let fut = match self.send_peer_created(first_peer, second_peer) { Ok(res) => { Box::new(res.then(|res, room, ctx| -> ActFuture<(), ()> { if res.is_ok() { return Box::new(future::ok(()).into_actor(room)); } error!( - "Failed handle command, because {}. Room will be \ - stopped.", + "Failed connect peers, because {}. Room [id = {}] \ + will be stopped.", res.unwrap_err(), + room.id, ); room.close_gracefully(ctx) })) } Err(err) => { error!( - "Failed handle command, because {}. Room will be stopped.", - err + "Failed connect peers, because {}. Room [id = {}] will be \ + stopped.", + err, self.id, ); self.close_gracefully(ctx) } @@ -471,7 +524,7 @@ impl Room { &mut self, ctx: &mut Context, ) -> ResponseActFuture { - info!("Closing Room [id = {:?}]", self.id); + info!("Closing Room [id = {}]", self.id); self.state = State::Stopping; Box::new( @@ -494,13 +547,16 @@ impl Room { member_id: MemberId, ctx: &mut Context, ) -> ActFuture<(), ()> { - info!("Peers {:?} removed for member '{}'.", peers_id, member_id); + info!( + "Peers {:?} removed for member [id = {}].", + peers_id, member_id + ); if let Some(member) = self.members.get_member_by_id(&member_id) { member.peers_removed(&peers_id); } else { error!( - "Participant with id {} for which received \ - Event::PeersRemoved not found. Closing room.", + "Member [id = {}] for which received Event::PeersRemoved not \ + found. Closing room.", member_id ); @@ -530,12 +586,361 @@ impl Room { }, )) } + + /// Removes [`Peer`]s and call [`Room::member_peers_removed`] for every + /// [`Member`]. + /// + /// This will delete [`Peer`]s from [`PeerRepository`] and send + /// [`Event::PeersRemoved`] event to [`Member`]. + fn remove_peers( + &mut self, + member_id: &MemberId, + peer_ids_to_remove: HashSet, + ctx: &mut Context, + ) { + debug!("Remove peers."); + self.peers + .remove_peers(&member_id, peer_ids_to_remove) + .into_iter() + .for_each(|(member_id, peers_id)| { + let fut = self.member_peers_removed(peers_id, member_id, ctx); + ctx.spawn(fut); + }); + } + + /// Deletes [`Member`] from this [`Room`] by [`MemberId`]. + fn delete_member(&mut self, member_id: &MemberId, ctx: &mut Context) { + debug!( + "Deleting Member [id = {}] in Room [id = {}].", + member_id, self.id + ); + if let Some(member) = self.members.get_member_by_id(member_id) { + let peers: HashSet = member + .sinks() + .values() + .filter_map(WebRtcPlayEndpoint::peer_id) + .chain( + member + .srcs() + .values() + .flat_map(WebRtcPublishEndpoint::peer_ids), + ) + .collect(); + + // Send PeersRemoved to `Member`s which have related to this + // `Member` `Peer`s. + self.remove_peers(&member.id(), peers, ctx); + + self.members.delete_member(member_id, ctx); + + debug!( + "Member [id = {}] deleted from Room [id = {}].", + member_id, self.id + ); + } + } + + /// Deletes endpoint from this [`Room`] by ID. + fn delete_endpoint( + &mut self, + member_id: &MemberId, + endpoint_id: EndpointId, + ctx: &mut Context, + ) { + let endpoint_id = if let Some(member) = + self.members.get_member_by_id(member_id) + { + let play_id = endpoint_id.into(); + if let Some(endpoint) = member.take_sink(&play_id) { + if let Some(peer_id) = endpoint.peer_id() { + let removed_peers = + self.peers.remove_peer(member_id, peer_id); + for (member_id, peers_ids) in removed_peers { + let fut = self + .member_peers_removed(peers_ids, member_id, ctx); + ctx.spawn(fut); + } + } + } + + let publish_id = String::from(play_id).into(); + if let Some(endpoint) = member.take_src(&publish_id) { + let peer_ids = endpoint.peer_ids(); + self.remove_peers(member_id, peer_ids, ctx); + } + + publish_id.into() + } else { + endpoint_id + }; + + debug!( + "Endpoint [id = {}] removed in Member [id = {}] from Room [id = \ + {}].", + endpoint_id, member_id, self.id + ); + } + + /// Creates new [`WebRtcPlayEndpoint`] in specified [`Member`]. + /// + /// This function will check that new [`WebRtcPublishEndpoint`]'s ID is not + /// present in [`ParticipantService`]. + /// + /// Returns [`RoomError::EndpointAlreadyExists`] when + /// [`WebRtcPublishEndpoint`]'s ID already presented in [`Member`]. + pub fn create_src_endpoint( + &mut self, + member_id: &MemberId, + publish_id: WebRtcPublishId, + spec: WebRtcPublishEndpointSpec, + ) -> Result<(), RoomError> { + let member = self.members.get_member(&member_id)?; + + let is_member_have_this_src_id = + member.get_src_by_id(&publish_id).is_some(); + + let play_id = String::from(publish_id).into(); + let is_member_have_this_sink_id = + member.get_sink_by_id(&play_id).is_some(); + + if is_member_have_this_sink_id || is_member_have_this_src_id { + return Err(RoomError::EndpointAlreadyExists( + member.get_fid_to_endpoint(play_id.into()), + )); + } + + let endpoint = WebRtcPublishEndpoint::new( + String::from(play_id).into(), + spec.p2p, + member.downgrade(), + ); + + debug!( + "Create WebRtcPublishEndpoint [id = {}] for Member [id = {}] in \ + Room [id = {}]", + endpoint.id(), + member_id, + self.id + ); + + member.insert_src(endpoint); + + Ok(()) + } + + /// Creates new [`WebRtcPlayEndpoint`] in specified [`Member`]. + /// + /// This function will check that new [`WebRtcPlayEndpoint`]'s ID is not + /// present in [`ParticipantService`]. + /// + /// Returns [`RoomError::EndpointAlreadyExists`] when + /// [`WebRtcPlayEndpoint`]'s ID already presented in [`Member`]. + pub fn create_sink_endpoint( + &mut self, + member_id: &MemberId, + endpoint_id: WebRtcPlayId, + spec: WebRtcPlayEndpointSpec, + ctx: &mut Context, + ) -> Result<(), RoomError> { + let member = self.members.get_member(&member_id)?; + + let is_member_have_this_sink_id = + member.get_sink_by_id(&endpoint_id).is_some(); + + let publish_id = String::from(endpoint_id).into(); + let is_member_have_this_src_id = + member.get_src_by_id(&publish_id).is_some(); + if is_member_have_this_sink_id || is_member_have_this_src_id { + return Err(RoomError::EndpointAlreadyExists( + member.get_fid_to_endpoint(publish_id.into()), + )); + } + + let partner_member = self.members.get_member(&spec.src.member_id)?; + let src = partner_member + .get_src_by_id(&spec.src.endpoint_id) + .ok_or_else(|| { + MemberError::EndpointNotFound( + partner_member.get_fid_to_endpoint( + spec.src.endpoint_id.clone().into(), + ), + ) + })?; + + let sink = WebRtcPlayEndpoint::new( + String::from(publish_id).into(), + spec.src, + src.downgrade(), + member.downgrade(), + ); + + src.add_sink(sink.downgrade()); + + debug!( + "Created WebRtcPlayEndpoint [id = {}] for Member [id = {}] in \ + Room [id = {}].", + sink.id(), + member_id, + self.id + ); + + member.insert_sink(sink); + + if self.members.member_has_connection(member_id) { + self.init_member_connections(&member, ctx); + } + + Ok(()) + } + + /// Creates new [`Member`] in this [`ParticipantService`]. + /// + /// This function will check that new [`Member`]'s ID is not present in + /// [`ParticipantService`]. + /// + /// Returns [`RoomError::MemberAlreadyExists`] when + /// [`Member`]'s ID already presented in [`ParticipantService`]. + pub fn create_member( + &mut self, + id: MemberId, + spec: &MemberSpec, + ) -> Result<(), RoomError> { + if self.members.get_member_by_id(&id).is_some() { + return Err(RoomError::MemberAlreadyExists( + self.members.get_fid_to_member(id), + )); + } + let signalling_member = Member::new( + id.clone(), + spec.credentials().to_string(), + self.id.clone(), + ); + + for (id, publish) in spec.publish_endpoints() { + let signalling_publish = WebRtcPublishEndpoint::new( + id.clone(), + publish.p2p.clone(), + signalling_member.downgrade(), + ); + signalling_member.insert_src(signalling_publish); + } + + for (id, play) in spec.play_endpoints() { + let partner_member = + self.members.get_member(&play.src.member_id)?; + let src = partner_member + .get_src_by_id(&play.src.endpoint_id) + .ok_or_else(|| { + MemberError::EndpointNotFound( + partner_member.get_fid_to_endpoint( + play.src.endpoint_id.clone().into(), + ), + ) + })?; + + let sink = WebRtcPlayEndpoint::new( + id.clone(), + play.src.clone(), + src.downgrade(), + signalling_member.downgrade(), + ); + + signalling_member.insert_sink(sink); + } + + // This is needed for atomicity. + for (_, sink) in signalling_member.sinks() { + let src = sink.src(); + src.add_sink(sink.downgrade()); + } + + self.members.insert_member(id, signalling_member); + + Ok(()) + } } /// [`Actor`] implementation that provides an ergonomic way /// to interact with [`Room`]. impl Actor for Room { type Context = Context; + + fn started(&mut self, _: &mut Self::Context) { + debug!("Room [id = {}] started.", self.id); + } +} + +impl Into for &mut Room { + fn into(self) -> ElementProto { + let mut element = ElementProto::new(); + let mut room = RoomProto::new(); + + let pipeline = self + .members + .members() + .into_iter() + .map(|(id, member)| (id.to_string(), member.into())) + .collect(); + + room.set_pipeline(pipeline); + room.set_id(self.id().to_string()); + element.set_room(room); + + element + } +} + +// TODO: Tightly coupled with protobuf. +// We should name this method GetElements, that will return some +// intermediate DTO, that will be serialized at the caller side. +// But lets leave it as it is for now. + +/// Message for serializing this [`Room`] and [`Room`]'s elements to protobuf +/// spec. +#[derive(Message)] +#[rtype(result = "Result, RoomError>")] +pub struct SerializeProto(pub Vec); + +impl Handler for Room { + type Result = Result, RoomError>; + + fn handle( + &mut self, + msg: SerializeProto, + _: &mut Self::Context, + ) -> Self::Result { + let mut serialized: HashMap = HashMap::new(); + for fid in msg.0 { + match &fid { + StatefulFid::Room(room_fid) => { + if room_fid.room_id() == &self.id { + let current_room: ElementProto = self.into(); + serialized.insert(fid, current_room); + } else { + return Err(RoomError::WrongRoomId( + fid, + self.id.clone(), + )); + } + } + StatefulFid::Member(member_fid) => { + let member = + self.members.get_member(member_fid.member_id())?; + serialized.insert(fid, member.into()); + } + StatefulFid::Endpoint(endpoint_fid) => { + let member = + self.members.get_member(endpoint_fid.member_id())?; + let endpoint = member.get_endpoint_by_id( + endpoint_fid.endpoint_id().to_string(), + )?; + serialized.insert(fid, endpoint.into()); + } + } + } + + Ok(serialized) + } } impl Handler for Room { @@ -545,7 +950,7 @@ impl Handler for Room { fn handle( &mut self, msg: Authorize, - _ctx: &mut Self::Context, + _: &mut Self::Context, ) -> Self::Result { self.members .get_member_by_id_and_credentials(&msg.member_id, &msg.credentials) @@ -592,17 +997,19 @@ impl Handler for Room { return Box::new(future::ok(()).into_actor(room)); } error!( - "Failed handle command, because {}. Room will be \ - stopped.", + "Failed handle command, because {}. Room [id = {}] \ + will be stopped.", res.unwrap_err(), + room.id, ); room.close_gracefully(ctx) })) } Err(err) => { error!( - "Failed handle command, because {}. Room will be stopped.", - err + "Failed handle command, because {}. Room [id = {}] will \ + be stopped.", + err, self.id, ); self.close_gracefully(ctx) } @@ -623,7 +1030,10 @@ impl Handler for Room { msg: RpcConnectionEstablished, ctx: &mut Self::Context, ) -> Self::Result { - info!("RpcConnectionEstablished for member {}", msg.member_id); + info!( + "RpcConnectionEstablished for Member [id = {}].", + msg.member_id + ); let fut = self .members @@ -647,7 +1057,8 @@ impl Handler for Room { ctx: &mut Self::Context, ) -> Self::Result { info!( - "Room: {:?} received ShutdownGracefully message so shutting down", + "Room [id = {}] received ShutdownGracefully message so shutting \ + down", self.id ); self.close_gracefully(ctx) @@ -692,3 +1103,113 @@ impl Handler for Room { } } } + +/// Signal for closing this [`Room`]. +#[derive(Message, Debug)] +#[rtype(result = "()")] +pub struct Close; + +impl Handler for Room { + type Result = (); + + fn handle(&mut self, _: Close, ctx: &mut Self::Context) -> Self::Result { + for id in self.members.members().keys() { + self.delete_member(id, ctx); + } + let drop_fut = self.members.drop_connections(ctx); + ctx.wait(wrap_future(drop_fut)); + } +} + +/// Signal for deleting elements from this [`Room`]. +#[derive(Message, Debug)] +#[rtype(result = "()")] +pub struct Delete(pub Vec); + +impl Handler for Room { + type Result = (); + + fn handle(&mut self, msg: Delete, ctx: &mut Self::Context) { + let mut member_ids = Vec::new(); + let mut endpoint_ids = Vec::new(); + for id in msg.0 { + match id { + StatefulFid::Member(member_fid) => { + member_ids.push(member_fid); + } + StatefulFid::Endpoint(endpoint_fid) => { + endpoint_ids.push(endpoint_fid); + } + _ => warn!("Found Fid while deleting __from__ Room."), + } + } + member_ids.into_iter().for_each(|fid| { + self.delete_member(&fid.member_id(), ctx); + }); + endpoint_ids.into_iter().for_each(|fid| { + let (_, member_id, endpoint_id) = fid.take_all(); + self.delete_endpoint(&member_id, endpoint_id, ctx); + }); + } +} + +/// Signal for creating new [`Member`] in this [`Room`]. +#[derive(Message, Debug)] +#[rtype(result = "Result<(), RoomError>")] +pub struct CreateMember(pub MemberId, pub MemberSpec); + +impl Handler for Room { + type Result = Result<(), RoomError>; + + fn handle( + &mut self, + msg: CreateMember, + _: &mut Self::Context, + ) -> Self::Result { + self.create_member(msg.0.clone(), &msg.1)?; + debug!( + "Member [id = {}] created in Room [id = {}].", + msg.0, self.id + ); + Ok(()) + } +} + +/// Signal for creating new `Endpoint` from [`EndpointSpec`]. +#[derive(Message, Debug)] +#[rtype(result = "Result<(), RoomError>")] +pub struct CreateEndpoint { + pub member_id: MemberId, + pub endpoint_id: EndpointId, + pub spec: EndpointSpec, +} + +impl Handler for Room { + type Result = Result<(), RoomError>; + + fn handle( + &mut self, + msg: CreateEndpoint, + ctx: &mut Self::Context, + ) -> Self::Result { + match msg.spec { + EndpointSpec::WebRtcPlay(endpoint) => { + self.create_sink_endpoint( + &msg.member_id, + msg.endpoint_id.into(), + endpoint, + ctx, + )?; + } + EndpointSpec::WebRtcPublish(endpoint) => { + self.create_src_endpoint( + &msg.member_id, + msg.endpoint_id.into(), + endpoint, + )?; + } + } + + Ok(()) + } +} diff --git a/src/signalling/room_repo.rs b/src/signalling/room_repo.rs index 4ba52d31a..a6b2377e1 100644 --- a/src/signalling/room_repo.rs +++ b/src/signalling/room_repo.rs @@ -10,7 +10,7 @@ use actix::Addr; use crate::{api::control::RoomId, signalling::Room}; /// Repository that stores [`Room`]s addresses. -#[derive(Clone, Default)] +#[derive(Clone, Debug, Default)] pub struct RoomRepository { // TODO: Use crossbeam's concurrent hashmap when its done. // [Tracking](https://github.com/crossbeam-rs/rfcs/issues/32). @@ -30,4 +30,20 @@ impl RoomRepository { let rooms = self.rooms.lock().unwrap(); rooms.get(id).cloned() } + + /// Removes [`Room`] from [`RoomRepository`] by [`RoomId`]. + pub fn remove(&self, id: &RoomId) { + self.rooms.lock().unwrap().remove(id); + } + + /// Adds new [`Room`] into [`RoomRepository`]. + pub fn add(&self, id: RoomId, room: Addr) { + self.rooms.lock().unwrap().insert(id, room); + } + + /// Checks existence of [`Room`] in [`RoomRepository`] by provided + /// [`RoomId`]. + pub fn contains_room_with_id(&self, id: &RoomId) -> bool { + self.rooms.lock().unwrap().contains_key(id) + } } diff --git a/src/signalling/room_service.rs b/src/signalling/room_service.rs new file mode 100644 index 000000000..0efdd5c38 --- /dev/null +++ b/src/signalling/room_service.rs @@ -0,0 +1,882 @@ +//! Service which provides CRUD actions for [`Room`]. + +use std::{collections::HashMap, marker::PhantomData}; + +use actix::{ + Actor, Addr, Context, Handler, MailboxError, Message, ResponseFuture, +}; +use derive_more::Display; +use failure::Fail; +use futures::future::{self, Future}; +use medea_control_api_proto::grpc::api::Element as ElementProto; + +use crate::{ + api::control::{ + endpoints::EndpointSpec, + load_static_specs_from_dir, + refs::{Fid, StatefulFid, ToMember, ToRoom}, + EndpointId, LoadStaticControlSpecsError, MemberId, MemberSpec, RoomId, + RoomSpec, TryFromElementError, + }, + log::prelude::*, + shutdown::{self, GracefulShutdown}, + signalling::{ + room::{ + Close, CreateEndpoint, CreateMember, Delete, RoomError, + SerializeProto, + }, + room_repo::RoomRepository, + Room, + }, + AppContext, +}; + +/// Errors of [`RoomService`]. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Fail, Display)] +pub enum RoomServiceError { + /// [`Room`] not found in [`RoomRepository`]. + #[display(fmt = "Room [id = {}] not found.", _0)] + RoomNotFound(Fid), + + /// Wrapper for [`Room`]'s [`MailboxError`]. + #[display(fmt = "Room mailbox error: {:?}", _0)] + RoomMailboxErr(MailboxError), + + /// Attempt to create [`Room`] with [`RoomId`] which already exists in + /// [`RoomRepository`]. + #[display(fmt = "Room [id = {}] already exists.", _0)] + RoomAlreadyExists(Fid), + + /// Some error happened in [`Room`]. + /// + /// For more info read [`RoomError`] docs. + RoomError(RoomError), + + /// Error which can happen while converting protobuf objects into interior + /// [medea] [Control API] objects. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + /// [medea]: https://github.com/instrumentisto/medea + TryFromElement(TryFromElementError), + + /// Error which can happen while loading static [Control API] specs. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + #[display(fmt = "Failed to load static specs. {:?}", _0)] + FailedToLoadStaticSpecs(LoadStaticControlSpecsError), + + /// Provided empty [`Fid`] list. + #[display(fmt = "Empty URIs list.")] + EmptyUrisList, + + /// Provided not the same [`RoomId`]s in [`Fid`] list. + /// + /// Atm this error can happen in `Delete` method because `Delete` should be + /// called only for one [`Room`]. + #[display( + fmt = "Provided not the same Room IDs in elements IDs [ids = {:?}].", + _1 + )] + NotSameRoomIds(RoomId, RoomId), +} + +impl From for RoomServiceError { + fn from(err: RoomError) -> Self { + Self::RoomError(err) + } +} + +impl From for RoomServiceError { + fn from(err: LoadStaticControlSpecsError) -> Self { + Self::FailedToLoadStaticSpecs(err) + } +} + +/// Service for controlling [`Room`]s. +pub struct RoomService { + /// Repository that stores [`Room`]s addresses. + room_repo: RoomRepository, + + /// Global app context. + /// + /// Used for providing [`AppContext`] to the newly created [`Room`]s. + app: AppContext, + + /// Address to [`GracefulShutdown`]. + /// + /// Use for subscribe newly created [`Room`]s to [`GracefulShutdown`] and + /// unsubscribe deleted [`Room`]s from [`GracefulShutdown`]. + graceful_shutdown: Addr, + + /// Path to directory with static [Сontrol API] specs. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + static_specs_dir: String, + + /// Public URL of server. Address for exposed [Client API]. + /// + /// [Client API]: https://tinyurl.com/yx9thsnr + public_url: String, +} + +impl RoomService { + /// Creates new [`RoomService`]. + pub fn new( + room_repo: RoomRepository, + app: AppContext, + graceful_shutdown: Addr, + ) -> Self { + Self { + static_specs_dir: app.config.control.static_specs_dir.clone(), + public_url: app.config.server.client.http.public_url.clone(), + room_repo, + app, + graceful_shutdown, + } + } + + /// Closes [`Room`] with provided [`RoomId`]. + /// + /// This is also deletes this [`Room`] from [`RoomRepository`]. + fn close_room( + &self, + id: RoomId, + ) -> Box> { + if let Some(room) = self.room_repo.get(&id) { + shutdown::unsubscribe( + &self.graceful_shutdown, + room.clone().recipient(), + shutdown::Priority(2), + ); + + let room_repo = self.room_repo.clone(); + + Box::new(room.send(Close).map(move |_| { + debug!("Room [id = {}] removed.", id); + room_repo.remove(&id); + })) + } else { + Box::new(futures::future::ok(())) + } + } + + /// Returns [Control API] sid based on provided arguments and + /// `MEDEA_SERVER__CLIENT__HTTP__PUBLIC_URL` config value. + fn get_sid( + &self, + room_id: &RoomId, + member_id: &MemberId, + credentials: &str, + ) -> String { + format!( + "{}/{}/{}/{}", + self.public_url, room_id, member_id, credentials + ) + } +} + +impl Actor for RoomService { + type Context = Context; +} + +/// Signal for load all static specs and start [`Room`]s. +#[derive(Message)] +#[rtype(result = "Result<(), RoomServiceError>")] +pub struct StartStaticRooms; + +impl Handler for RoomService { + type Result = Result<(), RoomServiceError>; + + fn handle( + &mut self, + _: StartStaticRooms, + _: &mut Self::Context, + ) -> Self::Result { + let room_specs = load_static_specs_from_dir(&self.static_specs_dir)?; + + for spec in room_specs { + if self.room_repo.contains_room_with_id(spec.id()) { + return Err(RoomServiceError::RoomAlreadyExists( + Fid::::new(spec.id), + )); + } + + let room_id = spec.id().clone(); + + let room = Room::new(&spec, &self.app)?.start(); + shutdown::subscribe( + &self.graceful_shutdown, + room.clone().recipient(), + shutdown::Priority(2), + ); + + self.room_repo.add(room_id, room); + } + Ok(()) + } +} + +/// Type alias for success [`CreateResponse`]'s sids. +pub type Sids = HashMap; + +/// Signal for creating new [`Room`]. +#[derive(Message)] +#[rtype(result = "Result")] +pub struct CreateRoom { + /// [Control API] spec for [`Room`]. + /// + /// [Control API]: https://tinyurl.com/yxsqplq7 + pub spec: RoomSpec, +} + +impl Handler for RoomService { + type Result = Result; + + fn handle( + &mut self, + msg: CreateRoom, + _: &mut Self::Context, + ) -> Self::Result { + let room_spec = msg.spec; + let sid = match room_spec.members() { + Ok(members) => members + .iter() + .map(|(member_id, member)| { + let uri = self.get_sid( + room_spec.id(), + &member_id, + member.credentials(), + ); + (member_id.clone().to_string(), uri) + }) + .collect(), + Err(e) => return Err(RoomServiceError::TryFromElement(e)), + }; + + if self.room_repo.get(&room_spec.id).is_some() { + return Err(RoomServiceError::RoomAlreadyExists( + Fid::::new(room_spec.id), + )); + } + + let room = Room::new(&room_spec, &self.app)?; + let room_addr = room.start(); + + shutdown::subscribe( + &self.graceful_shutdown, + room_addr.clone().recipient(), + shutdown::Priority(2), + ); + + debug!("New Room [id = {}] started.", room_spec.id); + self.room_repo.add(room_spec.id, room_addr); + + Ok(sid) + } +} + +/// Signal for create new [`Member`] in [`Room`]. +/// +/// [`Member`]: crate::signalling::elements::member::Member +#[derive(Message)] +#[rtype(result = "Result")] +pub struct CreateMemberInRoom { + pub id: MemberId, + pub parent_fid: Fid, + pub spec: MemberSpec, +} + +impl Handler for RoomService { + type Result = ResponseFuture; + + fn handle( + &mut self, + msg: CreateMemberInRoom, + _: &mut Self::Context, + ) -> Self::Result { + let room_id = msg.parent_fid.take_room_id(); + let sid = self.get_sid(&room_id, &msg.id, msg.spec.credentials()); + let mut sids = HashMap::new(); + sids.insert(msg.id.to_string(), sid); + + if let Some(room) = self.room_repo.get(&room_id) { + Box::new( + room.send(CreateMember(msg.id, msg.spec)) + .map_err(RoomServiceError::RoomMailboxErr) + .and_then(move |r| { + r.map_err(RoomServiceError::from).map(move |_| sids) + }), + ) + } else { + Box::new(future::err(RoomServiceError::RoomNotFound( + Fid::::new(room_id), + ))) + } + } +} + +/// Signal for create new [`Endpoint`] in [`Room`]. +/// +/// [`Endpoint`]: crate::signalling::elements::endpoints::Endpoint +#[derive(Message)] +#[rtype(result = "Result")] +pub struct CreateEndpointInRoom { + pub id: EndpointId, + pub parent_fid: Fid, + pub spec: EndpointSpec, +} + +impl Handler for RoomService { + type Result = ResponseFuture; + + fn handle( + &mut self, + msg: CreateEndpointInRoom, + _: &mut Self::Context, + ) -> Self::Result { + let (room_id, member_id) = msg.parent_fid.take_all(); + let endpoint_id = msg.id; + + if let Some(room) = self.room_repo.get(&room_id) { + Box::new( + room.send(CreateEndpoint { + member_id, + endpoint_id, + spec: msg.spec, + }) + .map_err(RoomServiceError::RoomMailboxErr) + .and_then(|r| { + r.map_err(RoomServiceError::from).map(|_| HashMap::new()) + }), + ) + } else { + Box::new(future::err(RoomServiceError::RoomNotFound( + Fid::::new(room_id), + ))) + } + } +} + +/// State which indicates that [`DeleteElements`] message was validated and can +/// be send to [`RoomService`]. +pub struct Validated; + +/// State which indicates that [`DeleteElements`] message is unvalidated and +/// should be validated with `validate()` function of [`DeleteElements`] in +/// [`Unvalidated`] state before sending to [`RoomService`]. +pub struct Unvalidated; + +// Clippy lint show use_self errors for DeleteElements with generic state. This +// is fix for it. This allow not works on function. +#[allow(clippy::use_self)] +impl DeleteElements { + /// Creates new [`DeleteElements`] in [`Unvalidated`] state. + pub fn new() -> Self { + Self { + fids: Vec::new(), + _validation_state: PhantomData, + } + } + + /// Adds [`StatefulFid`] to request. + pub fn add_fid(&mut self, fid: StatefulFid) { + self.fids.push(fid) + } + + /// Validates request. It must have at least one fid, all fids must share + /// same [`RoomId`]. + pub fn validate( + self, + ) -> Result, RoomServiceError> { + if self.fids.is_empty() { + return Err(RoomServiceError::EmptyUrisList); + } + + let first_room_id = self.fids[0].room_id(); + + for id in &self.fids { + if first_room_id != id.room_id() { + return Err(RoomServiceError::NotSameRoomIds( + first_room_id.clone(), + id.room_id().clone(), + )); + } + } + + Ok(DeleteElements { + fids: self.fids, + _validation_state: PhantomData, + }) + } +} + +/// Signal for delete [Control API] elements. +/// +/// This message can be in two states: [`Validated`] and [`Unvalidated`]. +/// +/// For ability to send this message to [`RoomService`] [`DeleteElements`] +/// should be in [`Validated`] state. You can go to [`Validated`] state +/// from [`Unvalidated`] with [`DeleteElements::validate`] function +/// which will validate all [`StatefulFid`]s. +/// +/// Validation doesn't guarantee that message can't return [`RoomServiceError`]. +/// This is just validation for errors which we can catch before sending +/// message. +/// +/// [Control API]: https://tinyurl.com/yxsqplq7 +#[derive(Message, Default)] +#[rtype(result = "Result<(), RoomServiceError>")] +pub struct DeleteElements { + fids: Vec, + _validation_state: PhantomData, +} + +impl Handler> for RoomService { + type Result = ResponseFuture<(), RoomServiceError>; + + // TODO: delete 'clippy::unnecessary_filter_map` when drain_filter TODO will + // be resolved. + #[allow(clippy::if_not_else, clippy::unnecessary_filter_map)] + fn handle( + &mut self, + msg: DeleteElements, + _: &mut Self::Context, + ) -> Self::Result { + let mut deletes_from_room: Vec = Vec::new(); + // TODO: use Vec::drain_filter when it will be in stable + let room_messages_futs: Vec< + Box>, + > = msg + .fids + .into_iter() + .filter_map(|fid| { + if let StatefulFid::Room(room_id) = fid { + Some(self.close_room(room_id.take_room_id())) + } else { + deletes_from_room.push(fid); + None + } + }) + .collect(); + + if !room_messages_futs.is_empty() { + Box::new( + futures::future::join_all(room_messages_futs) + .map(|_| ()) + .map_err(RoomServiceError::RoomMailboxErr), + ) + } else if !deletes_from_room.is_empty() { + let room_id = deletes_from_room[0].room_id().clone(); + + if let Some(room) = self.room_repo.get(&room_id) { + Box::new( + room.send(Delete(deletes_from_room)) + .map_err(RoomServiceError::RoomMailboxErr), + ) + } else { + Box::new(future::ok(())) + } + } else { + Box::new(future::err(RoomServiceError::EmptyUrisList)) + } + } +} + +/// Serialized to protobuf `Element`s which will be returned from [`Get`] on +/// success result. +type SerializedElements = HashMap; + +/// Message which returns serialized to protobuf objects by provided +/// [`Fid`]. +#[derive(Message)] +#[rtype(result = "Result")] +pub struct Get(pub Vec); + +impl Handler for RoomService { + type Result = ResponseFuture; + + fn handle(&mut self, msg: Get, _: &mut Self::Context) -> Self::Result { + let mut rooms_elements = HashMap::new(); + for fid in msg.0 { + let room_id = fid.room_id(); + + if let Some(room) = self.room_repo.get(room_id) { + rooms_elements + .entry(room) + .or_insert_with(Vec::new) + .push(fid); + } else { + return Box::new(future::err(RoomServiceError::RoomNotFound( + fid.into(), + ))); + } + } + + let mut futs = Vec::new(); + for (room, elements) in rooms_elements { + futs.push(room.send(SerializeProto(elements))); + } + + Box::new( + futures::future::join_all(futs) + .map_err(RoomServiceError::RoomMailboxErr) + .and_then(|results| { + let mut all = HashMap::new(); + for result in results { + match result { + Ok(res) => all.extend(res), + Err(e) => return Err(RoomServiceError::from(e)), + } + } + Ok(all) + }), + ) + } +} + +#[cfg(test)] +mod delete_elements_validation_specs { + use std::convert::TryFrom as _; + + use super::*; + + #[test] + fn empty_fids_list() { + let elements = DeleteElements::new(); + match elements.validate() { + Ok(_) => panic!( + "Validation should fail with EmptyUrisList but returned Ok." + ), + Err(e) => match e { + RoomServiceError::EmptyUrisList => (), + _ => panic!( + "Validation should fail with EmptyList error but errored \ + with {:?}.", + e + ), + }, + } + } + + #[test] + fn error_if_not_same_room_ids() { + let mut elements = DeleteElements::new(); + ["room_id/member", "another_room_id/member"] + .iter() + .map(|fid| StatefulFid::try_from(fid.to_string()).unwrap()) + .for_each(|fid| elements.add_fid(fid)); + + match elements.validate() { + Ok(_) => panic!( + "Validation should fail with NotSameRoomIds but returned Ok." + ), + Err(e) => match e { + RoomServiceError::NotSameRoomIds(first, another) => { + assert_eq!(&first.to_string(), "room_id"); + assert_eq!(&another.to_string(), "another_room_id"); + } + _ => panic!( + "Validation should fail with NotSameRoomIds error but \ + errored with {:?}.", + e + ), + }, + } + } + + #[test] + fn success_if_all_ok() { + let mut elements = DeleteElements::new(); + [ + "room_id/member_id", + "room_id/another_member_id", + "room_id/member_id/endpoint_id", + ] + .iter() + .map(|fid| StatefulFid::try_from(fid.to_string()).unwrap()) + .for_each(|fid| elements.add_fid(fid)); + + assert!(elements.validate().is_ok()); + } +} + +#[cfg(test)] +mod room_service_specs { + use std::convert::TryFrom as _; + + use crate::{ + api::control::{ + endpoints::webrtc_publish_endpoint::P2pMode, + refs::{Fid, ToEndpoint}, + RootElement, + }, + conf::Conf, + }; + + use super::*; + + /// Returns [`RoomSpec`] parsed from + /// `../../tests/specs/pub-sub-video-call.yml` file. + /// + /// Note that YAML spec is loads on compile time with [`include_str`] + /// macro. + fn room_spec() -> RoomSpec { + const ROOM_SPEC: &str = + include_str!("../../tests/specs/pub-sub-video-call.yml"); + + let parsed: RootElement = serde_yaml::from_str(ROOM_SPEC).unwrap(); + RoomSpec::try_from(&parsed).unwrap() + } + + /// Returns [`AppContext`] with default [`Conf`] and mocked + /// [`TurnAuthService`]. + fn app_ctx() -> AppContext { + let turn_service = crate::turn::new_turn_auth_service_mock(); + AppContext::new(Conf::default(), turn_service) + } + + /// Returns [`Addr`] to [`RoomService`]. + fn room_service(room_repo: RoomRepository) -> Addr { + let conf = Conf::default(); + let shutdown_timeout = conf.shutdown.timeout; + + let app = app_ctx(); + let graceful_shutdown = GracefulShutdown::new(shutdown_timeout).start(); + + RoomService::new(room_repo, app, graceful_shutdown).start() + } + + /// Returns [`Future`] used for testing of all create methods of + /// [`RoomService`]. + /// + /// This macro automatically stops [`actix::System`] when test completed. + /// + /// `$room_service` - [`Addr`] to [`RoomService`], + /// + /// `$create_msg` - [`actix::Message`] which will create `Element`, + /// + /// `$element_fid` - [`StatefulFid`] to `Element` which you try to + /// create, + /// + /// `$test` - closure in which will be provided created + /// [`Element`]. + macro_rules! test_for_create { + ( + $room_service:expr, + $create_msg:expr, + $element_fid:expr, + $test:expr + ) => {{ + let get_msg = Get(vec![$element_fid.clone()]); + $room_service + .send($create_msg) + .and_then(move |res| { + res.unwrap(); + $room_service.send(get_msg) + }) + .map(move |r| { + let mut resp = r.unwrap(); + resp.remove(&$element_fid).unwrap() + }) + .map($test) + .map(|_| actix::System::current().stop()) + .map_err(|e| panic!("{:?}", e)) + }}; + } + + #[test] + fn create_room() { + let sys = actix::System::new("room-service-tests"); + + let room_service = room_service(RoomRepository::new(HashMap::new())); + let spec = room_spec(); + let caller_fid = + StatefulFid::try_from("pub-sub-video-call/caller".to_string()) + .unwrap(); + + actix::spawn(test_for_create!( + room_service, + CreateRoom { spec }, + caller_fid, + |member_el| { + assert_eq!(member_el.get_member().get_pipeline().len(), 1); + } + )); + + sys.run().unwrap(); + } + + #[test] + fn create_member() { + let sys = actix::System::new("room-service-tests"); + + let spec = room_spec(); + let member_spec = spec + .members() + .unwrap() + .get(&"caller".to_string().into()) + .unwrap() + .clone(); + + let room_id: RoomId = "pub-sub-video-call".to_string().into(); + let room_service = room_service(RoomRepository::new(hashmap!( + room_id.clone() => Room::new(&spec, &app_ctx()).unwrap().start(), + ))); + + let member_parent_fid = Fid::::new(room_id); + let member_id: MemberId = "test-member".to_string().into(); + let member_fid: StatefulFid = member_parent_fid + .clone() + .push_member_id(member_id.clone()) + .into(); + + actix::spawn(test_for_create!( + room_service, + CreateMemberInRoom { + id: member_id, + spec: member_spec, + parent_fid: member_parent_fid, + }, + member_fid, + |member_el| { + assert_eq!(member_el.get_member().get_pipeline().len(), 1); + } + )); + + sys.run().unwrap(); + } + + #[test] + fn create_endpoint() { + let sys = actix::System::new("room-service-tests"); + + let spec = room_spec(); + + let mut endpoint_spec = spec + .members() + .unwrap() + .get(&"caller".to_string().into()) + .unwrap() + .get_publish_endpoint_by_id("publish".to_string().into()) + .unwrap() + .clone(); + endpoint_spec.p2p = P2pMode::Never; + let endpoint_spec = endpoint_spec.into(); + + let room_id: RoomId = "pub-sub-video-call".to_string().into(); + let room_service = room_service(RoomRepository::new(hashmap!( + room_id.clone() => Room::new(&spec, &app_ctx()).unwrap().start(), + ))); + + let endpoint_parent_fid = + Fid::::new(room_id, "caller".to_string().into()); + let endpoint_id: EndpointId = "test-publish".to_string().into(); + let endpoint_fid: StatefulFid = endpoint_parent_fid + .clone() + .push_endpoint_id(endpoint_id.clone()) + .into(); + + actix::spawn(test_for_create!( + room_service, + CreateEndpointInRoom { + id: endpoint_id, + spec: endpoint_spec, + parent_fid: endpoint_parent_fid, + }, + endpoint_fid, + |endpoint_el| { + assert_eq!( + endpoint_el.get_webrtc_pub().get_p2p(), + P2pMode::Never.into() + ); + } + )); + + sys.run().unwrap(); + } + + /// Returns [`Future`] used for testing of all delete/get methods of + /// [`RoomService`]. + /// + /// This test is simply try to delete element with provided + /// [`StatefulFid`] and the try to get it. If result of getting + /// deleted element is error then test considers successful. + /// + /// This function automatically stops [`actix::System`] when test completed. + fn test_for_delete_and_get( + room_service: Addr, + element_fid: StatefulFid, + ) -> impl Future { + let mut delete_msg = DeleteElements::new(); + delete_msg.add_fid(element_fid.clone()); + let delete_msg = delete_msg.validate().unwrap(); + + room_service + .send(delete_msg) + .and_then(move |res| { + res.unwrap(); + room_service.send(Get(vec![element_fid])) + }) + .map(move |res| { + assert!(res.is_err()); + actix::System::current().stop(); + }) + .map_err(|e| panic!("{:?}", e)) + } + + #[test] + fn delete_and_get_room() { + let sys = actix::System::new("room-service-tests"); + + let room_id: RoomId = "pub-sub-video-call".to_string().into(); + let room_fid = StatefulFid::from(Fid::::new(room_id.clone())); + + let room_service = room_service(RoomRepository::new(hashmap!( + room_id => Room::new(&room_spec(), &app_ctx()).unwrap().start(), + ))); + + actix::spawn(test_for_delete_and_get(room_service, room_fid)); + + sys.run().unwrap(); + } + + #[test] + fn delete_and_get_member() { + let sys = actix::System::new("room-service-tests"); + + let room_id: RoomId = "pub-sub-video-call".to_string().into(); + let member_fid = StatefulFid::from(Fid::::new( + room_id.clone(), + "caller".to_string().into(), + )); + + let room_service = room_service(RoomRepository::new(hashmap!( + room_id => Room::new(&room_spec(), &app_ctx()).unwrap().start(), + ))); + + actix::spawn(test_for_delete_and_get(room_service, member_fid)); + + sys.run().unwrap(); + } + + #[test] + fn delete_and_get_endpoint() { + let sys = actix::System::new("room-service-tests"); + + let room_id: RoomId = "pub-sub-video-call".to_string().into(); + let endpoint_fid = StatefulFid::from(Fid::::new( + room_id.clone(), + "caller".to_string().into(), + "publish".to_string().into(), + )); + + let room_service = room_service(RoomRepository::new(hashmap!( + room_id => Room::new(&room_spec(), &app_ctx()).unwrap().start(), + ))); + + actix::spawn(test_for_delete_and_get(room_service, endpoint_fid)); + + sys.run().unwrap(); + } +} diff --git a/tests/e2e/grpc_control_api/create.rs b/tests/e2e/grpc_control_api/create.rs new file mode 100644 index 000000000..c6d44fe04 --- /dev/null +++ b/tests/e2e/grpc_control_api/create.rs @@ -0,0 +1,358 @@ +//! Tests for `Create` method of gRPC [Control API]. +//! +//! The specificity of these tests is such that the `Get` method is also +//! being tested at the same time. +//! +//! [Control API]: https://tinyurl.com/yxsqplq7 + +use medea::api::control::error_codes::ErrorCode; +use medea_control_api_proto::grpc::api::WebRtcPublishEndpoint_P2P; + +use crate::gen_insert_str_macro; + +use super::{ + create_room_req, ControlClient, MemberBuilder, RoomBuilder, + WebRtcPlayEndpointBuilder, WebRtcPublishEndpointBuilder, +}; + +mod room { + use super::*; + + #[test] + fn room() { + gen_insert_str_macro!("create-room"); + + let client = ControlClient::new(); + let sids = client.create(&create_room_req(&insert_str!("{}"))); + assert_eq!(sids.len(), 2); + sids.get(&"publisher".to_string()).unwrap(); + let responder_sid = + sids.get(&"responder".to_string()).unwrap().as_str(); + assert_eq!( + responder_sid, + &insert_str!("ws://127.0.0.1:8080/ws/{}/responder/test") + ); + + let mut get_resp = client.get(&insert_str!("{}")); + let room = get_resp.take_room(); + + let responder = + room.get_pipeline().get("responder").unwrap().get_member(); + assert_eq!(responder.get_credentials(), "test"); + let responder_pipeline = responder.get_pipeline(); + assert_eq!(responder_pipeline.len(), 1); + let responder_play = + responder_pipeline.get("play").unwrap().get_webrtc_play(); + assert_eq!( + responder_play.get_src(), + insert_str!("local://{}/publisher/publish") + ); + + let publisher = + room.get_pipeline().get("publisher").unwrap().get_member(); + assert_ne!(publisher.get_credentials(), "test"); + assert_ne!(publisher.get_credentials(), ""); + let publisher_pipeline = responder.get_pipeline(); + assert_eq!(publisher_pipeline.len(), 1); + } + + #[test] + fn cant_create_rooms_with_duplicate_ids() { + gen_insert_str_macro!("cant_create_rooms_with_duplicate_ids"); + + let client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .build() + .unwrap() + .build_request(""); + + client.create(&create_room); + + if let Err(err) = client.try_create(&create_room) { + assert_eq!(err.code, ErrorCode::RoomAlreadyExists as u32) + } else { + panic!("should err") + } + } + + #[test] + fn element_id_mismatch() { + gen_insert_str_macro!("element_id_mismatch"); + + let client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .build() + .unwrap() + .build_request(insert_str!("{}")); + + if let Err(err) = client.try_create(&create_room) { + assert_eq!(err.code, ErrorCode::ElementIdMismatch as u32) + } else { + panic!("should err") + } + } +} + +mod member { + + use super::*; + + #[test] + fn member() { + gen_insert_str_macro!("create-member"); + + let client = ControlClient::new(); + client.create(&create_room_req(&insert_str!("{}"))); + + let add_member = MemberBuilder::default() + .id("test-member") + .credentials("qwerty") + .add_endpoint( + WebRtcPlayEndpointBuilder::default() + .id("play") + .src(insert_str!("local://{}/publisher/publish")) + .build() + .unwrap(), + ) + .build() + .unwrap() + .build_request(insert_str!("{}")); + + let sids = client.create(&add_member); + let e2e_test_member_sid = + sids.get(&"test-member".to_string()).unwrap().as_str(); + assert_eq!( + e2e_test_member_sid, + insert_str!("ws://127.0.0.1:8080/ws/{}/test-member/qwerty") + ); + + let member = client.get(&insert_str!("{}/test-member")).take_member(); + assert_eq!(member.get_pipeline().len(), 1); + assert_eq!(member.get_credentials(), "qwerty"); + } + + #[test] + fn cant_create_member_in_non_existent_room() { + gen_insert_str_macro!("cant_create_member_in_non_existent_room"); + + let client = ControlClient::new(); + + let create_member = MemberBuilder::default() + .id("caller") + .build() + .unwrap() + .build_request(insert_str!("{}")); + + if let Err(err) = client.try_create(&create_member) { + assert_eq!(err.code, ErrorCode::RoomNotFound as u32) + } else { + panic!("should err") + } + } + + #[test] + fn cant_create_members_with_duplicate_ids() { + gen_insert_str_macro!("cant_create_members_with_duplicate_ids"); + + let client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .build() + .unwrap() + .build_request(""); + + client.create(&create_room); + + let create_member = MemberBuilder::default() + .id("caller") + .build() + .unwrap() + .build_request(insert_str!("{}")); + + client.create(&create_member); + + if let Err(err) = client.try_create(&create_member) { + assert_eq!(err.code, ErrorCode::MemberAlreadyExists as u32) + } else { + panic!("should err") + } + } + + #[test] + fn element_id_mismatch() { + let client = ControlClient::new(); + + let create_member = MemberBuilder::default() + .id("asd") + .build() + .unwrap() + .build_request("qwe/qwe"); + + if let Err(err) = client.try_create(&create_member) { + assert_eq!(err.code, ErrorCode::ElementIdMismatch as u32) + } else { + panic!("should err") + } + } +} + +mod endpoint { + + use super::*; + + #[test] + fn endpoint() { + gen_insert_str_macro!("create-endpoint"); + + let client = ControlClient::new(); + client.create(&create_room_req(&insert_str!("{}"))); + + let create_req = WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::NEVER) + .build() + .unwrap() + .build_request(insert_str!("{}/responder")); + + let sids = client.create(&create_req); + assert_eq!(sids.len(), 0); + + let endpoint = client + .get(&insert_str!("{}/responder/publish")) + .take_webrtc_pub(); + assert_eq!(endpoint.get_p2p(), WebRtcPublishEndpoint_P2P::NEVER); + } + + #[test] + fn cant_create_endpoint_in_non_existent_member() { + gen_insert_str_macro!("cant_create_endpoint_in_non_existent_member"); + + let client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .build() + .unwrap() + .build_request(""); + + client.create(&create_room); + + let create_play = WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap() + .build_request(insert_str!("{}/member")); + + if let Err(err) = client.try_create(&create_play) { + assert_eq!(err.code, ErrorCode::MemberNotFound as u32) + } else { + panic!("should err") + } + } + + #[test] + fn cant_create_endpoint_in_non_existent_room() { + gen_insert_str_macro!("cant_create_endpoint_in_non_existent_room"); + + let client = ControlClient::new(); + + let create_publish = WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap() + .build_request(insert_str!("{}/member")); + + if let Err(err) = client.try_create(&create_publish) { + assert_eq!(err.code, ErrorCode::RoomNotFound as u32) + } else { + panic!("should err") + } + } + + #[test] + fn cant_create_endpoints_with_duplicate_ids() { + gen_insert_str_macro!("cant_create_endpoints_with_duplicate_ids"); + + let client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .add_member(MemberBuilder::default().id("member").build().unwrap()) + .build() + .unwrap() + .build_request(""); + + client.create(&create_room); + + let create_endpoint = WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap() + .build_request(insert_str!("{}/member")); + + client.create(&create_endpoint); + + if let Err(err) = client.try_create(&create_endpoint) { + assert_eq!(err.code, ErrorCode::EndpointAlreadyExists as u32) + } else { + panic!("should err") + } + } + + #[test] + fn cant_create_play_endpoint_when_no_pusblish_endpoints() { + gen_insert_str_macro!( + "cant_create_play_endpoint_when_no_pusblish_endpoints" + ); + + let client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .add_member(MemberBuilder::default().id("member").build().unwrap()) + .build() + .unwrap() + .build_request(""); + + client.create(&create_room); + + let create_endpoint = WebRtcPlayEndpointBuilder::default() + .id("play") + .src(insert_str!("local://{}/member/publish")) + .build() + .unwrap() + .build_request(insert_str!("{}/member")); + + if let Err(err) = client.try_create(&create_endpoint) { + assert_eq!(err.code, ErrorCode::EndpointNotFound as u32) + } else { + panic!("should err") + } + } + + #[test] + fn element_id_mismatch() { + let client = ControlClient::new(); + + let create_endpoint = WebRtcPublishEndpointBuilder::default() + .id("asd") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap() + .build_request("qwe"); + + if let Err(err) = client.try_create(&create_endpoint) { + assert_eq!(err.code, ErrorCode::ElementIdMismatch as u32) + } else { + panic!("should err") + } + } +} diff --git a/tests/e2e/grpc_control_api/delete.rs b/tests/e2e/grpc_control_api/delete.rs new file mode 100644 index 000000000..c6479d67c --- /dev/null +++ b/tests/e2e/grpc_control_api/delete.rs @@ -0,0 +1,169 @@ +//! Tests for `Delete` method of gRPC [Control API]. +//! +//! The specificity of these tests is such that the `Get` method is also +//! being tested at the same time. +//! +//! [Control API]: https://tinyurl.com/yxsqplq7 + +use medea::api::control::error_codes::{ + ErrorCode as MedeaErrorCode, ErrorCode, +}; + +use crate::gen_insert_str_macro; + +use super::{create_room_req, ControlClient}; + +/// Tests `Delete` method of [Medea]'s [Control API]. +/// +/// # Arguments +/// +/// `room_id`: `Room` ID which will be created and from will be deleted +/// `Element`s, +/// +/// `element_id`: `Element` ID which will be deleted from this `Room`, +/// +/// `error_code`: [`ErrorCode`] which should be returned from [`ControlAPI`] +/// when we tries get deleted `Element`. +/// +/// [Medea]: https://github.com/instrumentisto/medea +/// [Control API]: https://tinyurl.com/yxsqplq7 +/// [`ErrorCode`]: medea::api::control::error_codes::ErrorCode +fn test_for_delete( + room_id: &str, + element_id: &str, + error_code: MedeaErrorCode, +) { + let client = ControlClient::new(); + client.create(&create_room_req(room_id)); + + client.try_get(element_id).unwrap(); + + client.delete(&[element_id]).unwrap(); + + let get_room_err = match client.try_get(element_id) { + Ok(_) => panic!("{} not deleted!", element_id), + Err(e) => e, + }; + assert_eq!(get_room_err.code, error_code as u32); +} + +#[test] +fn room() { + gen_insert_str_macro!("delete-room"); + test_for_delete( + &insert_str!("{}"), + &insert_str!("{}"), + ErrorCode::RoomNotFound, + ); +} + +#[test] +fn member() { + gen_insert_str_macro!("delete-member"); + test_for_delete( + &insert_str!("{}"), + &insert_str!("{}/publisher"), + ErrorCode::MemberNotFound, + ); +} + +#[test] +fn endpoint() { + gen_insert_str_macro!("delete-endpoint"); + test_for_delete( + &insert_str!("{}"), + &insert_str!("{}/publisher/publish"), + ErrorCode::EndpointNotFound, + ); +} + +/// Tests that `Delete` method on parent element also deletes all nested +/// elements. +/// +/// # Arguments +/// +/// `room_id`: `Room` ID which will be created and from will be deleted +/// `Element`s, +/// +/// `elements_uris`: `Element`s IDs which will be deleted from this `Element`, +/// +/// `error_code`: [`ErrorCode`] which should be returned from [`ControlAPI`] +/// when we tries get deleted `Element`, +/// +/// `root_elem_uri`: URI to parent `Element`. +/// +/// [Medea]: https://github.com/instrumentisto/medea +/// [Control API]: https://tinyurl.com/yxsqplq7 +/// [`ErrorCode`]: medea::api::control::error_codes::ErrorCode +fn test_cascade_delete( + room_id: &str, + elements_uris: &[&str], + code: MedeaErrorCode, + root_elem_uri: &str, +) { + let client = ControlClient::new(); + client.create(&create_room_req(room_id)); + client.delete(elements_uris).unwrap(); + + match client.try_get(root_elem_uri) { + Ok(_) => panic!("Member not deleted!"), + Err(e) => { + assert_eq!(e.code, code as u32); + } + } +} + +#[test] +fn cascade_delete_endpoints_when_deleting_member() { + gen_insert_str_macro!("member-and-endpoint-same-time"); + + test_cascade_delete( + &insert_str!("{}"), + &[ + &insert_str!("{}/publisher"), + &insert_str!("{}/publisher/publish"), + ], + MedeaErrorCode::MemberNotFound, + &insert_str!("{}/publisher"), + ); +} + +#[test] +fn cascade_delete_everything_when_deleting_room() { + gen_insert_str_macro!("room-and-inner-elements-same-time"); + + test_cascade_delete( + &insert_str!("{}"), + &[ + &insert_str!("{}"), + &insert_str!("{}/publisher"), + &insert_str!("{}/publisher/publish"), + ], + MedeaErrorCode::RoomNotFound, + &insert_str!("{}"), + ); +} + +#[test] +fn cant_delete_members_from_different_rooms_in_single_request() { + let client = ControlClient::new(); + + if let Err(err) = client.delete(&["room1/member1", "room2/member1"]) { + assert_eq!(err.code, MedeaErrorCode::ProvidedNotSameRoomIds as u32); + } else { + panic!("should err") + } +} + +#[test] +fn cant_delete_endpoints_from_different_rooms_in_single_request() { + let client = ControlClient::new(); + + if let Err(err) = + client.delete(&["room1/member1/endpoint1", "room2/member1/endpoint1"]) + { + assert_eq!(err.code, MedeaErrorCode::ProvidedNotSameRoomIds as u32); + } else { + panic!("should err") + } +} diff --git a/tests/e2e/grpc_control_api/mod.rs b/tests/e2e/grpc_control_api/mod.rs new file mode 100644 index 000000000..01accab98 --- /dev/null +++ b/tests/e2e/grpc_control_api/mod.rs @@ -0,0 +1,397 @@ +//! Tests for gRPC [Medea]'s [Control API]. +//! +//! [Medea]: https://github.com/instrumentisto/medea +//! [Control API]: https://tinyurl.com/yxsqplq7 + +mod create; +mod delete; +mod signaling; + +use std::{collections::HashMap, sync::Arc}; + +use derive_builder::*; +use grpcio::{ChannelBuilder, EnvBuilder}; +use medea_control_api_proto::grpc::{ + api::{ + CreateRequest, Element, Error, IdRequest, Member as GrpcMember, + Member_Element, Room as GrpcRoom, Room_Element, + WebRtcPlayEndpoint as GrpcWebRtcPlayEndpoint, + WebRtcPublishEndpoint as GrpcWebRtcPublishEndpoint, + WebRtcPublishEndpoint_P2P, + }, + api_grpc::ControlApiClient, +}; +use protobuf::RepeatedField; + +/// Client for [Medea]'s gRPC [Control API]. +/// +/// [Medea]: https://github.com/instrumentisto/medea +#[derive(Clone)] +struct ControlClient(ControlApiClient); + +impl ControlClient { + /// Create new [`ControlClient`]. + /// + /// Client will connect to `localhost:6565`. + /// + /// Note that this function don't connects to the server. This mean that + /// when you call [`ControlClient::new`] and server not working you will + /// don't know it until try to send something with this client. + pub fn new() -> Self { + let env = Arc::new(EnvBuilder::new().build()); + let ch = ChannelBuilder::new(env).connect("127.0.0.1:6565"); + Self(ControlApiClient::new(ch)) + } + + /// Gets some [`Element`] by local URI. + /// + /// # Panics + /// + /// - if [`GetResponse`] has error + /// - if connection with server failed + pub fn get(&self, uri: &str) -> Element { + let mut get_room_request = IdRequest::new(); + let mut room = RepeatedField::new(); + room.push(uri.to_string()); + get_room_request.set_fid(room); + + let mut resp = self.0.get(&get_room_request).unwrap(); + if resp.has_error() { + panic!("{:?}", resp.get_error()); + } + resp.take_elements().remove(&uri.to_string()).unwrap() + } + + /// Tries to get some [`Element`] by local URI. + /// + /// # Panics + /// + /// - if connection with server failed. + pub fn try_get(&self, uri: &str) -> Result { + let mut get_room_request = IdRequest::new(); + let mut room = RepeatedField::new(); + room.push(uri.to_string()); + get_room_request.set_fid(room); + + let mut resp = self.0.get(&get_room_request).unwrap(); + if resp.has_error() { + return Err(resp.take_error()); + } + Ok(resp.take_elements().remove(&uri.to_string()).unwrap()) + } + + /// Creates `Element` and returns it sids. + /// + /// # Panics + /// + /// - if [`CreateResponse`] has error. + /// - if connection with server failed. + pub fn create(&self, req: &CreateRequest) -> HashMap { + let resp = self.0.create(&req).unwrap(); + if resp.has_error() { + panic!("{:?}", resp.get_error()); + } + + resp.sid + } + + /// Tries to create `Element` and returns it sids. + /// + /// # Panics + /// + /// - if connection with server failed. + pub fn try_create( + &self, + req: &CreateRequest, + ) -> Result, Error> { + let mut resp = self.0.create(&req).unwrap(); + + if resp.has_error() { + Err(resp.take_error()) + } else { + Ok(resp.sid) + } + } + + /// Deletes `Element`s by local URIs. + /// + /// # Panics + /// + /// - if [`Response`] has error + /// - if connection with server failed. + pub fn delete(&self, ids: &[&str]) -> Result<(), Error> { + let mut delete_req = IdRequest::new(); + let mut delete_ids = RepeatedField::new(); + ids.iter().for_each(|id| delete_ids.push(id.to_string())); + delete_req.set_fid(delete_ids); + + let mut resp = self.0.delete(&delete_req).unwrap(); + if resp.has_error() { + Err(resp.take_error()) + } else { + Ok(()) + } + } +} + +#[derive(Builder)] +#[builder(setter(into))] +pub struct Room { + id: String, + + #[builder(default = "HashMap::new()")] + members: HashMap, +} + +impl Room { + pub fn build_request>(self, uri: T) -> CreateRequest { + let mut request = CreateRequest::default(); + + let mut grpc_room = GrpcRoom::new(); + let mut members = HashMap::new(); + + for (id, member) in self.members { + let mut room_element = Room_Element::new(); + room_element.set_member(member.into()); + + members.insert(id, room_element); + } + + grpc_room.set_id(self.id); + grpc_room.set_pipeline(members); + + request.set_parent_fid(uri.into()); + request.set_room(grpc_room); + + request + } +} + +impl RoomBuilder { + fn add_member>(&mut self, member: T) -> &mut Self { + let member = member.into(); + + self.members + .get_or_insert(HashMap::new()) + .insert(member.id.clone(), member); + + self + } +} + +#[derive(Builder, Clone)] +#[builder(setter(into))] +pub struct Member { + id: String, + #[builder(default = "None")] + #[builder(setter(strip_option))] + credentials: Option, + #[builder(default = "HashMap::new()")] + endpoints: HashMap, +} + +impl Into for Member { + fn into(self) -> GrpcMember { + let mut grpc_member = GrpcMember::new(); + + let mut pipeline = HashMap::new(); + + for (id, element) in self.endpoints { + pipeline.insert(id, element.into()); + } + + if let Some(credentials) = self.credentials { + grpc_member.set_credentials(credentials) + } + + grpc_member.set_pipeline(pipeline); + grpc_member.set_id(self.id); + + grpc_member + } +} + +impl Member { + fn build_request>(self, url: T) -> CreateRequest { + let mut request = CreateRequest::default(); + + request.set_parent_fid(url.into()); + request.set_member(self.into()); + + request + } +} + +impl MemberBuilder { + fn add_endpoint>(&mut self, element: T) -> &mut Self { + let element = element.into(); + + self.endpoints + .get_or_insert(HashMap::new()) + .insert(element.id(), element); + self + } +} + +#[derive(Clone)] +pub enum Endpoint { + WebRtcPlayElement(WebRtcPlayEndpoint), + WebRtcPublishElement(WebRtcPublishEndpoint), +} + +impl Endpoint { + fn id(&self) -> String { + match self { + Self::WebRtcPlayElement(endpoint) => endpoint.id.clone(), + Self::WebRtcPublishElement(endpoint) => endpoint.id.clone(), + } + } +} + +impl Into for Endpoint { + fn into(self) -> Member_Element { + let mut member_elem = Member_Element::new(); + + match self { + Self::WebRtcPlayElement(element) => { + member_elem.set_webrtc_play(element.into()); + } + Self::WebRtcPublishElement(element) => { + member_elem.set_webrtc_pub(element.into()) + } + } + + member_elem + } +} + +#[derive(Builder, Clone)] +#[builder(setter(into))] +pub struct WebRtcPlayEndpoint { + id: String, + src: String, +} + +impl WebRtcPlayEndpoint { + fn build_request>(self, url: T) -> CreateRequest { + let mut request = CreateRequest::default(); + + request.set_parent_fid(url.into()); + request.set_webrtc_play(self.into()); + + request + } +} + +impl Into for WebRtcPlayEndpoint { + fn into(self) -> GrpcWebRtcPlayEndpoint { + let mut endpoint = GrpcWebRtcPlayEndpoint::new(); + endpoint.set_src(self.src); + endpoint.set_id(self.id); + + endpoint + } +} + +impl Into for WebRtcPlayEndpoint { + fn into(self) -> Endpoint { + Endpoint::WebRtcPlayElement(self) + } +} + +#[derive(Builder, Clone)] +#[builder(setter(into))] +pub struct WebRtcPublishEndpoint { + id: String, + p2p_mode: WebRtcPublishEndpoint_P2P, +} + +impl WebRtcPublishEndpoint { + fn build_request>(self, url: T) -> CreateRequest { + let mut request = CreateRequest::default(); + + request.set_parent_fid(url.into()); + request.set_webrtc_pub(self.into()); + + request + } +} + +impl Into for WebRtcPublishEndpoint { + fn into(self) -> GrpcWebRtcPublishEndpoint { + let mut endpoint = GrpcWebRtcPublishEndpoint::new(); + endpoint.set_p2p(self.p2p_mode); + endpoint.set_id(self.id); + + endpoint + } +} + +impl Into for WebRtcPublishEndpoint { + fn into(self) -> Endpoint { + Endpoint::WebRtcPublishElement(self) + } +} + +/// Creates [`CreateRequest`] for creating `Room` element with provided `Room` +/// ID. +/// +/// # Spec of `Room` which will be created with this [`CreateRequest`] +/// +/// ```yaml +/// kind: Room +/// id: {{ room_id }} +/// spec: +/// pipeline: +/// publisher: +/// kind: Member +/// spec: +/// pipeline: +/// publish: +/// kind: WebRtcPublishEndpoint +/// spec: +/// p2p: Always +/// responder: +/// kind: Member +/// credentials: test +/// spec: +/// pipeline: +/// play: +/// kind: WebRtcPlayEndpoint +/// spec: +/// src: "local://{{ room_id }}/publisher/publish" +/// ``` +fn create_room_req(room_id: &str) -> CreateRequest { + RoomBuilder::default() + .id(room_id.to_string()) + .add_member( + MemberBuilder::default() + .id("publisher") + .add_endpoint( + WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap(), + ) + .build() + .unwrap(), + ) + .add_member( + MemberBuilder::default() + .id("responder") + .credentials("test") + .add_endpoint( + WebRtcPlayEndpointBuilder::default() + .id("play") + .src(format!("local://{}/publisher/publish", room_id)) + .build() + .unwrap(), + ) + .build() + .unwrap(), + ) + .build() + .unwrap() + .build_request(String::new()) +} diff --git a/tests/e2e/grpc_control_api/signaling.rs b/tests/e2e/grpc_control_api/signaling.rs new file mode 100644 index 000000000..11c7e0131 --- /dev/null +++ b/tests/e2e/grpc_control_api/signaling.rs @@ -0,0 +1,303 @@ +//! Tests for signaling which should be happen after gRPC [Control API] call. +//! +//! [Control API]: https://tinyurl.com/yxsqplq7 + +use std::{cell::Cell, rc::Rc, time::Duration}; + +use actix::{Arbiter, AsyncContext, Context, System}; +use futures::future::Future as _; +use medea_client_api_proto::Event; +use medea_control_api_proto::grpc::api::WebRtcPublishEndpoint_P2P; + +use crate::{ + gen_insert_str_macro, grpc_control_api::ControlClient, + signalling::TestMember, +}; + +use super::{ + MemberBuilder, RoomBuilder, WebRtcPlayEndpointBuilder, + WebRtcPublishEndpointBuilder, +}; + +fn stop_on_peer_created( +) -> impl Fn(&Event, &mut Context, Vec<&Event>) + Clone { + let peers_created = Rc::new(Cell::new(0)); + move |event: &Event, ctx: &mut Context, _: Vec<&Event>| { + if let Event::PeerCreated { .. } = event { + peers_created.set(peers_created.get() + 1); + if peers_created.get() == 2 { + ctx.run_later(Duration::from_secs(1), |_, _| { + actix::System::current().stop(); + }); + } + } + } +} + +#[test] +fn signalling_starts_when_create_play_member_after_pub_member() { + gen_insert_str_macro!("create-play-member-after-pub-member"); + let sys = System::new(insert_str!("{}")); + + let control_client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .add_member( + MemberBuilder::default() + .id("publisher") + .credentials("test") + .add_endpoint( + WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap(), + ) + .build() + .unwrap(), + ) + .build() + .unwrap() + .build_request(""); + + control_client.create(&create_room); + + let on_event = stop_on_peer_created(); + + let deadline = Some(Duration::from_secs(5)); + Arbiter::spawn( + TestMember::connect( + &insert_str!("ws://127.0.0.1:8080/ws/{}/publisher/test"), + Box::new(on_event.clone()), + deadline, + ) + .and_then(move |_| { + let create_play_member = MemberBuilder::default() + .id("responder") + .credentials("qwerty") + .add_endpoint( + WebRtcPlayEndpointBuilder::default() + .id("play") + .src(insert_str!("local://{}/publisher/publish")) + .build() + .unwrap(), + ) + .build() + .unwrap() + .build_request(insert_str!("{}")); + + control_client.create(&create_play_member); + TestMember::connect( + &insert_str!("ws://127.0.0.1:8080/ws/{}/responder/qwerty"), + Box::new(on_event), + deadline, + ) + }) + .map(|_| ()), + ); + + sys.run().unwrap(); +} + +#[test] +fn signalling_starts_when_create_play_endpoint_after_pub_member() { + gen_insert_str_macro!( + "signalling_starts_when_create_play_endpoint_after_pub_member" + ); + let sys = System::new(insert_str!("{}")); + + let control_client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .add_member( + MemberBuilder::default() + .id("publisher") + .credentials("test") + .add_endpoint( + WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap(), + ) + .build() + .unwrap(), + ) + .build() + .unwrap() + .build_request(""); + + control_client.create(&create_room); + + let on_event = stop_on_peer_created(); + + let deadline = Some(Duration::from_secs(5)); + Arbiter::spawn( + TestMember::connect( + &insert_str!("ws://127.0.0.1:8080/ws/{}/publisher/test"), + Box::new(on_event.clone()), + deadline, + ) + .and_then(move |_| { + let create_second_member = MemberBuilder::default() + .id("responder") + .credentials("qwerty") + .build() + .unwrap() + .build_request(insert_str!("{}")); + control_client.create(&create_second_member); + + let create_play = WebRtcPlayEndpointBuilder::default() + .id("play") + .src(insert_str!("local://{}/publisher/publish")) + .build() + .unwrap() + .build_request(insert_str!("{}/responder")); + + control_client.create(&create_play); + + TestMember::connect( + &insert_str!("ws://127.0.0.1:8080/ws/{}/responder/qwerty"), + Box::new(on_event), + deadline, + ) + }) + .map(|_| ()), + ); + + sys.run().unwrap(); +} + +#[test] +fn signalling_starts_in_loopback_scenario() { + gen_insert_str_macro!("signalling_starts_in_loopback_scenario"); + let sys = System::new(insert_str!("{}")); + + let control_client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .add_member( + MemberBuilder::default() + .id("publisher") + .credentials("test") + .add_endpoint( + WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap(), + ) + .build() + .unwrap(), + ) + .build() + .unwrap() + .build_request(""); + + control_client.create(&create_room); + + let on_event = stop_on_peer_created(); + + let deadline = Some(Duration::from_secs(5)); + Arbiter::spawn( + TestMember::connect( + &insert_str!("ws://127.0.0.1:8080/ws/{}/publisher/test"), + Box::new(on_event.clone()), + deadline, + ) + .and_then(move |_| { + let create_play = WebRtcPlayEndpointBuilder::default() + .id("play") + .src(insert_str!("local://{}/publisher/publish")) + .build() + .unwrap() + .build_request(insert_str!("{}/publisher")); + + control_client.create(&create_play); + Ok(()) + }) + .map(|_| ()), + ); + + sys.run().unwrap(); +} + +#[test] +fn peers_removed_on_delete_member() { + gen_insert_str_macro!("delete-member-check-peers-removed"); + let sys = System::new(&insert_str!("{}")); + + let control_client = ControlClient::new(); + + let create_room = RoomBuilder::default() + .id(insert_str!("{}")) + .add_member( + MemberBuilder::default() + .id("publisher") + .credentials("test") + .add_endpoint( + WebRtcPublishEndpointBuilder::default() + .id("publish") + .p2p_mode(WebRtcPublishEndpoint_P2P::ALWAYS) + .build() + .unwrap(), + ) + .build() + .unwrap(), + ) + .add_member( + MemberBuilder::default() + .id("responder") + .credentials("test") + .add_endpoint( + WebRtcPlayEndpointBuilder::default() + .id("play") + .src(insert_str!("local://{}/publisher/publish")) + .build() + .unwrap(), + ) + .build() + .unwrap(), + ) + .build() + .unwrap() + .build_request(""); + + control_client.create(&create_room); + + let peers_created = Rc::new(Cell::new(0)); + let on_event = + move |event: &Event, _: &mut Context, _: Vec<&Event>| { + match event { + Event::PeerCreated { .. } => { + peers_created.set(peers_created.get() + 1); + if peers_created.get() == 2 { + control_client + .delete(&[&insert_str!("{}/responder")]) + .unwrap(); + } + } + Event::PeersRemoved { .. } => { + actix::System::current().stop(); + } + _ => {} + } + }; + + let deadline = Some(Duration::from_secs(5)); + TestMember::start( + &insert_str!("ws://127.0.0.1:8080/ws/{}/publisher/test"), + Box::new(on_event.clone()), + deadline, + ); + TestMember::start( + &insert_str!("ws://127.0.0.1:8080/ws/{}/responder/test"), + Box::new(on_event), + deadline, + ); + + sys.run().unwrap(); +} diff --git a/tests/e2e/main.rs b/tests/e2e/main.rs index 406a0fb40..a5383d8ec 100644 --- a/tests/e2e/main.rs +++ b/tests/e2e/main.rs @@ -1 +1,36 @@ -mod signalling; +mod grpc_control_api; +pub mod signalling; + +/// Generates `insert_str!` macro, that can be used as `format!` macro with one +/// predefined argument. +/// +/// # Example usage +/// +/// ``` +/// fn first_test() { +/// gen_insert_str_macro!("first-test"); +/// +/// let addr = insert_str!("ws://127.0.0.1:8080/{}/publisher/test"); +/// assert_eq!(addr, "ws://127.0.0.1:8080/first-test/publisher/test"); +/// } +/// +/// fn second_test() { +/// gen_insert_str_macro!("second-test"); +/// +/// let addr = insert_str!("local://{}/publisher"); +/// assert_eq!(addr, "local://second-test/publisher"); +/// } +/// +/// # first_test(); +/// # second_test(); +/// ``` +#[macro_export] +macro_rules! gen_insert_str_macro { + ($name:expr) => { + macro_rules! insert_str { + ($fmt:expr) => { + format!($fmt, $name) + }; + } + }; +} diff --git a/tests/e2e/signalling/mod.rs b/tests/e2e/signalling/mod.rs index 91f9e50da..3e423b810 100644 --- a/tests/e2e/signalling/mod.rs +++ b/tests/e2e/signalling/mod.rs @@ -5,7 +5,9 @@ mod three_pubs; use std::time::Duration; -use actix::{Actor, Arbiter, AsyncContext, Context, Handler, StreamHandler}; +use actix::{ + Actor, Addr, Arbiter, AsyncContext, Context, Handler, StreamHandler, +}; use actix_codec::Framed; use actix_http::ws; use awc::{ @@ -13,7 +15,7 @@ use awc::{ ws::{CloseCode, CloseReason, Frame}, BoxedSocket, }; -use futures::{stream::SplitSink, Future as _, Sink as _, Stream as _}; +use futures::{stream::SplitSink, Future, Future as _, Sink as _, Stream as _}; use medea_client_api_proto::{Command, Event, IceCandidate}; use serde_json::error::Error as SerdeError; @@ -54,10 +56,35 @@ impl TestMember { } /// Sends command to the server. - fn send_command(&mut self, msg: Command) { - let json = serde_json::to_string(&msg).unwrap(); - self.writer.start_send(ws::Message::Text(json)).unwrap(); - self.writer.poll_complete().unwrap(); + fn send_command(&mut self, msg: &Command) { + let json = serde_json::to_string(msg).unwrap(); + self.writer.start_send(ws::Message::Text(json)).ok(); + self.writer.poll_complete().ok(); + } + + /// Returns [`Future`] which will connect to the WebSocket and starts + /// [`TestMember`] actor. + pub fn connect( + uri: &str, + on_message: MessageHandler, + deadline: Option, + ) -> impl Future, Error = ()> { + awc::Client::new() + .ws(uri) + .connect() + .map_err(|e| panic!("Error: {}", e)) + .map(move |(_, framed)| { + let (sink, stream) = framed.split(); + Self::create(move |ctx| { + Self::add_stream(stream, ctx); + Self { + writer: sink, + events: Vec::new(), + deadline, + on_message, + } + }) + }) } /// Starts test member in new [`Arbiter`] by given URI. @@ -68,24 +95,7 @@ impl TestMember { on_message: MessageHandler, deadline: Option, ) { - Arbiter::spawn( - awc::Client::new() - .ws(uri) - .connect() - .map_err(|e| panic!("Error: {}", e)) - .map(move |(_, framed)| { - let (sink, stream) = framed.split(); - TestMember::create(move |ctx| { - TestMember::add_stream(stream, ctx); - TestMember { - writer: sink, - events: Vec::new(), - deadline, - on_message, - } - }); - }), - ) + Arbiter::spawn(Self::connect(uri, on_message, deadline).map(|_| ())) } } @@ -137,10 +147,7 @@ impl StreamHandler for TestMember { let txt = String::from_utf8(txt.unwrap().to_vec()).unwrap(); let event: Result = serde_json::from_str(&txt); if let Ok(event) = event { - let mut events: Vec<&Event> = self.events.iter().collect(); - events.push(&event); // Test function call - (self.on_message)(&event, ctx, events); if let Event::PeerCreated { peer_id, @@ -150,11 +157,11 @@ impl StreamHandler for TestMember { } = &event { match sdp_offer { - Some(_) => self.send_command(Command::MakeSdpAnswer { + Some(_) => self.send_command(&Command::MakeSdpAnswer { peer_id: *peer_id, sdp_answer: "responder_answer".into(), }), - None => self.send_command(Command::MakeSdpOffer { + None => self.send_command(&Command::MakeSdpOffer { peer_id: *peer_id, sdp_offer: "caller_offer".into(), mids: tracks @@ -166,7 +173,7 @@ impl StreamHandler for TestMember { }), } - self.send_command(Command::SetIceCandidate { + self.send_command(&Command::SetIceCandidate { peer_id: *peer_id, candidate: IceCandidate { candidate: "ice_candidate".to_string(), @@ -175,6 +182,9 @@ impl StreamHandler for TestMember { }, }); } + let mut events: Vec<&Event> = self.events.iter().collect(); + events.push(&event); + (self.on_message)(&event, ctx, events); self.events.push(event); } } diff --git a/tests/e2e/signalling/three_pubs.rs b/tests/e2e/signalling/three_pubs.rs index 0f5693e02..21562001f 100644 --- a/tests/e2e/signalling/three_pubs.rs +++ b/tests/e2e/signalling/three_pubs.rs @@ -6,6 +6,7 @@ use medea_client_api_proto::{Direction, Event, PeerId}; use crate::signalling::{CloseSocket, TestMember}; #[test] +#[allow(clippy::too_many_lines)] fn three_members_p2p_video_call() { System::run(|| { let base_url = "ws://127.0.0.1:8080/ws/three-members-conference"; @@ -58,13 +59,11 @@ fn three_members_p2p_video_call() { .iter() .filter_map(|t| match &t.direction { Direction::Recv { sender, .. } => { + assert_ne!(sender, peer_id); Some(sender) } _ => None, }) - .map(|sender| { - assert_ne!(sender, peer_id); - }) .count(); assert_eq!(recv_count, 2); @@ -73,13 +72,15 @@ fn three_members_p2p_video_call() { .filter_map(|t| match &t.direction { Direction::Send { receivers, .. - } => Some(receivers), + } => { + assert!( + !receivers.contains(peer_id) + ); + assert_eq!(receivers.len(), 1); + Some(receivers) + } _ => None, }) - .map(|receivers| { - assert!(!receivers.contains(peer_id)); - assert_eq!(receivers.len(), 1); - }) .count(); assert_eq!(send_count, 2); }