From ced3ef4e2ac1fc93701a4b300c5eafacdb402214 Mon Sep 17 00:00:00 2001 From: evdokimovs <49490279+evdokimovs@users.noreply.github.com> Date: Fri, 18 Dec 2020 17:38:29 +0300 Subject: [PATCH] Free media devices before switch in Jason (#160, #27) - add 'rollback_on_fail' flag to the Room.set_local_media_settings() method - add 'stop_first' flag to the Room.set_local_media_settings() method --- Cargo.lock | 196 ++++----- jason/CHANGELOG.md | 5 +- jason/Cargo.toml | 1 + jason/demo/index.html | 33 +- jason/e2e-demo/js/index.js | 25 +- jason/src/api/mod.rs | 5 +- jason/src/api/room.rs | 340 ++++++++++++++- jason/src/media/constraints.rs | 103 ++++- jason/src/media/manager.rs | 4 +- jason/src/peer/media/mod.rs | 43 ++ jason/src/peer/media/sender.rs | 42 +- jason/src/peer/mod.rs | 32 +- jason/src/peer/transceiver.rs | 7 + jason/tests/api/room.rs | 760 +++++++++++++++++++++++---------- jason/tests/web.rs | 10 + 15 files changed, 1218 insertions(+), 388 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 989ef8c31..7be7c7dcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,7 +121,7 @@ dependencies = [ "pin-project 1.0.2", "rand 0.7.3", "regex", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "serde_urlencoded", "sha-1", @@ -136,7 +136,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655" dependencies = [ "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -149,7 +149,7 @@ dependencies = [ "http", "log", "regex", - "serde 1.0.117", + "serde 1.0.118", ] [[package]] @@ -288,7 +288,7 @@ dependencies = [ "mime", "pin-project 1.0.2", "regex", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "serde_urlencoded", "socket2", @@ -321,7 +321,7 @@ checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -332,7 +332,7 @@ checksum = "b95aceadaf327f18f0df5962fedc1bde2f870566a0b9f65c89508a3b1f79334c" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -370,15 +370,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" +checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4" [[package]] name = "arc-swap" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" +checksum = "dabe5a181f83789739c194cbe5a897dde195078fac08568d09221fd6137a7ba8" [[package]] name = "arrayref" @@ -459,12 +459,24 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-recursion" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5444eec77a9ec2bfe4524139e09195862e981400c4358d3b760cae634e4c4ee" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.54", +] + [[package]] name = "async-std" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e82538bc65a25dbdff70e4c5439d52f068048ab97cdea0acd73f131594caa1" +checksum = "8f9f84f1280a2b436a2c77c2582602732b6c2f4321d5494d6e799e6c367859a8" dependencies = [ + "async-channel", "async-global-executor", "async-io", "async-mutex", @@ -480,7 +492,7 @@ dependencies = [ "memchr", "num_cpus", "once_cell", - "pin-project-lite 0.1.11", + "pin-project-lite 0.2.0", "pin-utils", "slab", "wasm-bindgen-futures", @@ -504,7 +516,7 @@ checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -521,7 +533,7 @@ checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -566,7 +578,7 @@ dependencies = [ "mime", "percent-encoding", "rand 0.7.3", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "serde_urlencoded", ] @@ -742,15 +754,6 @@ dependencies = [ "vec_map", ] -[[package]] -name = "cloudabi" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" -dependencies = [ - "bitflags", -] - [[package]] name = "combine" version = "4.4.0" @@ -782,7 +785,7 @@ dependencies = [ "lazy_static", "nom", "rust-ini", - "serde 1.0.117", + "serde 1.0.118", "serde-hjson", "serde_json", "toml", @@ -801,9 +804,9 @@ dependencies = [ [[package]] name = "const_fn" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" +checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826" [[package]] name = "constant_time_eq" @@ -907,7 +910,7 @@ dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", "strsim 0.9.3", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -918,7 +921,7 @@ checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ "darling_core", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -931,7 +934,7 @@ dependencies = [ "config", "crossbeam-queue", "num_cpus", - "serde 1.0.117", + "serde 1.0.118", "tokio", ] @@ -947,7 +950,7 @@ dependencies = [ "futures 0.3.8", "log", "redis", - "serde 1.0.117", + "serde 1.0.118", ] [[package]] @@ -958,7 +961,7 @@ checksum = "cb582b60359da160a9477ee80f15c8d784c477e69c217ef2cdd4169c24ea380f" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -971,7 +974,7 @@ dependencies = [ "derive_builder_core", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -983,7 +986,7 @@ dependencies = [ "darling", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -994,7 +997,7 @@ checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -1081,7 +1084,7 @@ dependencies = [ "heck", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -1108,7 +1111,7 @@ checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", "synstructure", ] @@ -1290,7 +1293,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -1441,9 +1444,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26" dependencies = [ "bytes", "fnv", @@ -1485,7 +1488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac34a56cfd4acddb469cc7fff187ed5ac36f498ba085caf8bbc725e3ff474058" dependencies = [ "humantime", - "serde 1.0.117", + "serde 1.0.118", ] [[package]] @@ -1531,9 +1534,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" dependencies = [ "autocfg", "hashbrown", @@ -1639,9 +1642,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" [[package]] name = "linked-hash-map" @@ -1737,7 +1740,7 @@ dependencies = [ "rand 0.7.3", "redis", "rust-crypto", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "serde_yaml", "serial_test", @@ -1762,7 +1765,7 @@ dependencies = [ "async-trait", "derive_more", "medea-macro", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "serde_with", ] @@ -1781,7 +1784,7 @@ dependencies = [ "humantime-serde", "medea-control-api-proto", "protobuf", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "slog", "slog-async", @@ -1822,6 +1825,7 @@ dependencies = [ name = "medea-jason" version = "0.2.0-dev" dependencies = [ + "async-recursion", "async-trait", "bitflags", "console_error_panic_hook", @@ -1837,7 +1841,7 @@ dependencies = [ "medea-reactive", "mockall", "predicates-tree", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "tracerr", "url", @@ -1858,7 +1862,7 @@ dependencies = [ "medea-jason", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", "synstructure", ] @@ -1964,7 +1968,7 @@ dependencies = [ "cfg-if 0.1.10", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -1985,9 +1989,9 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cf75f38f16cb05ea017784dc6dbfd354f76c223dba37701734c4f5a9337d02" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" dependencies = [ "cfg-if 0.1.10", "libc", @@ -2086,12 +2090,11 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0" dependencies = [ - "cfg-if 0.1.10", - "cloudabi", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall", @@ -2141,7 +2144,7 @@ checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -2152,7 +2155,7 @@ checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -2298,7 +2301,7 @@ dependencies = [ "itertools", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -2607,9 +2610,9 @@ checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" dependencies = [ "serde_derive", ] @@ -2629,13 +2632,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -2646,7 +2649,7 @@ checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" dependencies = [ "itoa", "ryu", - "serde 1.0.117", + "serde 1.0.118", ] [[package]] @@ -2667,7 +2670,7 @@ dependencies = [ "form_urlencoded", "itoa", "ryu", - "serde 1.0.117", + "serde 1.0.118", ] [[package]] @@ -2676,7 +2679,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15f6201e064705553ece353a736a64be975680bd244908cf63e8fa71e478a51a" dependencies = [ - "serde 1.0.117", + "serde 1.0.118", "serde_with_macros", ] @@ -2689,7 +2692,7 @@ dependencies = [ "darling", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -2700,7 +2703,7 @@ checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7" dependencies = [ "dtoa", "linked-hash-map 0.5.3", - "serde 1.0.117", + "serde 1.0.118", "yaml-rust", ] @@ -2723,7 +2726,7 @@ checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -2800,7 +2803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc0d2aff1f8f325ef660d9a0eb6e6dcd20b30b3f581a5897f58bf42d061c37a" dependencies = [ "chrono", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "slog", ] @@ -2842,9 +2845,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" +checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" [[package]] name = "smart-default" @@ -2854,18 +2857,17 @@ checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] name = "socket2" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +checksum = "97e0e9fd577458a4f61fb91fcb559ea2afecc54c934119421f9f5d3d5b1a1057" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", "winapi 0.3.9", ] @@ -2906,9 +2908,9 @@ checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "serde 1.0.117", + "serde 1.0.118", "serde_derive", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -2920,11 +2922,11 @@ dependencies = [ "base-x", "proc-macro2 1.0.24", "quote 1.0.7", - "serde 1.0.117", + "serde 1.0.118", "serde_derive", "serde_json", "sha1", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -2958,9 +2960,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.53" +version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" +checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", @@ -2975,7 +2977,7 @@ checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", "unicode-xid 0.2.1", ] @@ -3035,7 +3037,7 @@ checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -3102,7 +3104,7 @@ dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", "standback", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -3122,9 +3124,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" +checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48" dependencies = [ "bytes", "fnv", @@ -3151,7 +3153,7 @@ checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -3175,7 +3177,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" dependencies = [ - "serde 1.0.117", + "serde 1.0.118", ] [[package]] @@ -3217,7 +3219,7 @@ dependencies = [ "proc-macro2 1.0.24", "prost-build", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -3428,7 +3430,7 @@ checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", ] [[package]] @@ -3615,7 +3617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" dependencies = [ "cfg-if 1.0.0", - "serde 1.0.117", + "serde 1.0.118", "serde_json", "wasm-bindgen-macro", ] @@ -3631,7 +3633,7 @@ dependencies = [ "log", "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", "wasm-bindgen-shared", ] @@ -3665,7 +3667,7 @@ checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", - "syn 1.0.53", + "syn 1.0.54", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/jason/CHANGELOG.md b/jason/CHANGELOG.md index 54771e3e3..0de4e8d6d 100644 --- a/jason/CHANGELOG.md +++ b/jason/CHANGELOG.md @@ -53,7 +53,9 @@ All user visible changes to this project will be documented in this file. This p - `Jason.init_room()` ([#46]); - `Room.join()` ([#46]); - `Jason.close_room()` ([#147]). - - Ability to configure local media stream used by `Room` via `Room.set_local_media_settings()` ([#54], [#97], [#145]); + - Ability to configure local media stream used by `Room` via `Room.set_local_media_settings()` ([#54], [#97], [#145], [#160]): + - `Room.set_local_media_settings()` can be configured to stop used tracks before trying to acquire new tracks ([#160]); + - `Room.set_local_media_settings()` can be configured to rollback to previous settings if fail to set new settings ([#160]). - `Room.on_failed_local_media` callback ([#54], [#143]); - `Room.on_close` callback for WebSocket close initiated by server ([#55]); - `RemoteMediaTrack.on_enabled` and `RemoteMediaTrack.on_disabled` callbacks being called when `RemoteMediaTrack` is enabled or disabled ([#123], [#143], [#156]); @@ -132,6 +134,7 @@ All user visible changes to this project will be documented in this file. This p [#155]: /../../pull/155 [#156]: /../../pull/156 [#158]: /../../pull/158 +[#160]: /../../pull/160 diff --git a/jason/Cargo.toml b/jason/Cargo.toml index 00f325a71..b0fc1452a 100644 --- a/jason/Cargo.toml +++ b/jason/Cargo.toml @@ -31,6 +31,7 @@ default = ["console_error_panic_hook", "wee_alloc"] mockable = ["downcast", "fragile", "mockall", "predicates-tree"] [dependencies] +async-recursion = "0.3" async-trait = "0.1" bitflags = "1.2" console_error_panic_hook = { version = "0.1", optional = true } diff --git a/jason/demo/index.html b/jason/demo/index.html index 0f3fef64d..3558d9b86 100644 --- a/jason/demo/index.html +++ b/jason/demo/index.html @@ -372,7 +372,7 @@ .media_manager() .init_local_tracks(constraints); await updateLocalVideo(localTracks); - await room.set_local_media_settings(constraints); + await room.set_local_media_settings(constraints, false, false); } async function isVideoEnabledListener(e) { @@ -398,7 +398,7 @@ .media_manager() .init_local_tracks(constraints); await updateLocalVideo(localTracks); - await room.set_local_media_settings(constraints); + await room.set_local_media_settings(constraints, false, false); } function bindJoinButtons(roomId) { @@ -509,10 +509,7 @@ track.free(); } } - if (isAudioSendEnabled) { - constraints = await initLocalStream(); - } - await room.set_local_media_settings(constraints); + await room.set_local_media_settings(constraints, false, true); }); let videoSwitch = async () => { @@ -522,10 +519,24 @@ track.free(); } } - if (isVideoSendEnabled) { - constraints = await initLocalStream(); + try { + if (!isCallStarted) { + await initLocalStream(); + } + await room.set_local_media_settings(constraints, true, true); + } catch (e) { + let name = e.name(); + if (name === 'RecoveredException') { + alert('MediaStreamSettings set failed and current MediaStreamSettings was successfully recovered.'); + } else if (name === 'RecoverFailedException') { + alert('MediaStreamSettings set failed and MediaStreamSettings recovery failed.'); + for (const err of e.recover_fail_reasons()) { + console.error('Name: "' + err.name() + '";\nMessage: "' + err.message() + '";'); + } + } else if (name === 'ErroredException') { + alert('Fatal error occured while MediaStreamSettings update.'); + } } - await room.set_local_media_settings(constraints); }; videoSelect.addEventListener('change', videoSwitch); screenshareSwitchEl.addEventListener('change', videoSwitch); @@ -762,7 +773,7 @@ const constraints = await initLocalStream(); await fillMediaDevicesInputs(audioSelect, videoSelect); - await room.set_local_media_settings(constraints); + await room.set_local_media_settings(constraints, false, false); } else { chooseRoomButton.onclick = async function () { let roomId = roomIdInput.value; @@ -772,7 +783,7 @@ const constraints = await initLocalStream(); await fillMediaDevicesInputs(audioSelect, videoSelect); - await room.set_local_media_settings(constraints); + await room.set_local_media_settings(constraints, false, false); chooseRoomDiv.style.display = 'none'; videoCallDiv.style.display = ''; diff --git a/jason/e2e-demo/js/index.js b/jason/e2e-demo/js/index.js index 655f0eb1b..98d1f0672 100644 --- a/jason/e2e-demo/js/index.js +++ b/jason/e2e-demo/js/index.js @@ -576,7 +576,7 @@ window.onload = async function() { try { const constraints = await initLocalStream(); await fillMediaDevicesInputs(audioSelect, videoSelect, null); - await room.set_local_media_settings(constraints); + await room.set_local_media_settings(constraints, false, false); } catch (e) { console.error('Init local video failed: ' + e); } @@ -747,7 +747,7 @@ window.onload = async function() { if (!isAudioSendEnabled) { constraints = await initLocalStream(); } - await room.set_local_media_settings(constraints); + await room.set_local_media_settings(constraints, false, true); } catch (e) { console.error('Changing audio source failed: ' + e); } @@ -761,10 +761,25 @@ window.onload = async function() { track.free(); } } - if (isVideoSendEnabled) { - constraints = await initLocalStream(); + try { + if (!isCallStarted) { + await initLocalStream(); + } + await room.set_local_media_settings(constraints, true, true); + } catch (e) { + let name = e.name(); + if (name === 'RecoveredException') { + alert('MediaStreamSettings set failed and current MediaStreamSettings was successfully recovered.'); + } else if (name === 'RecoverFailedException') { + alert('MediaStreamSettings set failed and MediaStreamSettings recovery failed.'); + for (const err of e.recover_fail_reasons()) { + console.error('Name: "' + err.name() + '";\nMessage: "' + err.message() + '";'); + } + } else if (name === 'ErroredException') { + alert('Fatal error occured while MediaStreamSettings update.'); + } + console.error("Changing video source failed: " + name); } - await room.set_local_media_settings(constraints); } catch (e) { console.error('Changing video source failed: ' + e.message()); } diff --git a/jason/src/api/mod.rs b/jason/src/api/mod.rs index 6aa151c3b..f08174e09 100644 --- a/jason/src/api/mod.rs +++ b/jason/src/api/mod.rs @@ -22,7 +22,10 @@ use crate::{ #[doc(inline)] pub use self::{ connection::{Connection, ConnectionHandle, Connections}, - room::{Room, RoomCloseReason, RoomError, RoomHandle, WeakRoom}, + room::{ + ConstraintsUpdateException, Room, RoomCloseReason, RoomError, + RoomHandle, WeakRoom, + }, }; /// General library interface. diff --git a/jason/src/api/room.rs b/jason/src/api/room.rs index 0aa5ee5ed..b9ad39bfb 100644 --- a/jason/src/api/room.rs +++ b/jason/src/api/room.rs @@ -7,8 +7,9 @@ use std::{ rc::{Rc, Weak}, }; +use async_recursion::async_recursion; use async_trait::async_trait; -use derive_more::Display; +use derive_more::{Display, From}; use futures::{channel::mpsc, future, StreamExt as _}; use js_sys::Promise; use medea_client_api_proto::{ @@ -32,6 +33,7 @@ use crate::{ media_exchange_state, mute_state, LocalStreamUpdateCriteria, MediaConnectionsError, MediaState, PeerConnection, PeerError, PeerEvent, PeerEventHandler, PeerRepository, RtcStats, TrackDirection, + TransceiverSide, }, rpc::{ ClientDisconnect, CloseReason, ConnectionInfo, @@ -360,22 +362,41 @@ impl RoomHandle { /// update will change media tracks in all sending peers, so that might /// cause new [getUserMedia()][1] request. /// - /// Media obtaining/injection errors are fired to `on_failed_local_media` - /// callback. + /// Media obtaining/injection errors are additionally fired to + /// `on_failed_local_media` callback. + /// + /// If `stop_first` set to `true` then affected [`local::Track`]s will be + /// dropped before new [`MediaStreamSettings`] is applied. This is usually + /// required when changing video source device due to hardware limitations, + /// e.g. having an active track sourced from device `A` may hinder + /// [getUserMedia()][1] requests to device `B`. + /// + /// `rollback_on_fail` option configures [`MediaStreamSettings`] update + /// request to automatically rollback to previous settings if new settings + /// cannot be applied. + /// + /// If recovering from fail state isn't possible then affected media types + /// will be disabled. /// /// [`PeerConnection`]: crate::peer::PeerConnection /// [1]: https://tinyurl.com/w3-streams#dom-mediadevices-getusermedia pub fn set_local_media_settings( &self, settings: &MediaStreamSettings, + stop_first: bool, + rollback_on_fail: bool, ) -> Promise { let inner = upgrade_or_detached!(self.0, JasonError); let settings = settings.clone(); future_to_promise(async move { inner? - .set_local_media_settings(settings) + .set_local_media_settings( + settings, + stop_first, + rollback_on_fail, + ) .await - .map_err(JasonError::from)?; + .map_err(ConstraintsUpdateException::from)?; Ok(JsValue::UNDEFINED) }) } @@ -749,6 +770,182 @@ struct InnerRoom { close_reason: RefCell, } +/// JS exception for the [`RoomHandle::set_local_media_settings`]. +#[wasm_bindgen] +#[derive(Debug, From)] +#[from(forward)] +pub struct ConstraintsUpdateException(JsConstraintsUpdateError); + +#[wasm_bindgen] +impl ConstraintsUpdateException { + /// Returns name of this [`ConstraintsUpdateException`]. + pub fn name(&self) -> String { + self.0.to_string() + } + + /// Returns [`JasonError`] if this [`ConstraintsUpdateException`] represents + /// `RecoveredException` or `RecoverFailedException`. + /// + /// Returns `undefined` otherwise. + pub fn recover_reason(&self) -> JsValue { + use JsConstraintsUpdateError as E; + match &self.0 { + E::RecoverFailed { recover_reason, .. } + | E::Recovered { recover_reason, .. } => recover_reason.clone(), + _ => JsValue::UNDEFINED, + } + } + + /// Returns [`js_sys::Array`] with the [`JasonError`]s if this + /// [`ConstraintsUpdateException`] represents `RecoverFailedException`. + /// + /// Returns `undefined` otherwise. + pub fn recover_fail_reasons(&self) -> JsValue { + match &self.0 { + JsConstraintsUpdateError::RecoverFailed { + recover_fail_reasons, + .. + } => recover_fail_reasons.clone(), + _ => JsValue::UNDEFINED, + } + } + + /// Returns [`JasonError`] if this [`ConstraintsUpdateException`] represents + /// `ErroredException`. + /// + /// Returns `undefined` otherwise. + pub fn error(&self) -> JsValue { + match &self.0 { + JsConstraintsUpdateError::Errored { reason } => reason.clone(), + _ => JsValue::UNDEFINED, + } + } +} + +/// [`ConstraintsUpdateError`] for JS side. +/// +/// Should be wrapped to [`ConstraintsUpdateException`] before returning to the +/// JS side. +#[derive(Debug, Display)] +pub enum JsConstraintsUpdateError { + /// New [`MediaStreamSettings`] set failed and state was recovered + /// accordingly to the provided recover policy + /// (`rollback_on_fail`/`stop_first` arguments). + #[display(fmt = "RecoveredException")] + Recovered { + /// [`JasonError`] due to which recovery happened. + recover_reason: JsValue, + }, + + /// New [`MediaStreamSettings`] set failed and state recovering also + /// failed. + #[display(fmt = "RecoverFailedException")] + RecoverFailed { + /// [`JasonError`] due to which recovery happened. + recover_reason: JsValue, + + /// [`js_sys::Array`] with a [`JasonError`]s due to which recovery + /// failed. + recover_fail_reasons: JsValue, + }, + + /// Some another error occurred. + #[display(fmt = "ErroredException")] + Errored { reason: JsValue }, +} + +/// Constraints errors which are can occur while updating +/// [`MediaStreamSettings`] by [`InnerRoom::set_local_media_settings`] call. +#[derive(Debug)] +enum ConstraintsUpdateError { + /// New [`MediaStreamSettings`] set failed and state was recovered + /// accordingly to the provided recover policy + /// (`rollback_on_fail`/`stop_first` arguments). + Recovered { + /// [`RoomError`] due to which recovery happened. + recover_reason: Traced, + }, + + /// New [`MediaStreamSettings`] set failed and state recovering also + /// failed. + RecoverFailed { + /// [`RoomError`] due to which recovery happened. + recover_reason: Traced, + + /// [`RoomError`]s due to which recovery failed. + recover_fail_reasons: Vec>, + }, + + /// Indicates that some error occurred. + Errored { error: Traced }, +} + +impl ConstraintsUpdateError { + /// Returns new [`ConstraintsUpdateError::Recovered`]. + pub fn recovered(recover_reason: Traced) -> Self { + Self::Recovered { recover_reason } + } + + /// Converts this [`ConstraintsUpdateError`] to the + /// [`ConstraintsUpdateError::RecoverFailed`]. + pub fn recovery_failed(self, reason: Traced) -> Self { + match self { + Self::Recovered { recover_reason } => Self::RecoverFailed { + recover_reason: reason, + recover_fail_reasons: vec![recover_reason], + }, + Self::RecoverFailed { + recover_reason, + mut recover_fail_reasons, + } => { + recover_fail_reasons.push(recover_reason); + + Self::RecoverFailed { + recover_reason: reason, + recover_fail_reasons, + } + } + Self::Errored { error } => Self::RecoverFailed { + recover_reason: error, + recover_fail_reasons: vec![reason], + }, + } + } + + /// Returns [`ConstraintsUpdateError::Errored`] with a provided parameter. + pub fn errored(reason: Traced) -> Self { + Self::Errored { error: reason } + } +} + +impl From for JsConstraintsUpdateError { + fn from(from: ConstraintsUpdateError) -> Self { + use ConstraintsUpdateError as E; + match from { + E::Recovered { recover_reason } => Self::Recovered { + recover_reason: JasonError::from(recover_reason).into(), + }, + E::RecoverFailed { + recover_reason, + recover_fail_reasons, + } => Self::RecoverFailed { + recover_reason: JasonError::from(recover_reason).into(), + recover_fail_reasons: { + let arr = js_sys::Array::new(); + for e in recover_fail_reasons { + arr.push(&JasonError::from(e).into()); + } + + arr.into() + }, + }, + E::Errored { error: reason } => Self::Errored { + reason: JasonError::from(reason).into(), + }, + } + } +} + impl InnerRoom { /// Creates new [`InnerRoom`]. #[inline] @@ -938,6 +1135,34 @@ impl InnerRoom { .is_none() } + /// Updates [`MediaState`]s to the provided `states_update` and disables all + /// [`Sender`]s which are doesn't have [`local::Track`]. + /// + /// [`Sender`]: crate::peer::Sender + async fn disable_senders_without_tracks( + &self, + peer: &Rc, + kinds: LocalStreamUpdateCriteria, + mut states_update: HashMap>, + ) -> Result<(), Traced> { + use media_exchange_state::Stable::Disabled; + + self.send_constraints + .set_media_exchange_state_by_kinds(Disabled, kinds); + let senders_to_disable = peer.get_senders_without_tracks(kinds); + + states_update.entry(peer.id()).or_default().extend( + senders_to_disable + .into_iter() + .map(|s| (s.track_id(), MediaState::from(Disabled))), + ); + self.update_media_states(states_update) + .await + .map_err(tracerr::map_from_and_wrap!())?; + + Ok(()) + } + /// Updates this [`Room`]s [`MediaStreamSettings`]. This affects all /// [`PeerConnection`]s in this [`Room`]. If [`MediaStreamSettings`] is /// configured for some [`Room`], then this [`Room`] can only send @@ -951,34 +1176,107 @@ impl InnerRoom { /// Will update [`media_exchange_state::Stable`]s of the [`Sender`]s which /// are should be enabled or disabled. /// + /// If `stop_first` set to `true` then affected [`local::Track`]s will be + /// dropped before new [`MediaStreamSettings`] is applied. This is usually + /// required when changing video source device due to hardware limitations, + /// e.g. having an active track sourced from device `A` may hinder + /// [getUserMedia()][1] requests to device `B`. + /// + /// `rollback_on_fail` option configures [`MediaStreamSettings`] update + /// request to automatically rollback to previous settings if new settings + /// cannot be applied. + /// + /// If recovering from fail state isn't possible and `stop_first` set to + /// `true` then affected media types will be disabled. + /// /// [1]: https://tinyurl.com/rnxcavf /// [`PeerConnection`]: crate::peer::PeerConnection /// [`Sender`]: crate::peer::Sender + #[async_recursion(?Send)] async fn set_local_media_settings( &self, - settings: MediaStreamSettings, - ) -> Result<(), Traced> { - self.send_constraints.constrain(settings); + new_settings: MediaStreamSettings, + stop_first: bool, + rollback_on_fail: bool, + ) -> Result<(), ConstraintsUpdateError> { + use ConstraintsUpdateError as E; + + let current_settings = self.send_constraints.inner(); + self.send_constraints.constrain(new_settings); + let criteria_kinds_diff = self + .send_constraints + .calculate_kinds_diff(¤t_settings); + let peers = self.peers.get_all(); + + if stop_first { + for peer in &peers { + peer.drop_send_tracks(criteria_kinds_diff).await; + } + } - let mut states_update = HashMap::new(); - for peer in self.peers.get_all() { - peer.update_local_stream(LocalStreamUpdateCriteria::all()) + let mut states_update: HashMap<_, HashMap<_, _>> = HashMap::new(); + for peer in peers { + match peer + .update_local_stream(LocalStreamUpdateCriteria::all()) .await - .map_err(tracerr::map_from_and_wrap!(=> RoomError)) - .map(|new_media_exchange_states| { - states_update.insert( - peer.id(), - new_media_exchange_states - .into_iter() - .map(|(id, s)| (id, s.into())) - .collect(), + { + Ok(states) => { + states_update.entry(peer.id()).or_default().extend( + states.into_iter().map(|(id, s)| (id, s.into())), ); - })?; + } + Err(e) => { + if !matches!(e.as_ref(), PeerError::MediaManager(_)) { + return Err(E::errored(tracerr::map_from_and_wrap!()( + e.clone(), + ))); + } + + let err = if rollback_on_fail { + self.set_local_media_settings( + current_settings, + stop_first, + false, + ) + .await + .map_err(|err| { + err.recovery_failed(tracerr::map_from_and_wrap!()( + e.clone(), + )) + })?; + + E::recovered(tracerr::map_from_and_wrap!()(e.clone())) + } else if stop_first { + self.disable_senders_without_tracks( + &peer, + criteria_kinds_diff, + states_update, + ) + .await + .map_err(|err| { + E::RecoverFailed { + recover_reason: tracerr::map_from_and_new!( + e.clone() + ), + recover_fail_reasons: vec![ + tracerr::map_from_and_new!(err), + ], + } + })?; + + E::recovered(tracerr::map_from_and_wrap!()(e.clone())) + } else { + E::errored(tracerr::map_from_and_wrap!()(e.clone())) + }; + + return Err(err); + } + } } self.update_media_states(states_update) .await - .map_err(tracerr::map_from_and_wrap!()) + .map_err(|e| E::errored(tracerr::map_from_and_new!(e))) } /// Stops state transition timers in all [`PeerConnection`]'s in this diff --git a/jason/src/media/constraints.rs b/jason/src/media/constraints.rs index 6fb9137b0..8481f654a 100644 --- a/jason/src/media/constraints.rs +++ b/jason/src/media/constraints.rs @@ -15,7 +15,9 @@ use web_sys as sys; use crate::{ media::MediaKind, - peer::{media_exchange_state, mute_state, MediaState}, + peer::{ + media_exchange_state, mute_state, LocalStreamUpdateCriteria, MediaState, + }, utils::get_property_by_name, }; @@ -79,6 +81,16 @@ impl From for LocalTracksConstraints { } impl LocalTracksConstraints { + /// Returns [`LocalStreamUpdateCriteria`] with [`MediaKind`] and + /// [`MediaSourceKind`] which are different in the provided + /// [`MediaStreamSettings`]. + pub fn calculate_kinds_diff( + &self, + settings: &MediaStreamSettings, + ) -> LocalStreamUpdateCriteria { + self.0.borrow().calculate_kinds_diff(&settings) + } + /// Constrains the underlying [`MediaStreamSettings`] with the given `other` /// [`MediaStreamSettings`]. #[inline] @@ -107,6 +119,19 @@ impl LocalTracksConstraints { .set_track_media_state(state, kind, source_kind); } + /// Enables/disables provided [`LocalStreamUpdateCriteria`] based on + /// provided [`media_exchange_state`]. + #[inline] + pub fn set_media_exchange_state_by_kinds( + &self, + state: media_exchange_state::Stable, + kinds: LocalStreamUpdateCriteria, + ) { + self.0 + .borrow_mut() + .set_media_exchange_state_by_kinds(state, kinds) + } + /// Indicates whether provided [`MediaType`] is enabled in the underlying /// [`MediaStreamSettings`]. #[inline] @@ -139,7 +164,7 @@ impl LocalTracksConstraints { /// [MediaStreamConstraints][1] for the audio media type. /// /// [1]: https://w3.org/TR/mediacapture-streams/#dom-mediastreamconstraints -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct AudioMediaTracksSettings { /// Constraints applicable to video tracks. constraints: AudioTrackConstraints, @@ -163,7 +188,7 @@ impl Default for AudioMediaTracksSettings { } } -/// Returns `true` if provided [`SysMediaStreamTrack`] basically satisfies any +/// Returns `true` if provided [`sys::MediaStreamTrack`] basically satisfies any /// constraints with a provided [`MediaKind`]. #[inline] fn satisfies_track(track: &sys::MediaStreamTrack, kind: MediaKind) -> bool { @@ -174,7 +199,7 @@ fn satisfies_track(track: &sys::MediaStreamTrack, kind: MediaKind) -> bool { /// [MediaStreamConstraints][1] for the video media type. /// /// [1]: https://w3.org/TR/mediacapture-streams/#dom-mediastreamconstraints -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct VideoTrackConstraints { /// Constraints applicable to video tracks. /// @@ -245,8 +270,8 @@ impl VideoTrackConstraints { } impl VideoTrackConstraints { - /// Returns `true` if the provided [`SysMediaStreamTrack`] satisfies device - /// [`VideoTrackConstraints::constraints`]. + /// Indicates whether the provided [`sys::MediaStreamTrack`] satisfies + /// device [`VideoTrackConstraints::constraints`]. /// /// Returns `false` if [`VideoTrackConstraints::constraints`] is not set. fn satisfies(&self, track: &sys::MediaStreamTrack) -> bool { @@ -258,8 +283,8 @@ impl VideoTrackConstraints { } impl VideoTrackConstraints { - /// Returns `true` if the provided [`SysMediaStreamTrack`] satisfies device - /// [`VideoTrackConstraints::constraints`]. + /// Indicates whether the provided [`sys::MediaStreamTrack`] satisfies + /// device [`VideoTrackConstraints::constraints`]. /// /// Returns `false` if [`VideoTrackConstraints::constraints`] is not set. fn satisfies(&self, track: &sys::MediaStreamTrack) -> bool { @@ -274,7 +299,7 @@ impl VideoTrackConstraints { /// /// [1]: https://w3.org/TR/mediacapture-streams/#dom-mediastreamconstraints #[wasm_bindgen] -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct MediaStreamSettings { /// [MediaStreamConstraints][1] for the audio media type. /// @@ -360,6 +385,28 @@ impl MediaStreamSettings { } } + /// Returns [`LocalStreamUpdateCriteria`] with [`MediaKind`] and + /// [`MediaSourceKind`] which are different in the provided + /// [`MediaStreamSettings`]. + #[must_use] + pub fn calculate_kinds_diff( + &self, + another: &Self, + ) -> LocalStreamUpdateCriteria { + let mut kinds = LocalStreamUpdateCriteria::empty(); + if self.device_video != another.device_video { + kinds.add(MediaKind::Video, MediaSourceKind::Device); + } + if self.display_video != another.display_video { + kinds.add(MediaKind::Video, MediaSourceKind::Display); + } + if self.audio != another.audio { + kinds.add(MediaKind::Audio, MediaSourceKind::Device); + } + + kinds + } + /// Returns only audio constraints. #[inline] pub fn get_audio(&self) -> &AudioTrackConstraints { @@ -424,6 +471,26 @@ impl MediaStreamSettings { } } + /// Enables/disables provided [`LocalStreamUpdateCriteria`] based on + /// provided [`media_exchange_state`]. + #[inline] + pub fn set_media_exchange_state_by_kinds( + &mut self, + state: media_exchange_state::Stable, + kinds: LocalStreamUpdateCriteria, + ) { + let enabled = state == media_exchange_state::Stable::Enabled; + if kinds.has(MediaKind::Audio, MediaSourceKind::Device) { + self.set_audio_publish(enabled); + } + if kinds.has(MediaKind::Video, MediaSourceKind::Device) { + self.set_video_publish(enabled, Some(MediaSourceKind::Device)); + } + if kinds.has(MediaKind::Video, MediaSourceKind::Display) { + self.set_video_publish(enabled, Some(MediaSourceKind::Display)); + } + } + /// Sets the underlying [`AudioMediaTracksSettings::muted`] to the provided /// value. fn set_audio_muted(&mut self, muted: bool) { @@ -787,7 +854,7 @@ impl From for TrackConstraints { /// Constraints applicable to audio tracks. #[wasm_bindgen] -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct AudioTrackConstraints { /// The identifier of the device generating the content for the media /// track. @@ -889,7 +956,7 @@ trait Constraint { /// [MediaStreamTrack][1]. /// /// [1]: https://w3.org/TR/mediacapture-streams/#dom-mediastreamtrack -#[derive(AsRef, Clone, Debug)] +#[derive(AsRef, Clone, Debug, Eq, PartialEq)] #[as_ref(forward)] struct DeviceId(String); @@ -898,7 +965,7 @@ impl Constraint for DeviceId { } /// Height, in pixels, of the video. -#[derive(Debug, Clone, Copy, Into)] +#[derive(Clone, Copy, Debug, Eq, Into, PartialEq)] struct Height(u32); impl Constraint for Height { @@ -906,7 +973,7 @@ impl Constraint for Height { } /// Width, in pixels, of the video. -#[derive(Debug, Clone, Copy, Into)] +#[derive(Clone, Copy, Debug, Eq, Into, PartialEq)] struct Width(u32); impl Constraint for Width { @@ -918,7 +985,7 @@ impl Constraint for Width { /// /// [1]: https://w3.org/TR/mediacapture-streams/#dom-videofacingmodeenum #[wasm_bindgen] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FacingMode { /// Facing toward the user (a self-view camera). User, @@ -952,7 +1019,7 @@ impl Constraint for FacingMode { /// `[0, 4294967295]` range. /// /// [1]: https://tinyurl.com/w3-streams#dom-constrainulong -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ConstrainU32 { /// Must be the parameter's value. Exact(T), @@ -1016,7 +1083,7 @@ impl> From> /// possible) constrain. /// /// [1]: https://w3.org/TR/mediacapture-streams/#dom-constraindomstring -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ConstrainString { Exact(T), Ideal(T), @@ -1057,7 +1124,7 @@ impl> From<&ConstrainString> /// Constraints applicable to video tracks that are sourced from some media /// device. #[wasm_bindgen] -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct DeviceVideoTrackConstraints { /// Importance of this [`DeviceVideoTrackConstraints`]. /// @@ -1200,7 +1267,7 @@ impl DeviceVideoTrackConstraints { /// Constraints applicable to video tracks sourced from screen capture. #[wasm_bindgen] -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct DisplayVideoTrackConstraints { /// Importance of this [`DisplayVideoTrackConstraints`]. /// diff --git a/jason/src/media/manager.rs b/jason/src/media/manager.rs index 9e6293565..f46541da9 100644 --- a/jason/src/media/manager.rs +++ b/jason/src/media/manager.rs @@ -329,10 +329,10 @@ impl InnerMediaManager { /// # Errors /// /// - [`MediaManagerError::LocalTrackIsEnded`] if at least one track from - /// the provided [`SysMediaStream`] is in [`ended`][1] state. + /// the provided [`sys::MediaStream`] is in [`ended`][1] state. /// /// - [`MediaManagerError::LocalTrackIsMuted`] if at least one track from - /// the provided [`SysMediaStream`] is in [`muted`][2] state. + /// the provided [`sys::MediaStream`] is in [`muted`][2] state. /// /// In case of error all tracks are stopped and are not saved in /// [`MediaManager`]'s tracks storage. diff --git a/jason/src/peer/media/mod.rs b/jason/src/peer/media/mod.rs index 6118cf5f0..0e185cdcc 100644 --- a/jason/src/peer/media/mod.rs +++ b/jason/src/peer/media/mod.rs @@ -818,6 +818,39 @@ impl MediaConnections { .into_iter() .for_each(|t| t.reset_media_state_transition_timeout()); } + + /// Returns all [`Sender`]s which are matches provided + /// [`LocalStreamUpdateCriteria`] and doesn't have [`local::Track`]. + pub fn get_senders_without_tracks( + &self, + kinds: LocalStreamUpdateCriteria, + ) -> Vec> { + self.0 + .borrow() + .senders + .values() + .filter(|s| { + kinds.has(s.kind(), s.source_kind()) + && s.enabled() + && !s.has_track() + }) + .cloned() + .collect() + } + + /// Drops [`local::Track`]s of all [`Sender`]s which are matches provided + /// [`LocalStreamUpdateCriteria`]. + pub async fn drop_send_tracks(&self, kinds: LocalStreamUpdateCriteria) { + for sender in self + .0 + .borrow() + .senders + .values() + .filter(|s| kinds.has(s.kind(), s.source_kind())) + { + sender.remove_track().await; + } + } } #[cfg(feature = "mockable")] @@ -899,4 +932,14 @@ impl MediaConnections { .find(|s| s.muted()) .is_none() } + + /// Returns all underlying [`Sender`]'s. + pub fn get_senders(&self) -> Vec> { + self.0 + .borrow() + .senders + .values() + .map(|sndr| Rc::clone(&sndr)) + .collect() + } } diff --git a/jason/src/peer/media/sender.rs b/jason/src/peer/media/sender.rs index d6d5b2323..5bb8dfdfd 100644 --- a/jason/src/peer/media/sender.rs +++ b/jason/src/peer/media/sender.rs @@ -100,11 +100,35 @@ impl Sender { &self.caps } - /// Returns `true` if this [`Sender`] is publishing media traffic. + /// Indicates whether this [`Sender`] is publishing media traffic. + #[inline] + #[must_use] pub fn is_publishing(&self) -> bool { self.transceiver.has_direction(TransceiverDirection::SEND) } + /// Drops [`local::Track`] used by this [`Sender`]. Sets track used by + /// sending side of inner transceiver to [`None`]. + #[inline] + pub async fn remove_track(&self) { + // cannot fail + self.transceiver.set_send_track(None).await.unwrap(); + } + + /// Indicates whether this [`Sender`] has [`local::Track`]. + #[inline] + #[must_use] + pub fn has_track(&self) -> bool { + self.transceiver.has_send_track() + } + + /// Indicates whether this [`Sender`] is enabled. + #[inline] + #[must_use] + pub fn enabled(&self) -> bool { + self.media_exchange_state.enabled() + } + /// Updates [`Sender`]s general media exchange state based on the provided /// [`media_exchange_state::Stable`]. /// @@ -232,6 +256,8 @@ impl Sender { } /// Returns [`Transceiver`] of this [`Sender`]. + #[inline] + #[must_use] pub fn transceiver(&self) -> Transceiver { self.transceiver.clone() } @@ -242,13 +268,6 @@ impl Sender { self.general_media_exchange_state.get() == media_exchange_state::Stable::Enabled } - - /// Drops [`local::Track`] used by this [`Sender`]. Sets track used by - /// sending side of inner transceiver to `None`. - async fn remove_track(&self) { - // cannot fail - self.transceiver.set_send_track(None).await.unwrap(); - } } #[cfg(feature = "mockable")] @@ -275,13 +294,6 @@ impl Sender { pub fn muted(&self) -> bool { self.mute_state.muted() } - - /// Indicates whether this [`Sender`] is enabled. - #[inline] - #[must_use] - pub fn enabled(&self) -> bool { - self.media_exchange_state.enabled() - } } impl TransceiverSide for Sender { diff --git a/jason/src/peer/mod.rs b/jason/src/peer/mod.rs index d7afcb261..592a80edb 100644 --- a/jason/src/peer/mod.rs +++ b/jason/src/peer/mod.rs @@ -317,6 +317,24 @@ impl PeerConnection { Ok(Rc::new(peer)) } + /// Returns all [`Sender`]s which are matches provided + /// [`LocalStreamUpdateCriteria`] and doesn't have [`local::Track`]. + #[inline] + #[must_use] + pub fn get_senders_without_tracks( + &self, + kinds: LocalStreamUpdateCriteria, + ) -> Vec> { + self.media_connections.get_senders_without_tracks(kinds) + } + + /// Drops [`local::Track`]s of all [`Sender`]s which are matches provided + /// [`LocalStreamUpdateCriteria`]. + #[inline] + pub async fn drop_send_tracks(&self, kinds: LocalStreamUpdateCriteria) { + self.media_connections.drop_send_tracks(kinds).await + } + /// Stops inner state transitions expiry timers. /// /// Inner state transitions initiated via external APIs that can not be @@ -922,7 +940,6 @@ impl PeerConnection { .map_err(tracerr::map_from_and_wrap!()) } - #[cfg(feature = "mockable")] /// Indicates whether all [`Receiver`]s audio tracks are enabled. #[inline] #[must_use] @@ -930,7 +947,6 @@ impl PeerConnection { self.media_connections.is_recv_audio_enabled() } - #[cfg(feature = "mockable")] /// Indicates whether all [`Receiver`]s video tracks are enabled. #[inline] #[must_use] @@ -985,6 +1001,18 @@ impl PeerConnection { pub fn is_send_audio_unmuted(&self) -> bool { self.media_connections.is_send_audio_unmuted() } + + /// Returns all [`local::Track`]s from [`PeerConnection`]'s + /// [`Transceiver`]s. + #[inline] + #[must_use] + pub fn get_send_tracks(&self) -> Vec> { + self.media_connections + .get_senders() + .into_iter() + .filter_map(|sndr| sndr.transceiver().send_track()) + .collect() + } } impl Drop for PeerConnection { diff --git a/jason/src/peer/transceiver.rs b/jason/src/peer/transceiver.rs index b28801f08..630efc574 100644 --- a/jason/src/peer/transceiver.rs +++ b/jason/src/peer/transceiver.rs @@ -76,6 +76,13 @@ impl Transceiver { self.send_track.borrow().clone() } + /// Indicates whether this [`Transceiver`] has [`local::Track`]. + #[inline] + #[must_use] + pub fn has_send_track(&self) -> bool { + self.send_track.borrow().is_some() + } + /// Sets the underlying [`local::Track`]'s `enabled` field to the provided /// value, if any. pub fn set_send_track_enabled(&self, enabled: bool) { diff --git a/jason/tests/api/room.rs b/jason/tests/api/room.rs index 42c84fca3..b15189149 100644 --- a/jason/tests/api/room.rs +++ b/jason/tests/api/room.rs @@ -10,8 +10,9 @@ use futures::{ stream::{self, BoxStream, StreamExt as _}, }; use medea_client_api_proto::{ - Command, Event, NegotiationRole, PeerId, Track, TrackId, TrackPatchEvent, - TrackUpdate, + Command, Direction, Event, IceConnectionState, MediaSourceKind, MediaType, + MemberId, NegotiationRole, PeerId, PeerMetrics, Track, TrackId, + TrackPatchEvent, TrackUpdate, VideoSettings, }; use medea_jason::{ api::Room, @@ -27,10 +28,10 @@ use wasm_bindgen_futures::{spawn_local, JsFuture}; use wasm_bindgen_test::*; use crate::{ - delay_for, get_jason_error, get_test_recv_tracks, get_test_required_tracks, - get_test_tracks, get_test_unrequired_tracks, media_stream_settings, - timeout, wait_and_check_test_result, yield_now, MockNavigator, - TEST_ROOM_URL, + delay_for, get_constraints_update_exception, get_jason_error, + get_test_recv_tracks, get_test_required_tracks, get_test_tracks, + get_test_unrequired_tracks, media_stream_settings, timeout, + wait_and_check_test_result, yield_now, MockNavigator, TEST_ROOM_URL, }; wasm_bindgen_test_configure!(run_in_browser); @@ -45,6 +46,9 @@ macro_rules! can_fail_in_firefox { }; } +/// Returns [`Room`] with [`MockRpcSession`] configured to emit events from +/// provided stream and [`UnboundedReceiver`] of [`Command`]'s so you can assert +/// commands sent by [`Room`]. fn get_test_room( events: BoxStream<'static, Event>, ) -> (Room, UnboundedReceiver) { @@ -105,10 +109,11 @@ async fn get_test_room_and_exist_peer( let room = Room::new(Rc::new(rpc), Box::new(Repository::new(Rc::default()))); if let Some(media_stream_settings) = &media_stream_settings { - JsFuture::from( - room.new_handle() - .set_local_media_settings(&media_stream_settings), - ) + JsFuture::from(room.new_handle().set_local_media_settings( + &media_stream_settings, + false, + false, + )) .await .unwrap(); } @@ -128,163 +133,16 @@ async fn get_test_room_and_exist_peer( (room, peer, event_tx) } -/// Tests RoomHandle::set_local_media_settings before creating PeerConnection. -/// Setup: -/// 1. Create Room. -/// 2. Set `on_failed_local_media` callback. -/// 3. Invoke `room_handle.set_local_media_settings` with one track. -/// 4. Send `PeerCreated` to room wth two tracks -/// Assertions: -/// 1. `on_failed_local_media` callback was invoked. -#[wasm_bindgen_test] -async fn error_inject_invalid_local_stream_into_new_peer() { - let (event_tx, event_rx) = mpsc::unbounded(); - let (room, _rx) = get_test_room(Box::pin(event_rx)); - let room_handle = room.new_handle(); - - let (cb, test_result) = js_callback!(|err: JasonError| { - cb_assert_eq!(&err.name(), "CannotDisableRequiredSender"); - cb_assert_eq!( - err.message(), - "MediaExchangeState of Sender can't be transited into disabled \ - state, because this Sender is required." - ); - }); - room_handle.on_failed_local_media(cb.into()).unwrap(); - - let (audio_track, video_track) = get_test_required_tracks(); - - let mut constraints = MediaStreamSettings::new(); - constraints.audio(AudioTrackConstraints::new()); - - JsFuture::from(room_handle.set_local_media_settings(&constraints)) - .await - .unwrap(); - - event_tx - .unbounded_send(Event::PeerCreated { - peer_id: PeerId(1), - negotiation_role: NegotiationRole::Offerer, - tracks: vec![audio_track, video_track], - ice_servers: Vec::new(), - force_relay: false, - }) - .unwrap(); - - wait_and_check_test_result(test_result, || {}).await; -} - -/// Tests RoomHandle::set_local_media_settings for existing PeerConnection. -/// Setup: -/// 1. Create Room. -/// 2. Set `on_failed_local_media` callback. -/// 3. Invoke `peer.get_offer` with two tracks. -/// 4. Invoke `room_handle.set_local_media_settings` with only one track. -/// Assertions: -/// 1. `on_failed_local_media` was invoked. -#[wasm_bindgen_test] -async fn error_inject_invalid_local_stream_into_room_on_exists_peer() { - let (cb, test_result) = js_callback!(|err: JasonError| { - cb_assert_eq!(&err.name(), "TracksRequest"); - cb_assert_eq!( - &err.message(), - "provided multiple device video MediaStreamTracks" - ); - }); - let (audio_track, video_track) = get_test_required_tracks(); - let (room, _peer, _) = - get_test_room_and_exist_peer(vec![audio_track, video_track], None) - .await; - - let mut constraints = MediaStreamSettings::new(); - constraints.audio(AudioTrackConstraints::new()); - let room_handle = room.new_handle(); - room_handle.on_failed_local_media(cb.into()).unwrap(); - let err = get_jason_error( - JsFuture::from(room_handle.set_local_media_settings(&constraints)) - .await - .unwrap_err(), - ); - assert_eq!(err.name(), "InvalidLocalTracks"); - assert_eq!( - err.message(), - "Invalid local tracks: provided multiple device video \ - MediaStreamTracks" - ); - - wait_and_check_test_result(test_result, || {}).await; -} - -#[wasm_bindgen_test] -async fn no_errors_if_track_not_provided_when_its_optional() { - async fn helper( - audio_required: bool, - video_required: bool, - add_audio: bool, - add_video: bool, - ) -> Result<(), ()> { - let (test_tx, test_rx) = oneshot::channel(); - let closure = wasm_bindgen::closure::Closure::once_into_js(move || { - test_tx.send(()).unwrap(); - }); - let (audio_track, video_track) = - get_test_tracks(audio_required, video_required); - let (room, _peer, _) = - get_test_room_and_exist_peer(vec![audio_track, video_track], None) - .await; - - let mut constraints = MediaStreamSettings::new(); - if add_audio { - constraints.audio(AudioTrackConstraints::new()); - } - if add_video { - constraints.device_video(DeviceVideoTrackConstraints::new()); - } - - let room_handle = room.new_handle(); - room_handle.on_failed_local_media(closure.into()).unwrap(); - - let is_should_be_ok = - audio_required == add_audio || video_required == add_video; - assert_eq!( - JsFuture::from(room_handle.set_local_media_settings(&constraints)) - .await - .is_ok(), - is_should_be_ok, - "audio_required: {}; add_audio: {}; video_required: {}; \ - add_video: {}", - audio_required, - add_audio, - video_required, - add_video, - ); - - timeout(1000, test_rx) - .await - .map(|rx| rx.unwrap()) - .map_err(|_| ()) - } - - // on_failed_local_media callback does not fire - helper(true, false, true, false).await.unwrap_err(); - helper(false, true, false, true).await.unwrap_err(); - helper(false, false, false, false).await.unwrap_err(); - - // on_failed_local_media callback fires - helper(true, false, false, true).await.unwrap(); - helper(false, true, true, false).await.unwrap(); - helper(true, true, false, false).await.unwrap(); -} - #[wasm_bindgen_test] async fn error_get_local_stream_on_new_peer() { let (event_tx, event_rx) = mpsc::unbounded(); let (room, _) = get_test_room(Box::pin(event_rx)); let room_handle = room.new_handle(); - JsFuture::from( - room_handle - .set_local_media_settings(&media_stream_settings(true, true)), - ) + JsFuture::from(room_handle.set_local_media_settings( + &media_stream_settings(true, true), + false, + false, + )) .await .unwrap(); @@ -830,10 +688,11 @@ mod disable_send_tracks { async fn disable_audio_room_before_init_peer() { let (event_tx, event_rx) = mpsc::unbounded(); let (room, mut commands_rx) = get_test_room(Box::pin(event_rx)); - JsFuture::from( - room.new_handle() - .set_local_media_settings(&media_stream_settings(true, true)), - ) + JsFuture::from(room.new_handle().set_local_media_settings( + &media_stream_settings(true, true), + false, + false, + )) .await .unwrap(); @@ -880,10 +739,11 @@ mod disable_send_tracks { async fn enable_video_room_before_init_peer() { let (event_tx, event_rx) = mpsc::unbounded(); let (room, mut commands_rx) = get_test_room(Box::pin(event_rx)); - JsFuture::from( - room.new_handle() - .set_local_media_settings(&media_stream_settings(true, true)), - ) + JsFuture::from(room.new_handle().set_local_media_settings( + &media_stream_settings(true, true), + false, + false, + )) .await .unwrap(); @@ -1821,64 +1681,534 @@ async fn only_one_gum_performed_on_enable_by_server() { mock.stop(); } -/// Tests that calling [`RoomHandle::set_local_media_settings`] updates needed -/// [`media_exchange_state::State`]s of the [`Sender`]s. -#[wasm_bindgen_test] -async fn set_local_media_stream_settings_updates_media_exchange_state() { - let (event_tx, event_rx) = mpsc::unbounded(); - let (room, mut commands_rx) = get_test_room(Box::pin(event_rx)); - let room_handle = room.new_handle(); - room_handle - .on_failed_local_media(js_sys::Function::new_no_args("")) +/// Tests for [`RoomHandle::set_local_media_settings`]. +mod set_local_media_settings { + use super::*; + + /// Sets up connection between two peers in single room with first peer + /// sending video to second peer. + async fn room_with_connected_peers( + ) -> (Room, Rc, Rc) { + let (event_tx, event_rx) = mpsc::unbounded(); + let (room, mut commands_rx) = get_test_room(Box::pin(event_rx)); + + event_tx + .unbounded_send(Event::PeerCreated { + peer_id: PeerId(0), + negotiation_role: NegotiationRole::Offerer, + tracks: vec![Track { + id: TrackId(1), + direction: Direction::Send { + receivers: vec![MemberId::from("bob")], + mid: None, + }, + media_type: MediaType::Video(VideoSettings { + required: false, + source_kind: MediaSourceKind::Device, + }), + }], + ice_servers: Vec::new(), + force_relay: false, + }) + .unwrap(); + + let mut peers_connected = HashMap::new(); + peers_connected.insert(PeerId(0), false); + peers_connected.insert(PeerId(1), false); + while let Some(command) = commands_rx.next().await { + match command { + Command::MakeSdpOffer { sdp_offer, .. } => { + event_tx + .unbounded_send(Event::PeerCreated { + peer_id: PeerId(1), + negotiation_role: NegotiationRole::Answerer( + sdp_offer, + ), + tracks: vec![Track { + id: TrackId(1), + direction: Direction::Recv { + sender: MemberId::from("Alice"), + mid: Some(String::from("1")), + }, + media_type: MediaType::Video(VideoSettings { + required: true, + source_kind: MediaSourceKind::Device, + }), + }], + ice_servers: Vec::new(), + force_relay: false, + }) + .unwrap(); + } + Command::MakeSdpAnswer { sdp_answer, .. } => { + event_tx + .unbounded_send(Event::SdpAnswerMade { + peer_id: PeerId(0), + sdp_answer, + }) + .unwrap(); + } + Command::SetIceCandidate { peer_id, candidate } => { + let event_peer_id = match peer_id { + PeerId(0) => PeerId(1), + PeerId(1) => PeerId(0), + _ => unreachable!(), + }; + event_tx + .unbounded_send(Event::IceCandidateDiscovered { + peer_id: event_peer_id, + candidate, + }) + .unwrap(); + } + Command::AddPeerConnectionMetrics { + peer_id, + metrics: PeerMetrics::IceConnectionState(state), + } => { + if let IceConnectionState::Connected = state { + peers_connected.insert(peer_id, true); + } + } + _ => {} + }; + if peers_connected.values().all(|v| *v) { + break; + } + } + + spawn_local(async move { + while let Some(command) = commands_rx.next().await { + match command { + Command::UpdateTracks { + peer_id, + tracks_patches, + } => { + event_tx + .unbounded_send(Event::TracksApplied { + peer_id, + updates: tracks_patches + .into_iter() + .map(|p| TrackUpdate::Updated(p.into())) + .collect(), + negotiation_role: None, + }) + .unwrap(); + } + _ => (), + } + } + }); + + let peer1 = room.get_peer_by_id(PeerId(0)).unwrap(); + let peer2 = room.get_peer_by_id(PeerId(1)).unwrap(); + + assert!(peer1.is_send_video_enabled(Some(MediaSourceKind::Device))); + assert!(peer1.is_send_video_unmuted(Some(MediaSourceKind::Device))); + assert!(peer2.is_recv_video_enabled()); + + let mut send_tracks = peer1.get_send_tracks(); + assert_eq!(send_tracks.len(), 1); + + let track = send_tracks.pop().unwrap(); + assert_eq!(track.kind(), MediaKind::Video); + assert_eq!(track.media_source_kind(), MediaSourceKind::Device); + + (room, peer1, peer2) + } + + /// Returns [`MediaStreamSettings`] which requires that device ID should be + /// `foobar`. + fn media_settings_with_device_id() -> MediaStreamSettings { + let mut settings = MediaStreamSettings::new(); + let mut device_video = DeviceVideoTrackConstraints::new(); + device_video.device_id("foobar".to_string()); + settings.device_video(device_video); + + settings + } + + /// Tests RoomHandle::set_local_media_settings before creating + /// PeerConnection. Setup: + /// 1. Create Room. + /// 2. Set `on_failed_local_media` callback. + /// 3. Invoke `room_handle.set_local_media_settings` with one track. + /// 4. Send `PeerCreated` to room wth two tracks + /// Assertions: + /// 1. `on_failed_local_media` callback was invoked. + #[wasm_bindgen_test] + async fn error_inject_invalid_local_stream_into_new_peer() { + let (event_tx, event_rx) = mpsc::unbounded(); + let (room, _rx) = get_test_room(Box::pin(event_rx)); + let room_handle = room.new_handle(); + + let (cb, test_result) = js_callback!(|err: JasonError| { + cb_assert_eq!(&err.name(), "CannotDisableRequiredSender"); + cb_assert_eq!( + err.message(), + "MediaExchangeState of Sender can't be transited into \ + disabled state, because this Sender is required." + ); + }); + room_handle.on_failed_local_media(cb.into()).unwrap(); + + let (audio_track, video_track) = get_test_required_tracks(); + + let mut constraints = MediaStreamSettings::new(); + constraints.audio(AudioTrackConstraints::new()); + + JsFuture::from(room_handle.set_local_media_settings( + &constraints, + false, + false, + )) + .await .unwrap(); - JsFuture::from( - room_handle - .set_local_media_settings(&media_stream_settings(true, false)), - ) - .await - .unwrap(); - let (audio_track, video_track) = get_test_unrequired_tracks(); - event_tx - .unbounded_send(Event::PeerCreated { - peer_id: PeerId(1), - negotiation_role: NegotiationRole::Offerer, - tracks: vec![audio_track, video_track], - ice_servers: Vec::new(), - force_relay: false, - }) + event_tx + .unbounded_send(Event::PeerCreated { + peer_id: PeerId(1), + negotiation_role: NegotiationRole::Offerer, + tracks: vec![audio_track, video_track], + ice_servers: Vec::new(), + force_relay: false, + }) + .unwrap(); + + wait_and_check_test_result(test_result, || {}).await; + } + + /// Tests RoomHandle::set_local_media_settings for existing PeerConnection. + /// Setup: + /// 1. Create Room. + /// 2. Set `on_failed_local_media` callback. + /// 3. Invoke `peer.get_offer` with two tracks. + /// 4. Invoke `room_handle.set_local_media_settings` with only one + /// track. Assertions: + /// 1. `on_failed_local_media` was invoked. + #[wasm_bindgen_test] + async fn error_inject_invalid_local_stream_into_room_on_exists_peer() { + let (cb, test_result) = js_callback!(|err: JasonError| { + cb_assert_eq!(&err.name(), "TracksRequest"); + cb_assert_eq!( + &err.message(), + "provided multiple device video MediaStreamTracks" + ); + }); + let (audio_track, video_track) = get_test_required_tracks(); + let (room, _peer, _) = + get_test_room_and_exist_peer(vec![audio_track, video_track], None) + .await; + + let mut constraints = MediaStreamSettings::new(); + constraints.audio(AudioTrackConstraints::new()); + let room_handle = room.new_handle(); + room_handle.on_failed_local_media(cb.into()).unwrap(); + let err = get_constraints_update_exception( + JsFuture::from(room_handle.set_local_media_settings( + &constraints, + false, + false, + )) + .await + .unwrap_err(), + ); + let err = get_jason_error(err.error()); + assert_eq!(err.name(), "InvalidLocalTracks"); + assert_eq!( + err.message(), + "Invalid local tracks: provided multiple device video \ + MediaStreamTracks" + ); + + wait_and_check_test_result(test_result, || {}).await; + } + + #[wasm_bindgen_test] + async fn no_errors_if_track_not_provided_when_its_optional() { + async fn helper( + audio_required: bool, + video_required: bool, + add_audio: bool, + add_video: bool, + ) -> Result<(), ()> { + let (test_tx, test_rx) = oneshot::channel(); + let closure = + wasm_bindgen::closure::Closure::once_into_js(move || { + test_tx.send(()).unwrap(); + }); + let (audio_track, video_track) = + get_test_tracks(audio_required, video_required); + let (room, _peer, _) = get_test_room_and_exist_peer( + vec![audio_track, video_track], + None, + ) + .await; + + let mut constraints = MediaStreamSettings::new(); + if add_audio { + constraints.audio(AudioTrackConstraints::new()); + } + if add_video { + constraints.device_video(DeviceVideoTrackConstraints::new()); + } + + let room_handle = room.new_handle(); + room_handle.on_failed_local_media(closure.into()).unwrap(); + + let is_should_be_ok = + audio_required == add_audio || video_required == add_video; + assert_eq!( + JsFuture::from(room_handle.set_local_media_settings( + &constraints, + false, + false + )) + .await + .is_ok(), + is_should_be_ok, + "audio_required: {}; add_audio: {}; video_required: {}; \ + add_video: {}", + audio_required, + add_audio, + video_required, + add_video, + ); + + timeout(1000, test_rx) + .await + .map(|rx| rx.unwrap()) + .map_err(|_| ()) + } + + // on_failed_local_media callback does not fire + helper(true, false, true, false).await.unwrap_err(); + helper(false, true, false, true).await.unwrap_err(); + helper(false, false, false, false).await.unwrap_err(); + + // on_failed_local_media callback fires + helper(true, false, false, true).await.unwrap(); + helper(false, true, true, false).await.unwrap(); + helper(true, true, false, false).await.unwrap(); + } + + /// Tests that calling [`RoomHandle::set_local_media_settings`] updates + /// needed [`media_exchange_state::State`]s of the [`Sender`]s. + #[wasm_bindgen_test] + async fn set_local_media_stream_settings_updates_media_exchange_state() { + let (event_tx, event_rx) = mpsc::unbounded(); + let (room, mut commands_rx) = get_test_room(Box::pin(event_rx)); + let room_handle = room.new_handle(); + room_handle + .on_failed_local_media(js_sys::Function::new_no_args("")) + .unwrap(); + JsFuture::from(room_handle.set_local_media_settings( + &media_stream_settings(true, false), + false, + false, + )) + .await .unwrap(); - delay_for(10).await; - spawn_local(async move { - let err = - get_jason_error( + let (audio_track, video_track) = get_test_unrequired_tracks(); + event_tx + .unbounded_send(Event::PeerCreated { + peer_id: PeerId(1), + negotiation_role: NegotiationRole::Offerer, + tracks: vec![audio_track, video_track], + ice_servers: Vec::new(), + force_relay: false, + }) + .unwrap(); + delay_for(10).await; + + spawn_local(async move { + let err = get_constraints_update_exception( JsFuture::from(room_handle.set_local_media_settings( &media_stream_settings(true, true), + false, + false, )) .await .unwrap_err(), ); - assert_eq!(err.name(), "MediaConnections"); + let err = get_jason_error(err.error()); + assert_eq!(err.name(), "MediaConnections"); + assert_eq!( + err.message(), + "Some MediaConnectionsError: MediaState of Sender transits \ + into opposite to expected MediaExchangeState" + ); + }); + yield_now().await; + + while let Some(update_tracks_cmd) = commands_rx.next().await { + if let Command::UpdateTracks { + peer_id, + mut tracks_patches, + } = update_tracks_cmd + { + assert_eq!(peer_id, PeerId(1)); + let track_patch = tracks_patches.pop().unwrap(); + assert_eq!(track_patch.enabled, Some(true)); + assert!(tracks_patches.is_empty()); + break; + } + } + } + + /// Checks that [`RoomHandle::set_local_media_settings`] will disable media + /// types on fail. + #[wasm_bindgen_test] + async fn disables_on_fail_if_no_rollback() { + let (room, peer1, _peer2) = room_with_connected_peers().await; + + let mock_navigator = MockNavigator::new(); + mock_navigator.error_get_user_media("disables_on_fail".into()); + let err = get_constraints_update_exception( + JsFuture::from(room.new_handle().set_local_media_settings( + &media_settings_with_device_id(), + true, + false, + )) + .await + .unwrap_err(), + ); + mock_navigator.stop(); + + assert_eq!(&err.name(), "RecoveredException"); + let err = get_jason_error(err.recover_reason()); + + assert_eq!(err.name(), "CouldNotGetLocalMedia"); assert_eq!( err.message(), - "Some MediaConnectionsError: MediaState of Sender transits into \ - opposite to expected MediaExchangeState" + "Failed to get local tracks: MediaDevices.getUserMedia() failed: \ + Unknown JS error: disables_on_fail" ); - }); - yield_now().await; - while let Some(update_tracks_cmd) = commands_rx.next().await { - if let Command::UpdateTracks { - peer_id, - mut tracks_patches, - } = update_tracks_cmd - { - assert_eq!(peer_id, PeerId(1)); - let track_patch = tracks_patches.pop().unwrap(); - assert_eq!(track_patch.enabled, Some(true)); - assert!(tracks_patches.is_empty()); - break; - } + assert!(!peer1.is_send_video_enabled(Some(MediaSourceKind::Device))); + assert!(peer1.get_send_tracks().is_empty()); + } + + /// Checks that [`RoomHandle::set_local_media_settings`] will rollback + /// [`MediaStreamSettings`] to the previous one on fail. + #[wasm_bindgen_test] + async fn rollbacks_on_fail() { + let (room, peer1, _peer2) = room_with_connected_peers().await; + + JsFuture::from(room.new_handle().set_local_media_settings( + &media_stream_settings(true, true), + false, + false, + )) + .await + .unwrap(); + + let mock_navigator = MockNavigator::new(); + let err = get_constraints_update_exception( + JsFuture::from(room.new_handle().set_local_media_settings( + &media_settings_with_device_id(), + true, + true, + )) + .await + .unwrap_err(), + ); + mock_navigator.stop(); + + assert_eq!(err.name(), "RecoveredException"); + let recover_reason = get_jason_error(err.recover_reason()); + assert_eq!(recover_reason.name(), "CouldNotGetLocalMedia"); + + assert_eq!(mock_navigator.get_user_media_requests_count(), 2); + + assert!(peer1.is_send_video_enabled(Some(MediaSourceKind::Device))); + assert_eq!(peer1.get_send_tracks().len(), 1); + } + + /// Checks that [`RoomHandle::set_local_media_settings`] will disable media + /// types on rollback fail. + #[wasm_bindgen_test] + async fn disables_on_rollback_fail() { + let (room, peer1, _peer2) = room_with_connected_peers().await; + + JsFuture::from(room.new_handle().set_local_media_settings( + &media_stream_settings(true, true), + false, + false, + )) + .await + .unwrap(); + + let mock_navigator = MockNavigator::new(); + mock_navigator.error_get_user_media("disables_on_rollback_fail".into()); + let err = get_constraints_update_exception( + JsFuture::from(room.new_handle().set_local_media_settings( + &media_settings_with_device_id(), + true, + true, + )) + .await + .unwrap_err(), + ); + mock_navigator.stop(); + + assert_eq!(err.name(), "RecoverFailedException"); + let recover_reason = get_jason_error(err.recover_reason()); + assert_eq!(recover_reason.name(), "CouldNotGetLocalMedia"); + assert_eq!( + recover_reason.message(), + "Failed to get local tracks: MediaDevices.getUserMedia() failed: \ + Unknown JS error: disables_on_rollback_fail" + ); + let recover_fail_reasons = + js_sys::Array::from(&err.recover_fail_reasons()); + assert_eq!(recover_fail_reasons.length(), 1); + let recover_fail_reason = get_jason_error(recover_fail_reasons.pop()); + assert_eq!( + recover_fail_reason.message(), + "Failed to get local tracks: MediaDevices.getUserMedia() failed: \ + Unknown JS error: disables_on_rollback_fail" + ); + assert_eq!(recover_fail_reason.name(), "CouldNotGetLocalMedia"); + + assert!(!peer1.is_send_video_enabled(Some(MediaSourceKind::Device))); + assert!(peer1.get_send_tracks().is_empty()); + } + + /// Checks that [`RoomHandle::set_local_media_settings`] with `stop_first` + /// set to `false` will not disable media types on rollback fail. + #[wasm_bindgen_test] + async fn doesnt_disables_if_not_stop_first() { + let (room, peer1, _peer2) = room_with_connected_peers().await; + + JsFuture::from(room.new_handle().set_local_media_settings( + &media_stream_settings(true, true), + false, + false, + )) + .await + .unwrap(); + + let mock_navigator = MockNavigator::new(); + mock_navigator + .error_get_user_media("doesnt_disables_if_not_stop_first".into()); + + let err = get_constraints_update_exception( + JsFuture::from(room.new_handle().set_local_media_settings( + &media_settings_with_device_id(), + false, + true, + )) + .await + .unwrap_err(), + ); + mock_navigator.stop(); + + assert_eq!(err.name(), "RecoveredException"); + let err = get_jason_error(err.recover_reason()); + assert_eq!(err.name(), "CouldNotGetLocalMedia"); + + assert!(peer1.is_send_video_enabled(Some(MediaSourceKind::Device))); + assert_eq!(peer1.get_send_tracks().len(), 1); } } diff --git a/jason/tests/web.rs b/jason/tests/web.rs index 3502501b6..ea4d9078d 100644 --- a/jason/tests/web.rs +++ b/jason/tests/web.rs @@ -87,6 +87,7 @@ use medea_client_api_proto::{ TrackId, VideoSettings, }; use medea_jason::{ + api::ConstraintsUpdateException, media::{track::remote, LocalTracksConstraints, MediaKind, MediaManager}, peer::media_exchange_state, rpc::ApiUrl, @@ -141,6 +142,15 @@ extern "C" { fn get_jason_error(err: JsValue) -> JasonError; } +#[wasm_bindgen( + inline_js = "export const get_constraints_update_exception = (e) => e;" +)] +extern "C" { + fn get_constraints_update_exception( + err: JsValue, + ) -> ConstraintsUpdateException; +} + pub fn get_test_required_tracks() -> (Track, Track) { get_test_tracks(true, true) }